src/automata@cron@validator.erl

-module(automata@cron@validator).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/cron/validator.gleam").
-export([minute/1, hour/1, day_of_month/1, month/1, day_of_week/1, selector_values/4, to_string/1, validate/1]).
-export_type([selector/0, item/0, step_base/0, valid_cron/0, validation_error/0, alias_mode/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-type selector() :: any | {values, list(item())}.

-type item() :: {exact, integer()} |
    {range, integer(), integer()} |
    {step, step_base(), integer()}.

-type step_base() :: step_any |
    {step_exact, integer()} |
    {step_range, integer(), integer()}.

-opaque valid_cron() :: {valid_cron,
        selector(),
        selector(),
        selector(),
        selector(),
        selector()}.

-type validation_error() :: {unsupported_syntax,
        automata@cron@ast:field(),
        binary()} |
    {invalid_number, automata@cron@ast:field(), binary()} |
    {invalid_alias, automata@cron@ast:field(), binary()} |
    {invalid_range, automata@cron@ast:field(), binary()} |
    {invalid_step, automata@cron@ast:field(), binary()} |
    {invalid_list, automata@cron@ast:field(), binary()} |
    {out_of_range, automata@cron@ast:field(), integer(), integer(), integer()} |
    {impossible_date, binary(), binary()}.

-type alias_mode() :: no_aliases | month_aliases | day_aliases.

-file("src/automata/cron/validator.gleam", 39).
-spec minute(valid_cron()) -> selector().
minute(Spec) ->
    erlang:element(2, Spec).

-file("src/automata/cron/validator.gleam", 43).
-spec hour(valid_cron()) -> selector().
hour(Spec) ->
    erlang:element(3, Spec).

-file("src/automata/cron/validator.gleam", 47).
-spec day_of_month(valid_cron()) -> selector().
day_of_month(Spec) ->
    erlang:element(4, Spec).

-file("src/automata/cron/validator.gleam", 51).
-spec month(valid_cron()) -> selector().
month(Spec) ->
    erlang:element(5, Spec).

-file("src/automata/cron/validator.gleam", 55).
-spec day_of_week(valid_cron()) -> selector().
day_of_week(Spec) ->
    erlang:element(6, Spec).

-file("src/automata/cron/validator.gleam", 143).
-spec day_of_week_is_any(selector()) -> boolean().
day_of_week_is_any(Selector) ->
    case Selector of
        any ->
            true;

        {values, _} ->
            false
    end.

-file("src/automata/cron/validator.gleam", 355).
?DOC(
    " Detect reserved Quartz-style extensions (`?`, `L`, `W`, `H`, `#`) that\n"
    " the validator does not support. Run only after `int.parse` and\n"
    " `parse_alias` have both failed, so that perfectly valid alias tokens\n"
    " like `WED`, `JUL`, `THU` (which contain `W`, `L`, `H` as substrings)\n"
    " still reach the alias resolver before being misclassified.\n"
).
-spec has_reserved_quartz_syntax(binary()) -> boolean().
has_reserved_quartz_syntax(Value) ->
    (((gleam_stdlib:contains_string(Value, <<"?"/utf8>>) orelse gleam_stdlib:contains_string(
        Value,
        <<"L"/utf8>>
    ))
    orelse gleam_stdlib:contains_string(Value, <<"W"/utf8>>))
    orelse gleam_stdlib:contains_string(Value, <<"#"/utf8>>))
    orelse gleam_stdlib:contains_string(Value, <<"H"/utf8>>).

-file("src/automata/cron/validator.gleam", 363).
-spec ensure_range(automata@cron@ast:field(), integer(), integer(), integer()) -> {ok,
        integer()} |
    {error, validation_error()}.
ensure_range(Field, Number, Min, Max) ->
    case (Number < Min) orelse (Number > Max) of
        true ->
            {error, {out_of_range, Field, Min, Max, Number}};

        false ->
            {ok, Number}
    end.

-file("src/automata/cron/validator.gleam", 375).
-spec parse_alias(automata@cron@ast:field(), binary(), alias_mode()) -> {ok,
        integer()} |
    {error, nil}.
parse_alias(_, Value, Aliases) ->
    Upper = string:uppercase(Value),
    case Aliases of
        no_aliases ->
            {error, nil};

        month_aliases ->
            case Upper of
                <<"JAN"/utf8>> ->
                    {ok, 1};

                <<"FEB"/utf8>> ->
                    {ok, 2};

                <<"MAR"/utf8>> ->
                    {ok, 3};

                <<"APR"/utf8>> ->
                    {ok, 4};

                <<"MAY"/utf8>> ->
                    {ok, 5};

                <<"JUN"/utf8>> ->
                    {ok, 6};

                <<"JUL"/utf8>> ->
                    {ok, 7};

                <<"AUG"/utf8>> ->
                    {ok, 8};

                <<"SEP"/utf8>> ->
                    {ok, 9};

                <<"OCT"/utf8>> ->
                    {ok, 10};

                <<"NOV"/utf8>> ->
                    {ok, 11};

                <<"DEC"/utf8>> ->
                    {ok, 12};

                _ ->
                    {error, nil}
            end;

        day_aliases ->
            case Upper of
                <<"SUN"/utf8>> ->
                    {ok, 0};

                <<"MON"/utf8>> ->
                    {ok, 1};

                <<"TUE"/utf8>> ->
                    {ok, 2};

                <<"WED"/utf8>> ->
                    {ok, 3};

                <<"THU"/utf8>> ->
                    {ok, 4};

                <<"FRI"/utf8>> ->
                    {ok, 5};

                <<"SAT"/utf8>> ->
                    {ok, 6};

                _ ->
                    {error, nil}
            end
    end.

-file("src/automata/cron/validator.gleam", 325).
-spec parse_value(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, integer()} | {error, validation_error()}.
parse_value(Field, Value, Min, Max, Aliases) ->
    case gleam_stdlib:parse_int(Value) of
        {ok, Number} ->
            ensure_range(Field, Number, Min, Max);

        {error, _} ->
            case parse_alias(Field, Value, Aliases) of
                {ok, Number@1} ->
                    ensure_range(Field, Number@1, Min, Max);

                {error, nil} ->
                    case has_reserved_quartz_syntax(Value) of
                        true ->
                            {error, {unsupported_syntax, Field, Value}};

                        false ->
                            case Aliases of
                                no_aliases ->
                                    {error, {invalid_number, Field, Value}};

                                _ ->
                                    {error, {invalid_alias, Field, Value}}
                            end
                    end
            end
    end.

-file("src/automata/cron/validator.gleam", 254).
-spec parse_step_base(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, step_base()} | {error, validation_error()}.
parse_step_base(Field, Base, Min, Max, Aliases) ->
    case Base of
        <<"*"/utf8>> ->
            {ok, step_any};

        _ ->
            case gleam_stdlib:contains_string(Base, <<"-"/utf8>>) of
                true ->
                    case gleam@string:split(Base, <<"-"/utf8>>) of
                        [Start_text, End_text] ->
                            case (Start_text =:= <<""/utf8>>) orelse (End_text
                            =:= <<""/utf8>>) of
                                true ->
                                    {error, {invalid_range, Field, Base}};

                                false ->
                                    case parse_value(
                                        Field,
                                        Start_text,
                                        Min,
                                        Max,
                                        Aliases
                                    ) of
                                        {error, Error} ->
                                            {error, Error};

                                        {ok, Start} ->
                                            case parse_value(
                                                Field,
                                                End_text,
                                                Min,
                                                Max,
                                                Aliases
                                            ) of
                                                {error, Error@1} ->
                                                    {error, Error@1};

                                                {ok, End} ->
                                                    case Start =< End of
                                                        true ->
                                                            {ok,
                                                                {step_range,
                                                                    Start,
                                                                    End}};

                                                        false ->
                                                            {error,
                                                                {invalid_range,
                                                                    Field,
                                                                    Base}}
                                                    end
                                            end
                                    end
                            end;

                        _ ->
                            {error, {invalid_range, Field, Base}}
                    end;

                false ->
                    case parse_value(Field, Base, Min, Max, Aliases) of
                        {ok, Value} ->
                            {ok, {step_exact, Value}};

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

-file("src/automata/cron/validator.gleam", 225).
-spec parse_step(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_step(Field, Part, Min, Max, Aliases) ->
    case gleam@string:split(Part, <<"/"/utf8>>) of
        [Base, Step_text] ->
            case (Base =:= <<""/utf8>>) orelse (Step_text =:= <<""/utf8>>) of
                true ->
                    {error, {invalid_step, Field, Part}};

                false ->
                    case gleam_stdlib:parse_int(Step_text) of
                        {error, _} ->
                            {error, {invalid_step, Field, Part}};

                        {ok, Step} ->
                            case Step > 0 of
                                false ->
                                    {error, {invalid_step, Field, Part}};

                                true ->
                                    case parse_step_base(
                                        Field,
                                        Base,
                                        Min,
                                        Max,
                                        Aliases
                                    ) of
                                        {ok, Step_base} ->
                                            {ok, {step, Step_base, Step}};

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

        _ ->
            {error, {invalid_step, Field, Part}}
    end.

-file("src/automata/cron/validator.gleam", 296).
-spec parse_range(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_range(Field, Part, Min, Max, Aliases) ->
    case gleam@string:split(Part, <<"-"/utf8>>) of
        [Start_text, End_text] ->
            case (Start_text =:= <<""/utf8>>) orelse (End_text =:= <<""/utf8>>) of
                true ->
                    {error, {invalid_range, Field, Part}};

                false ->
                    case parse_value(Field, Start_text, Min, Max, Aliases) of
                        {error, Error} ->
                            {error, Error};

                        {ok, Start} ->
                            case parse_value(Field, End_text, Min, Max, Aliases) of
                                {error, Error@1} ->
                                    {error, Error@1};

                                {ok, End} ->
                                    case Start =< End of
                                        true ->
                                            {ok, {range, Start, End}};

                                        false ->
                                            {error,
                                                {invalid_range, Field, Part}}
                                    end
                            end
                    end
            end;

        _ ->
            {error, {invalid_range, Field, Part}}
    end.

-file("src/automata/cron/validator.gleam", 204).
-spec parse_item(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_item(Field, Part, Min, Max, Aliases) ->
    case gleam_stdlib:contains_string(Part, <<"/"/utf8>>) of
        true ->
            parse_step(Field, Part, Min, Max, Aliases);

        false ->
            case gleam_stdlib:contains_string(Part, <<"-"/utf8>>) of
                true ->
                    parse_range(Field, Part, Min, Max, Aliases);

                false ->
                    case parse_value(Field, Part, Min, Max, Aliases) of
                        {ok, Value} ->
                            {ok, {exact, Value}};

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

-file("src/automata/cron/validator.gleam", 186).
-spec parse_items(
    list(binary()),
    automata@cron@ast:field(),
    integer(),
    integer(),
    alias_mode(),
    list(item())
) -> {ok, selector()} | {error, validation_error()}.
parse_items(Parts, Field, Min, Max, Aliases, Acc) ->
    case Parts of
        [] ->
            {ok, {values, lists:reverse(Acc)}};

        [Part | Rest] ->
            case parse_item(Field, Part, Min, Max, Aliases) of
                {ok, Item} ->
                    parse_items(Rest, Field, Min, Max, Aliases, [Item | Acc]);

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

-file("src/automata/cron/validator.gleam", 161).
-spec parse_selector(
    automata@cron@ast:field(),
    binary(),
    integer(),
    integer(),
    alias_mode()
) -> {ok, selector()} | {error, validation_error()}.
parse_selector(Field, Value, Min, Max, Aliases) ->
    case Value of
        <<"*"/utf8>> ->
            {ok, any};

        _ ->
            case gleam@list:any(
                gleam@string:split(Value, <<","/utf8>>),
                fun gleam@string:is_empty/1
            ) of
                true ->
                    {error, {invalid_list, Field, Value}};

                false ->
                    parse_items(
                        gleam@string:split(Value, <<","/utf8>>),
                        Field,
                        Min,
                        Max,
                        Aliases,
                        []
                    )
            end
    end.

-file("src/automata/cron/validator.gleam", 454).
-spec stepped_values(integer(), integer(), integer()) -> list(integer()).
stepped_values(Start, Stop, Step) ->
    case Start > Stop of
        true ->
            [];

        false ->
            [Start | stepped_values(Start + Step, Stop, Step)]
    end.

-file("src/automata/cron/validator.gleam", 461).
-spec inclusive_range(integer(), integer()) -> list(integer()).
inclusive_range(Start, Stop) ->
    case Start > Stop of
        true ->
            [];

        false ->
            [Start | inclusive_range(Start + 1, Stop)]
    end.

-file("src/automata/cron/validator.gleam", 441).
-spec item_values(item(), integer(), integer()) -> list(integer()).
item_values(Item, Min, Max) ->
    case Item of
        {exact, Value} ->
            [Value];

        {range, Start, End} ->
            inclusive_range(Start, End);

        {step, Base, Step} ->
            case Base of
                step_any ->
                    stepped_values(Min, Max, Step);

                {step_exact, Start@1} ->
                    stepped_values(Start@1, Max, Step);

                {step_range, Start@2, End@1} ->
                    stepped_values(Start@2, End@1, Step)
            end
    end.

-file("src/automata/cron/validator.gleam", 475).
-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/cron/validator.gleam", 468).
-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/cron/validator.gleam", 414).
-spec selector_values(selector(), integer(), integer(), boolean()) -> list(integer()).
selector_values(Selector, Min, Max, Normalize_day_of_week) ->
    Values = case Selector of
        any ->
            inclusive_range(Min, Max);

        {values, Items} ->
            _pipe = Items,
            gleam@list:flat_map(
                _pipe,
                fun(Item) -> item_values(Item, Min, Max) end
            )
    end,
    case Normalize_day_of_week of
        true ->
            _pipe@1 = Values,
            _pipe@2 = gleam@list:map(_pipe@1, fun(Value) -> case Value =:= 7 of
                        true ->
                            0;

                        false ->
                            Value
                    end end),
            dedup_sort(_pipe@2, []);

        false ->
            dedup_sort(Values, [])
    end.

-file("src/automata/cron/validator.gleam", 150).
-spec schedule_possible(selector(), selector()) -> boolean().
schedule_possible(Day_of_month, Month) ->
    Months = selector_values(Month, 1, 12, false),
    Days = selector_values(Day_of_month, 1, 31, false),
    gleam@list:any(
        Months,
        fun(Month@1) ->
            Maximum = automata@internal@calendar:days_in_month(2024, Month@1),
            gleam@list:any(Days, fun(Day) -> Day =< Maximum end) orelse gleam@list:any(
                Days,
                fun(Day@1) ->
                    automata@internal@calendar:days_in_month(2025, Month@1) >= Day@1
                end
            )
        end
    ).

-file("src/automata/cron/validator.gleam", 501).
-spec step_base_to_string(step_base()) -> binary().
step_base_to_string(Base) ->
    case Base of
        step_any ->
            <<"*"/utf8>>;

        {step_exact, Value} ->
            erlang:integer_to_binary(Value);

        {step_range, Start, End} ->
            <<<<(erlang:integer_to_binary(Start))/binary, "-"/utf8>>/binary,
                (erlang:integer_to_binary(End))/binary>>
    end.

-file("src/automata/cron/validator.gleam", 493).
-spec item_to_string(item()) -> binary().
item_to_string(Item) ->
    case Item of
        {exact, Value} ->
            erlang:integer_to_binary(Value);

        {range, Start, End} ->
            <<<<(erlang:integer_to_binary(Start))/binary, "-"/utf8>>/binary,
                (erlang:integer_to_binary(End))/binary>>;

        {step, Base, Step} ->
            <<<<(step_base_to_string(Base))/binary, "/"/utf8>>/binary,
                (erlang:integer_to_binary(Step))/binary>>
    end.

-file("src/automata/cron/validator.gleam", 486).
-spec selector_to_string(selector()) -> binary().
selector_to_string(Selector) ->
    case Selector of
        any ->
            <<"*"/utf8>>;

        {values, Items} ->
            gleam@string:join(
                gleam@list:map(Items, fun item_to_string/1),
                <<","/utf8>>
            )
    end.

-file("src/automata/cron/validator.gleam", 115).
-spec to_string(valid_cron()) -> binary().
to_string(Spec) ->
    gleam@string:join(
        [selector_to_string(erlang:element(2, Spec)),
            selector_to_string(erlang:element(3, Spec)),
            selector_to_string(erlang:element(4, Spec)),
            selector_to_string(erlang:element(5, Spec)),
            selector_to_string(erlang:element(6, Spec))],
        <<" "/utf8>>
    ).

-file("src/automata/cron/validator.gleam", 128).
-spec validate_semantics(valid_cron()) -> {ok, nil} |
    {error, validation_error()}.
validate_semantics(Spec) ->
    case day_of_week_is_any(erlang:element(6, Spec)) of
        false ->
            {ok, nil};

        true ->
            case schedule_possible(
                erlang:element(4, Spec),
                erlang:element(5, Spec)
            ) of
                true ->
                    {ok, nil};

                false ->
                    {error,
                        {impossible_date,
                            selector_to_string(erlang:element(4, Spec)),
                            selector_to_string(erlang:element(5, Spec))}}
            end
    end.

-file("src/automata/cron/validator.gleam", 76).
-spec validate(automata@cron@ast:raw_cron()) -> {ok, valid_cron()} |
    {error, validation_error()}.
validate(Raw) ->
    case parse_selector(minute, erlang:element(2, Raw), 0, 59, no_aliases) of
        {error, Error} ->
            {error, Error};

        {ok, Minute} ->
            case parse_selector(hour, erlang:element(3, Raw), 0, 23, no_aliases) of
                {error, Error@1} ->
                    {error, Error@1};

                {ok, Hour} ->
                    case parse_selector(
                        day_of_month,
                        erlang:element(4, Raw),
                        1,
                        31,
                        no_aliases
                    ) of
                        {error, Error@2} ->
                            {error, Error@2};

                        {ok, Day_of_month} ->
                            case parse_selector(
                                month,
                                erlang:element(5, Raw),
                                1,
                                12,
                                month_aliases
                            ) of
                                {error, Error@3} ->
                                    {error, Error@3};

                                {ok, Month} ->
                                    case parse_selector(
                                        day_of_week,
                                        erlang:element(6, Raw),
                                        0,
                                        7,
                                        day_aliases
                                    ) of
                                        {error, Error@4} ->
                                            {error, Error@4};

                                        {ok, Day_of_week} ->
                                            Spec = {valid_cron,
                                                Minute,
                                                Hour,
                                                Day_of_month,
                                                Month,
                                                Day_of_week},
                                            case validate_semantics(Spec) of
                                                {ok, _} ->
                                                    {ok, Spec};

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