src/automata@rrule@validator.erl

-module(automata@rrule@validator).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/rrule/validator.gleam").
-export([frequency/1, interval/1, end_condition/1, by_day/1, by_month/1, by_month_day/1, by_hour/1, by_minute/1, validate/1, to_string/1]).
-export_type([frequency/0, weekday_specifier/0, until/0, end_condition/0, rule_part/0, valid_r_rule/0, validation_error/0, partial_rule/0]).

-type frequency() :: daily | weekly | monthly | yearly.

-type weekday_specifier() :: {every_weekday, automata@schedule@ast:weekday()} |
    {nth_weekday, integer(), automata@schedule@ast:weekday()}.

-type until() :: {until_date, automata@schedule@ast:date()} |
    {until_date_time, automata@schedule@ast:date_time()}.

-type end_condition() :: forever | {count, integer()} | {until, until()}.

-type rule_part() :: freq_part |
    interval_part |
    count_part |
    until_part |
    by_day_part |
    by_month_part |
    by_month_day_part |
    by_hour_part |
    by_minute_part.

-opaque valid_r_rule() :: {valid_r_rule,
        frequency(),
        integer(),
        end_condition(),
        gleam@option:option(list(weekday_specifier())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer()))}.

-type validation_error() :: {missing_part, rule_part()} |
    {duplicate_part, rule_part()} |
    {unknown_part, binary()} |
    {unsupported_part, binary()} |
    {invalid_number, rule_part(), binary()} |
    {invalid_part_value, rule_part(), binary()} |
    {invalid_list, rule_part(), binary()} |
    {out_of_range, rule_part(), integer(), integer(), integer()} |
    {must_be_positive, rule_part(), integer()} |
    {mutually_exclusive_parts, rule_part(), rule_part()} |
    {numeric_weekday_requires_monthly_or_yearly, frequency()} |
    {impossible_date, binary(), binary()} |
    {incompatible_frequency_and_part, frequency(), rule_part()}.

-type partial_rule() :: {partial_rule,
        gleam@option:option(frequency()),
        gleam@option:option(integer()),
        end_condition(),
        gleam@option:option(list(weekday_specifier())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer())),
        gleam@option:option(list(integer()))}.

-file("src/automata/rrule/validator.gleam", 60).
-spec frequency(valid_r_rule()) -> frequency().
frequency(Spec) ->
    erlang:element(2, Spec).

-file("src/automata/rrule/validator.gleam", 64).
-spec interval(valid_r_rule()) -> integer().
interval(Spec) ->
    erlang:element(3, Spec).

-file("src/automata/rrule/validator.gleam", 68).
-spec end_condition(valid_r_rule()) -> end_condition().
end_condition(Spec) ->
    erlang:element(4, Spec).

-file("src/automata/rrule/validator.gleam", 72).
-spec by_day(valid_r_rule()) -> gleam@option:option(list(weekday_specifier())).
by_day(Spec) ->
    erlang:element(5, Spec).

-file("src/automata/rrule/validator.gleam", 76).
-spec by_month(valid_r_rule()) -> gleam@option:option(list(integer())).
by_month(Spec) ->
    erlang:element(6, Spec).

-file("src/automata/rrule/validator.gleam", 80).
-spec by_month_day(valid_r_rule()) -> gleam@option:option(list(integer())).
by_month_day(Spec) ->
    erlang:element(7, Spec).

-file("src/automata/rrule/validator.gleam", 84).
-spec by_hour(valid_r_rule()) -> gleam@option:option(list(integer())).
by_hour(Spec) ->
    erlang:element(8, Spec).

-file("src/automata/rrule/validator.gleam", 88).
-spec by_minute(valid_r_rule()) -> gleam@option:option(list(integer())).
by_minute(Spec) ->
    erlang:element(9, Spec).

-file("src/automata/rrule/validator.gleam", 150).
-spec empty_partial_rule() -> partial_rule().
empty_partial_rule() ->
    {partial_rule, none, none, forever, none, none, none, none, none}.

-file("src/automata/rrule/validator.gleam", 264).
-spec set_optional(
    gleam@option:option(FYP),
    {ok, FYP} | {error, validation_error()},
    rule_part(),
    fun((gleam@option:option(FYP)) -> partial_rule())
) -> {ok, partial_rule()} | {error, validation_error()}.
set_optional(Current, Parsed, Part, Setter) ->
    case Parsed of
        {error, Error} ->
            {error, Error};

        {ok, Value} ->
            case Current of
                none ->
                    {ok, Setter({some, Value})};

                {some, _} ->
                    {error, {duplicate_part, Part}}
            end
    end.

-file("src/automata/rrule/validator.gleam", 334).
-spec validate_frequency_compatibility(valid_r_rule()) -> {ok, nil} |
    {error, validation_error()}.
validate_frequency_compatibility(Spec) ->
    case {erlang:element(2, Spec), erlang:element(7, Spec)} of
        {weekly, {some, _}} ->
            {error,
                {incompatible_frequency_and_part, weekly, by_month_day_part}};

        {_, _} ->
            {ok, nil}
    end.

-file("src/automata/rrule/validator.gleam", 396).
-spec ordinal_within_bound(integer(), integer()) -> boolean().
ordinal_within_bound(Ordinal, Bound) ->
    ((Ordinal /= 0) andalso (Ordinal >= (0 - Bound))) andalso (Ordinal =< Bound).

-file("src/automata/rrule/validator.gleam", 417).
-spec day_matches_month_day(integer(), integer()) -> boolean().
day_matches_month_day(Day, Maximum) ->
    case Day > 0 of
        true ->
            Day =< Maximum;

        false ->
            (0 - Day) =< Maximum
    end.

-file("src/automata/rrule/validator.gleam", 400).
-spec impossible_month_day(
    gleam@option:option(list(integer())),
    gleam@option:option(list(integer()))
) -> boolean().
impossible_month_day(By_month, By_month_day) ->
    case {By_month, By_month_day} of
        {{some, Months}, {some, Days}} ->
            not gleam@list:any(
                Months,
                fun(Month) ->
                    Maximum = automata@internal@calendar:days_in_month(
                        2024,
                        Month
                    ),
                    gleam@list:any(
                        Days,
                        fun(Day) -> day_matches_month_day(Day, Maximum) end
                    )
                    orelse gleam@list:any(
                        Days,
                        fun(Day@1) ->
                            day_matches_month_day(
                                Day@1,
                                automata@internal@calendar:days_in_month(
                                    2025,
                                    Month
                                )
                            )
                        end
                    )
                end
            );

        {_, _} ->
            false
    end.

-file("src/automata/rrule/validator.gleam", 424).
-spec parse_frequency(binary()) -> {ok, frequency()} |
    {error, validation_error()}.
parse_frequency(Value) ->
    case string:uppercase(Value) of
        <<"DAILY"/utf8>> ->
            {ok, daily};

        <<"WEEKLY"/utf8>> ->
            {ok, weekly};

        <<"MONTHLY"/utf8>> ->
            {ok, monthly};

        <<"YEARLY"/utf8>> ->
            {ok, yearly};

        _ ->
            {error, {invalid_part_value, freq_part, Value}}
    end.

-file("src/automata/rrule/validator.gleam", 434).
-spec parse_positive_int(rule_part(), binary()) -> {ok, integer()} |
    {error, validation_error()}.
parse_positive_int(Part, Value) ->
    case gleam_stdlib:parse_int(Value) of
        {error, _} ->
            {error, {invalid_number, Part, Value}};

        {ok, Number} ->
            case Number > 0 of
                true ->
                    {ok, Number};

                false ->
                    {error, {must_be_positive, Part, Number}}
            end
    end.

-file("src/automata/rrule/validator.gleam", 560).
-spec parse_weekday(binary()) -> {ok, automata@schedule@ast:weekday()} |
    {error, nil}.
parse_weekday(Value) ->
    case Value of
        <<"MO"/utf8>> ->
            {ok, monday};

        <<"TU"/utf8>> ->
            {ok, tuesday};

        <<"WE"/utf8>> ->
            {ok, wednesday};

        <<"TH"/utf8>> ->
            {ok, thursday};

        <<"FR"/utf8>> ->
            {ok, friday};

        <<"SA"/utf8>> ->
            {ok, saturday};

        <<"SU"/utf8>> ->
            {ok, sunday};

        _ ->
            {error, nil}
    end.

-file("src/automata/rrule/validator.gleam", 529).
-spec parse_weekday_specifier(binary()) -> {ok, weekday_specifier()} |
    {error, nil}.
parse_weekday_specifier(Value) ->
    Upper = string:uppercase(Value),
    Length = string:length(Upper),
    case Length < 2 of
        true ->
            {error, nil};

        false ->
            Prefix_length = Length - 2,
            Prefix = gleam@string:slice(Upper, 0, Prefix_length),
            Suffix = gleam@string:slice(Upper, Prefix_length, 2),
            case parse_weekday(Suffix) of
                {error, nil} ->
                    {error, nil};

                {ok, Weekday} ->
                    case Prefix of
                        <<""/utf8>> ->
                            {ok, {every_weekday, Weekday}};

                        _ ->
                            case gleam_stdlib:parse_int(Prefix) of
                                {ok, Ordinal} ->
                                    case Ordinal =:= 0 of
                                        true ->
                                            {error, nil};

                                        false ->
                                            {ok,
                                                {nth_weekday, Ordinal, Weekday}}
                                    end;

                                {error, _} ->
                                    {error, nil}
                            end
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 514).
-spec collect_weekdays(list(binary()), list(weekday_specifier()), binary()) -> {ok,
        list(weekday_specifier())} |
    {error, validation_error()}.
collect_weekdays(Values, Acc, Original) ->
    case Values of
        [] ->
            {ok, lists:reverse(Acc)};

        [Value | Rest] ->
            case parse_weekday_specifier(Value) of
                {ok, Item} ->
                    collect_weekdays(Rest, [Item | Acc], Original);

                {error, nil} ->
                    {error, {invalid_part_value, by_day_part, Original}}
            end
    end.

-file("src/automata/rrule/validator.gleam", 505).
-spec parse_weekday_specifiers(binary()) -> {ok, list(weekday_specifier())} |
    {error, validation_error()}.
parse_weekday_specifiers(Value) ->
    case gleam@list:any(
        gleam@string:split(Value, <<","/utf8>>),
        fun gleam@string:is_empty/1
    ) of
        true ->
            {error, {invalid_list, by_day_part, Value}};

        false ->
            collect_weekdays(gleam@string:split(Value, <<","/utf8>>), [], Value)
    end.

-file("src/automata/rrule/validator.gleam", 654).
-spec parse_fixed_int(binary(), integer(), integer()) -> {ok, integer()} |
    {error, nil}.
parse_fixed_int(Value, Start, Length) ->
    gleam_stdlib:parse_int(gleam@string:slice(Value, Start, Length)).

-file("src/automata/rrule/validator.gleam", 596).
-spec parse_date(binary()) -> {ok, automata@schedule@ast:date()} |
    {error, validation_error()}.
parse_date(Value) ->
    case parse_fixed_int(Value, 0, 4) of
        {error, _} ->
            {error, {invalid_part_value, until_part, Value}};

        {ok, Year} ->
            case parse_fixed_int(Value, 4, 2) of
                {error, _} ->
                    {error, {invalid_part_value, until_part, Value}};

                {ok, Month} ->
                    case parse_fixed_int(Value, 6, 2) of
                        {error, _} ->
                            {error, {invalid_part_value, until_part, Value}};

                        {ok, Day} ->
                            Date = {date, Year, Month, Day},
                            case automata@internal@calendar:is_valid_date(Date) of
                                true ->
                                    {ok, Date};

                                false ->
                                    {error,
                                        {invalid_part_value, until_part, Value}}
                            end
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 617).
-spec parse_datetime(binary()) -> {ok, automata@schedule@ast:date_time()} |
    {error, validation_error()}.
parse_datetime(Value) ->
    case parse_date(gleam@string:slice(Value, 0, 8)) of
        {error, Error} ->
            {error, Error};

        {ok, Date} ->
            case parse_fixed_int(Value, 9, 2) of
                {error, _} ->
                    {error, {invalid_part_value, until_part, Value}};

                {ok, Hour} ->
                    case parse_fixed_int(Value, 11, 2) of
                        {error, _} ->
                            {error, {invalid_part_value, until_part, Value}};

                        {ok, Minute} ->
                            case parse_fixed_int(Value, 13, 2) of
                                {error, _} ->
                                    {error,
                                        {invalid_part_value, until_part, Value}};

                                {ok, Second} ->
                                    Datetime = {date_time,
                                        Date,
                                        {time, Hour, Minute, Second}},
                                    case automata@internal@calendar:is_valid_datetime(
                                        Datetime
                                    ) of
                                        true ->
                                            {ok, Datetime};

                                        false ->
                                            {error,
                                                {invalid_part_value,
                                                    until_part,
                                                    Value}}
                                    end
                            end
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 573).
-spec parse_until(binary()) -> {ok, until()} | {error, validation_error()}.
parse_until(Value) ->
    case string:length(Value) of
        8 ->
            case parse_date(Value) of
                {ok, Date} ->
                    {ok, {until_date, Date}};

                {error, Error} ->
                    {error, Error}
            end;

        16 ->
            case (gleam@string:slice(Value, 8, 1) =:= <<"T"/utf8>>) andalso (gleam@string:slice(
                Value,
                15,
                1
            )
            =:= <<"Z"/utf8>>) of
                false ->
                    {error, {invalid_part_value, until_part, Value}};

                true ->
                    case parse_datetime(Value) of
                        {ok, Datetime} ->
                            {ok, {until_date_time, Datetime}};

                        {error, Error@1} ->
                            {error, Error@1}
                    end
            end;

        _ ->
            {error, {invalid_part_value, until_part, Value}}
    end.

-file("src/automata/rrule/validator.gleam", 691).
-spec weekday_to_string(automata@schedule@ast:weekday()) -> binary().
weekday_to_string(Day) ->
    case Day of
        monday ->
            <<"MO"/utf8>>;

        tuesday ->
            <<"TU"/utf8>>;

        wednesday ->
            <<"WE"/utf8>>;

        thursday ->
            <<"TH"/utf8>>;

        friday ->
            <<"FR"/utf8>>;

        saturday ->
            <<"SA"/utf8>>;

        sunday ->
            <<"SU"/utf8>>
    end.

-file("src/automata/rrule/validator.gleam", 374).
-spec validate_ordinal_range(list(weekday_specifier()), integer()) -> {ok, nil} |
    {error, validation_error()}.
validate_ordinal_range(Values, Bound) ->
    case Values of
        [] ->
            {ok, nil};

        [Item | Rest] ->
            case Item of
                {nth_weekday, Ordinal, Day} ->
                    case ordinal_within_bound(Ordinal, Bound) of
                        true ->
                            validate_ordinal_range(Rest, Bound);

                        false ->
                            {error,
                                {invalid_part_value,
                                    by_day_part,
                                    <<(erlang:integer_to_binary(Ordinal))/binary,
                                        (weekday_to_string(Day))/binary>>}}
                    end;

                {every_weekday, _} ->
                    validate_ordinal_range(Rest, Bound)
            end
    end.

-file("src/automata/rrule/validator.gleam", 347).
-spec validate_weekday_ordinals(
    frequency(),
    gleam@option:option(list(weekday_specifier()))
) -> {ok, nil} | {error, validation_error()}.
validate_weekday_ordinals(Frequency, By_day) ->
    case By_day of
        none ->
            {ok, nil};

        {some, Values} ->
            case gleam@list:any(Values, fun(Item) -> case Item of
                        {every_weekday, _} ->
                            false;

                        {nth_weekday, _, _} ->
                            true
                    end end) of
                false ->
                    {ok, nil};

                true ->
                    case Frequency of
                        monthly ->
                            validate_ordinal_range(Values, 5);

                        yearly ->
                            validate_ordinal_range(Values, 53);

                        _ ->
                            {error,
                                {numeric_weekday_requires_monthly_or_yearly,
                                    Frequency}}
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 684).
-spec weekday_specifier_to_string(weekday_specifier()) -> binary().
weekday_specifier_to_string(Specifier) ->
    case Specifier of
        {every_weekday, Day} ->
            weekday_to_string(Day);

        {nth_weekday, Ordinal, Day@1} ->
            <<(erlang:integer_to_binary(Ordinal))/binary,
                (weekday_to_string(Day@1))/binary>>
    end.

-file("src/automata/rrule/validator.gleam", 670).
-spec append_weekday_part(
    list(binary()),
    gleam@option:option(list(weekday_specifier()))
) -> list(binary()).
append_weekday_part(Parts, Values) ->
    case Values of
        none ->
            Parts;

        {some, Items} ->
            lists:append(
                Parts,
                [<<"BYDAY="/utf8,
                        (gleam@string:join(
                            gleam@list:map(
                                Items,
                                fun weekday_specifier_to_string/1
                            ),
                            <<","/utf8>>
                        ))/binary>>]
            )
    end.

-file("src/automata/rrule/validator.gleam", 703).
-spec frequency_to_string(frequency()) -> binary().
frequency_to_string(Frequency) ->
    case Frequency of
        daily ->
            <<"DAILY"/utf8>>;

        weekly ->
            <<"WEEKLY"/utf8>>;

        monthly ->
            <<"MONTHLY"/utf8>>;

        yearly ->
            <<"YEARLY"/utf8>>
    end.

-file("src/automata/rrule/validator.gleam", 732).
-spec int_list_to_string(list(integer())) -> binary().
int_list_to_string(Items) ->
    gleam@string:join(
        gleam@list:map(Items, fun erlang:integer_to_binary/1),
        <<","/utf8>>
    ).

-file("src/automata/rrule/validator.gleam", 307).
-spec validate_semantics(valid_r_rule()) -> {ok, nil} |
    {error, validation_error()}.
validate_semantics(Spec) ->
    case validate_frequency_compatibility(Spec) of
        {error, Error} ->
            {error, Error};

        {ok, _} ->
            case validate_weekday_ordinals(
                erlang:element(2, Spec),
                erlang:element(5, Spec)
            ) of
                {error, Error@1} ->
                    {error, Error@1};

                {ok, _} ->
                    case impossible_month_day(
                        erlang:element(6, Spec),
                        erlang:element(7, Spec)
                    ) of
                        true ->
                            {error,
                                {impossible_date,
                                    case erlang:element(6, Spec) of
                                        {some, Values} ->
                                            int_list_to_string(Values);

                                        none ->
                                            <<"*"/utf8>>
                                    end,
                                    case erlang:element(7, Spec) of
                                        {some, Values@1} ->
                                            int_list_to_string(Values@1);

                                        none ->
                                            <<"*"/utf8>>
                                    end}};

                        false ->
                            {ok, nil}
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 280).
-spec finalize(partial_rule()) -> {ok, valid_r_rule()} |
    {error, validation_error()}.
finalize(Partial) ->
    case erlang:element(2, Partial) of
        none ->
            {error, {missing_part, freq_part}};

        {some, Frequency} ->
            Spec = {valid_r_rule, Frequency, case erlang:element(3, Partial) of
                    none ->
                        1;

                    {some, Interval} ->
                        Interval
                end, erlang:element(4, Partial), erlang:element(5, Partial), erlang:element(
                    6,
                    Partial
                ), erlang:element(7, Partial), erlang:element(8, Partial), erlang:element(
                    9,
                    Partial
                )},
            case validate_semantics(Spec) of
                {ok, _} ->
                    {ok, Spec};

                {error, Error} ->
                    {error, Error}
            end
    end.

-file("src/automata/rrule/validator.gleam", 658).
-spec append_int_part(
    list(binary()),
    binary(),
    gleam@option:option(list(integer()))
) -> list(binary()).
append_int_part(Parts, Name, Values) ->
    case Values of
        none ->
            Parts;

        {some, Items} ->
            lists:append(
                Parts,
                [<<<<Name/binary, "="/utf8>>/binary,
                        (int_list_to_string(Items))/binary>>]
            )
    end.

-file("src/automata/rrule/validator.gleam", 743).
-spec dedup_sorted(list(integer()), integer(), list(integer())) -> list(integer()).
dedup_sorted(Rest, Previous, Acc) ->
    case Rest of
        [] ->
            lists:reverse(Acc);

        [Value | Tail] ->
            case Value =:= Previous of
                true ->
                    dedup_sorted(Tail, Previous, Acc);

                false ->
                    dedup_sorted(Tail, Value, [Value | Acc])
            end
    end.

-file("src/automata/rrule/validator.gleam", 736).
-spec dedup_sort(list(integer()), list(integer())) -> list(integer()).
dedup_sort(Values, Acc) ->
    case gleam@list:sort(Values, fun gleam@int:compare/2) of
        [] ->
            lists:reverse(Acc);

        [First | Rest] ->
            dedup_sorted(Rest, First, [First | Acc])
    end.

-file("src/automata/rrule/validator.gleam", 469).
-spec parse_int_items(
    list(binary()),
    rule_part(),
    integer(),
    integer(),
    boolean(),
    list(integer())
) -> {ok, list(integer())} | {error, validation_error()}.
parse_int_items(Values, Part, Min, Max, Reject_zero, Acc) ->
    case Values of
        [] ->
            {ok, dedup_sort(lists:reverse(Acc), [])};

        [Value | Rest] ->
            case gleam_stdlib:parse_int(Value) of
                {error, _} ->
                    {error, {invalid_number, Part, Value}};

                {ok, Number} ->
                    case Reject_zero andalso (Number =:= 0) of
                        true ->
                            {error, {invalid_part_value, Part, Value}};

                        false ->
                            case (Number < Min) orelse (Number > Max) of
                                true ->
                                    {error,
                                        {out_of_range, Part, Min, Max, Number}};

                                false ->
                                    parse_int_items(
                                        Rest,
                                        Part,
                                        Min,
                                        Max,
                                        Reject_zero,
                                        [Number | Acc]
                                    )
                            end
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 448).
-spec parse_int_list(rule_part(), binary(), integer(), integer(), boolean()) -> {ok,
        list(integer())} |
    {error, validation_error()}.
parse_int_list(Part, Value, Min, Max, Reject_zero) ->
    case gleam@list:any(
        gleam@string:split(Value, <<","/utf8>>),
        fun gleam@string:is_empty/1
    ) of
        true ->
            {error, {invalid_list, Part, Value}};

        false ->
            parse_int_items(
                gleam@string:split(Value, <<","/utf8>>),
                Part,
                Min,
                Max,
                Reject_zero,
                []
            )
    end.

-file("src/automata/rrule/validator.gleam", 177).
-spec apply_part(binary(), binary(), partial_rule()) -> {ok, partial_rule()} |
    {error, validation_error()}.
apply_part(Name, Value, Partial) ->
    case Name of
        <<"FREQ"/utf8>> ->
            set_optional(
                erlang:element(2, Partial),
                parse_frequency(Value),
                freq_part,
                fun(Updated) ->
                    {partial_rule,
                        Updated,
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        erlang:element(6, Partial),
                        erlang:element(7, Partial),
                        erlang:element(8, Partial),
                        erlang:element(9, Partial)}
                end
            );

        <<"INTERVAL"/utf8>> ->
            set_optional(
                erlang:element(3, Partial),
                parse_positive_int(interval_part, Value),
                interval_part,
                fun(Updated@1) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        Updated@1,
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        erlang:element(6, Partial),
                        erlang:element(7, Partial),
                        erlang:element(8, Partial),
                        erlang:element(9, Partial)}
                end
            );

        <<"COUNT"/utf8>> ->
            case parse_positive_int(count_part, Value) of
                {error, Error} ->
                    {error, Error};

                {ok, Count} ->
                    case erlang:element(4, Partial) of
                        forever ->
                            {ok,
                                {partial_rule,
                                    erlang:element(2, Partial),
                                    erlang:element(3, Partial),
                                    {count, Count},
                                    erlang:element(5, Partial),
                                    erlang:element(6, Partial),
                                    erlang:element(7, Partial),
                                    erlang:element(8, Partial),
                                    erlang:element(9, Partial)}};

                        {count, _} ->
                            {error, {duplicate_part, count_part}};

                        {until, _} ->
                            {error,
                                {mutually_exclusive_parts,
                                    count_part,
                                    until_part}}
                    end
            end;

        <<"UNTIL"/utf8>> ->
            case parse_until(Value) of
                {error, Error@1} ->
                    {error, Error@1};

                {ok, Until} ->
                    case erlang:element(4, Partial) of
                        forever ->
                            {ok,
                                {partial_rule,
                                    erlang:element(2, Partial),
                                    erlang:element(3, Partial),
                                    {until, Until},
                                    erlang:element(5, Partial),
                                    erlang:element(6, Partial),
                                    erlang:element(7, Partial),
                                    erlang:element(8, Partial),
                                    erlang:element(9, Partial)}};

                        {count, _} ->
                            {error,
                                {mutually_exclusive_parts,
                                    count_part,
                                    until_part}};

                        {until, _} ->
                            {error, {duplicate_part, until_part}}
                    end
            end;

        <<"BYDAY"/utf8>> ->
            set_optional(
                erlang:element(5, Partial),
                parse_weekday_specifiers(Value),
                by_day_part,
                fun(Updated@2) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        Updated@2,
                        erlang:element(6, Partial),
                        erlang:element(7, Partial),
                        erlang:element(8, Partial),
                        erlang:element(9, Partial)}
                end
            );

        <<"BYMONTH"/utf8>> ->
            set_optional(
                erlang:element(6, Partial),
                parse_int_list(by_month_part, Value, 1, 12, false),
                by_month_part,
                fun(Updated@3) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        Updated@3,
                        erlang:element(7, Partial),
                        erlang:element(8, Partial),
                        erlang:element(9, Partial)}
                end
            );

        <<"BYMONTHDAY"/utf8>> ->
            set_optional(
                erlang:element(7, Partial),
                parse_int_list(by_month_day_part, Value, -31, 31, true),
                by_month_day_part,
                fun(Updated@4) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        erlang:element(6, Partial),
                        Updated@4,
                        erlang:element(8, Partial),
                        erlang:element(9, Partial)}
                end
            );

        <<"BYHOUR"/utf8>> ->
            set_optional(
                erlang:element(8, Partial),
                parse_int_list(by_hour_part, Value, 0, 23, false),
                by_hour_part,
                fun(Updated@5) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        erlang:element(6, Partial),
                        erlang:element(7, Partial),
                        Updated@5,
                        erlang:element(9, Partial)}
                end
            );

        <<"BYMINUTE"/utf8>> ->
            set_optional(
                erlang:element(9, Partial),
                parse_int_list(by_minute_part, Value, 0, 59, false),
                by_minute_part,
                fun(Updated@6) ->
                    {partial_rule,
                        erlang:element(2, Partial),
                        erlang:element(3, Partial),
                        erlang:element(4, Partial),
                        erlang:element(5, Partial),
                        erlang:element(6, Partial),
                        erlang:element(7, Partial),
                        erlang:element(8, Partial),
                        Updated@6}
                end
            );

        <<"BYSECOND"/utf8>> ->
            {error, {unsupported_part, <<"BYSECOND"/utf8>>}};

        <<"BYYEARDAY"/utf8>> ->
            {error, {unsupported_part, <<"BYYEARDAY"/utf8>>}};

        <<"BYWEEKNO"/utf8>> ->
            {error, {unsupported_part, <<"BYWEEKNO"/utf8>>}};

        <<"BYSETPOS"/utf8>> ->
            {error, {unsupported_part, <<"BYSETPOS"/utf8>>}};

        <<"WKST"/utf8>> ->
            {error, {unsupported_part, <<"WKST"/utf8>>}};

        <<"BYEASTER"/utf8>> ->
            {error, {unsupported_part, <<"BYEASTER"/utf8>>}};

        _ ->
            {error, {unknown_part, Name}}
    end.

-file("src/automata/rrule/validator.gleam", 163).
-spec collect(list(automata@rrule@ast:raw_rule_part()), partial_rule()) -> {ok,
        valid_r_rule()} |
    {error, validation_error()}.
collect(Parts, Partial) ->
    case Parts of
        [] ->
            finalize(Partial);

        [{raw_rule_part, Name, Value} | Rest] ->
            case apply_part(Name, Value, Partial) of
                {ok, Next} ->
                    collect(Rest, Next);

                {error, Error} ->
                    {error, Error}
            end
    end.

-file("src/automata/rrule/validator.gleam", 121).
-spec validate(automata@rrule@ast:raw_r_rule()) -> {ok, valid_r_rule()} |
    {error, validation_error()}.
validate(Raw) ->
    collect(erlang:element(2, Raw), empty_partial_rule()).

-file("src/automata/rrule/validator.gleam", 754).
-spec pad2(integer()) -> binary().
pad2(Value) ->
    case Value < 10 of
        true ->
            <<"0"/utf8, (erlang:integer_to_binary(Value))/binary>>;

        false ->
            erlang:integer_to_binary(Value)
    end.

-file("src/automata/rrule/validator.gleam", 761).
-spec pad4(integer()) -> binary().
pad4(Value) ->
    case Value < 10 of
        true ->
            <<"000"/utf8, (erlang:integer_to_binary(Value))/binary>>;

        false ->
            case Value < 100 of
                true ->
                    <<"00"/utf8, (erlang:integer_to_binary(Value))/binary>>;

                false ->
                    case Value < 1000 of
                        true ->
                            <<"0"/utf8,
                                (erlang:integer_to_binary(Value))/binary>>;

                        false ->
                            erlang:integer_to_binary(Value)
                    end
            end
    end.

-file("src/automata/rrule/validator.gleam", 719).
-spec date_to_string(automata@schedule@ast:date()) -> binary().
date_to_string(Date) ->
    <<<<(pad4(erlang:element(2, Date)))/binary,
            (pad2(erlang:element(3, Date)))/binary>>/binary,
        (pad2(erlang:element(4, Date)))/binary>>.

-file("src/automata/rrule/validator.gleam", 723).
-spec datetime_to_string(automata@schedule@ast:date_time()) -> binary().
datetime_to_string(Datetime) ->
    <<<<<<<<<<(date_to_string(erlang:element(2, Datetime)))/binary, "T"/utf8>>/binary,
                    (pad2(erlang:element(2, erlang:element(3, Datetime))))/binary>>/binary,
                (pad2(erlang:element(3, erlang:element(3, Datetime))))/binary>>/binary,
            (pad2(erlang:element(4, erlang:element(3, Datetime))))/binary>>/binary,
        "Z"/utf8>>.

-file("src/automata/rrule/validator.gleam", 712).
-spec until_to_string(until()) -> binary().
until_to_string(Until) ->
    case Until of
        {until_date, Date} ->
            date_to_string(Date);

        {until_date_time, Datetime} ->
            datetime_to_string(Datetime)
    end.

-file("src/automata/rrule/validator.gleam", 127).
-spec to_string(valid_r_rule()) -> binary().
to_string(Spec) ->
    Parts = [<<"FREQ="/utf8,
            (frequency_to_string(erlang:element(2, Spec)))/binary>>],
    Parts@1 = case erlang:element(4, Spec) of
        forever ->
            Parts;

        {count, Count} ->
            lists:append(
                Parts,
                [<<"COUNT="/utf8, (erlang:integer_to_binary(Count))/binary>>]
            );

        {until, Until} ->
            lists:append(
                Parts,
                [<<"UNTIL="/utf8, (until_to_string(Until))/binary>>]
            )
    end,
    Parts@2 = case erlang:element(3, Spec) =:= 1 of
        true ->
            Parts@1;

        false ->
            lists:append(
                Parts@1,
                [<<"INTERVAL="/utf8,
                        (erlang:integer_to_binary(erlang:element(3, Spec)))/binary>>]
            )
    end,
    Parts@3 = append_weekday_part(Parts@2, erlang:element(5, Spec)),
    Parts@4 = append_int_part(
        Parts@3,
        <<"BYMONTH"/utf8>>,
        erlang:element(6, Spec)
    ),
    Parts@5 = append_int_part(
        Parts@4,
        <<"BYMONTHDAY"/utf8>>,
        erlang:element(7, Spec)
    ),
    Parts@6 = append_int_part(
        Parts@5,
        <<"BYHOUR"/utf8>>,
        erlang:element(8, Spec)
    ),
    Parts@7 = append_int_part(
        Parts@6,
        <<"BYMINUTE"/utf8>>,
        erlang:element(9, Spec)
    ),
    gleam@string:join(Parts@7, <<";"/utf8>>).