src/automata@schedule.erl

-module(automata@schedule).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/schedule.gleam").
-export([from_cron/1, from_rrule/2, from_every/2, from_once/1, step/1, matches/2, iterator_after/2, next_after/2]).
-export_type([schedule_error/0, schedule/0, every_plan/0, occurrence_iterator/0, iter_step/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 schedule_error() :: {every_interval_must_be_positive, integer()} |
    {invalid_r_rule_anchor, automata@schedule@ast:date_time()}.

-opaque schedule() :: {cron_schedule, automata@cron@normalize:cron_plan()} |
    {r_rule_schedule, automata@rrule@normalize:r_rule_plan()} |
    {every_schedule, every_plan()} |
    {once_schedule, automata@schedule@ast:date_time()}.

-type every_plan() :: {every_plan, automata@schedule@ast:date_time(), integer()}.

-opaque occurrence_iterator() :: {cron_occurrences,
        automata@cron@iterator:cron_iterator()} |
    {r_rule_occurrences, automata@rrule@iterator:r_rule_iterator()} |
    {every_occurrences, every_plan(), automata@schedule@ast:date_time()} |
    {once_occurrences, automata@schedule@ast:date_time(), boolean()}.

-type iter_step() :: {yield,
        automata@schedule@ast:valid_date_time(),
        occurrence_iterator()} |
    done.

-file("src/automata/schedule.gleam", 76).
?DOC(
    " Build a `Schedule` from a validated cron expression.\n"
    "\n"
    " Cron schedules are infinite; iterators never return `Done` on\n"
    " well-formed input. The return type is `Result` so that this\n"
    " constructor shares one shape with `from_rrule`, `from_every`, and\n"
    " `from_once`, letting generic helpers treat the four uniformly. The\n"
    " current implementation cannot fail (a `ValidCron` cannot produce a\n"
    " normalisation error), so callers can `let assert Ok(_) = ...` with\n"
    " confidence.\n"
).
-spec from_cron(automata@cron@validator:valid_cron()) -> {ok, schedule()} |
    {error, schedule_error()}.
from_cron(Spec) ->
    {ok, {cron_schedule, automata@cron@normalize:normalize(Spec)}}.

-file("src/automata/schedule.gleam", 87).
?DOC(
    " Build a `Schedule` from a validated RRULE plus an anchor.\n"
    "\n"
    " The anchor seeds defaults for unspecified `BYHOUR` / `BYMINUTE` /\n"
    " `BYDAY` / `BYMONTH` / `BYMONTHDAY` and contributes the second\n"
    " component for every yielded occurrence.\n"
).
-spec from_rrule(
    automata@rrule@validator:valid_r_rule(),
    automata@schedule@ast:valid_date_time()
) -> {ok, schedule()} | {error, schedule_error()}.
from_rrule(Spec, Anchor) ->
    case automata@rrule@normalize:normalize(
        Spec,
        automata@schedule@ast:valid_datetime_value(Anchor)
    ) of
        {ok, Plan} ->
            {ok, {r_rule_schedule, Plan}};

        {error, {invalid_anchor, At}} ->
            {error, {invalid_r_rule_anchor, At}}
    end.

-file("src/automata/schedule.gleam", 107).
?DOC(
    " Build an interval `Schedule` that fires `interval_seconds` apart\n"
    " starting at `anchor` (inclusive).\n"
    "\n"
    " Returns `EveryIntervalMustBePositive` when the interval is zero or\n"
    " negative.\n"
).
-spec from_every(integer(), automata@schedule@ast:valid_date_time()) -> {ok,
        schedule()} |
    {error, schedule_error()}.
from_every(Interval_seconds, Anchor) ->
    case Interval_seconds =< 0 of
        true ->
            {error, {every_interval_must_be_positive, Interval_seconds}};

        false ->
            {ok,
                {every_schedule,
                    {every_plan,
                        automata@schedule@ast:valid_datetime_value(Anchor),
                        Interval_seconds}}}
    end.

-file("src/automata/schedule.gleam", 130).
?DOC(
    " Build a one-shot `Schedule` that fires exactly once at `at`.\n"
    "\n"
    " The return type is `Result` so that this constructor shares one\n"
    " shape with `from_rrule`, `from_every`, and `from_cron`. The current\n"
    " implementation cannot fail (the `at` argument is already a\n"
    " `ValidDateTime`), so callers can `let assert Ok(_) = ...` with\n"
    " confidence.\n"
).
-spec from_once(automata@schedule@ast:valid_date_time()) -> {ok, schedule()} |
    {error, schedule_error()}.
from_once(At) ->
    {ok, {once_schedule, automata@schedule@ast:valid_datetime_value(At)}}.

-file("src/automata/schedule.gleam", 165).
?DOC(" Advance an iterator by one occurrence.\n").
-spec step(occurrence_iterator()) -> iter_step().
step(Iterator) ->
    case Iterator of
        {cron_occurrences, Cursor} ->
            case automata@cron@iterator:step(Cursor) of
                {yield, At, Next} ->
                    {yield, At, {cron_occurrences, Next}};

                done ->
                    done
            end;

        {r_rule_occurrences, Cursor@1} ->
            case automata@rrule@iterator:step(Cursor@1) of
                {yield, At@1, Next@1} ->
                    {yield, At@1, {r_rule_occurrences, Next@1}};

                done ->
                    done
            end;

        {every_occurrences, Plan, Cursor@2} ->
            {yield,
                automata@schedule@ast:unsafe_assume_valid(Cursor@2),
                {every_occurrences,
                    Plan,
                    automata@internal@calendar:add_seconds(
                        Cursor@2,
                        erlang:element(3, Plan)
                    )}};

        {once_occurrences, At@2, true} ->
            {yield,
                automata@schedule@ast:unsafe_assume_valid(At@2),
                {once_occurrences, At@2, false}};

        {once_occurrences, _, false} ->
            done
    end.

-file("src/automata/schedule.gleam", 260).
-spec once_eligible(
    automata@schedule@ast:date_time(),
    automata@schedule@ast:boundary()
) -> boolean().
once_eligible(At, Boundary) ->
    case Boundary of
        {inclusive, Valid} ->
            automata@internal@calendar:less_or_equal(
                automata@schedule@ast:valid_datetime_value(Valid),
                At
            );

        {exclusive, Valid@1} ->
            automata@internal@calendar:less_than(
                automata@schedule@ast:valid_datetime_value(Valid@1),
                At
            )
    end.

-file("src/automata/schedule.gleam", 269).
-spec modulo(integer(), integer()) -> integer().
modulo(Dividend, Divisor) ->
    _pipe = gleam@int:modulo(Dividend, Divisor),
    gleam@result:unwrap(_pipe, 0).

-file("src/automata/schedule.gleam", 215).
-spec every_matches(every_plan(), automata@schedule@ast:date_time()) -> boolean().
every_matches(Plan, At) ->
    case automata@internal@calendar:less_than(At, erlang:element(2, Plan)) of
        true ->
            false;

        false ->
            Delta = automata@internal@calendar:seconds_between(
                erlang:element(2, Plan),
                At
            ),
            modulo(Delta, erlang:element(3, Plan)) =:= 0
    end.

-file("src/automata/schedule.gleam", 137).
?DOC(
    " Return `True` when `at` is an occurrence of `schedule`.\n"
    "\n"
    " Pure: same inputs always produce the same output.\n"
).
-spec matches(schedule(), automata@schedule@ast:valid_date_time()) -> boolean().
matches(Schedule, At) ->
    Raw = automata@schedule@ast:valid_datetime_value(At),
    case Schedule of
        {cron_schedule, Plan} ->
            automata@cron@evaluator:matches(Plan, Raw);

        {r_rule_schedule, Plan@1} ->
            automata@rrule@evaluator:matches(Plan@1, Raw);

        {every_schedule, Plan@2} ->
            every_matches(Plan@2, Raw);

        {once_schedule, When} ->
            automata@internal@calendar:equals(When, Raw)
    end.

-file("src/automata/schedule.gleam", 244).
-spec every_align_cursor(
    every_plan(),
    automata@schedule@ast:date_time(),
    boolean()
) -> automata@schedule@ast:date_time().
every_align_cursor(Plan, Target, Exclusive) ->
    Delta = automata@internal@calendar:seconds_between(
        erlang:element(2, Plan),
        Target
    ),
    Interval = erlang:element(3, Plan),
    Remainder = modulo(Delta, Interval),
    Advance = case {Exclusive, Remainder} of
        {true, _} ->
            (Delta - Remainder) + Interval;

        {false, 0} ->
            Delta;

        {false, _} ->
            (Delta - Remainder) + Interval
    end,
    automata@internal@calendar:add_seconds(erlang:element(2, Plan), Advance).

-file("src/automata/schedule.gleam", 225).
-spec every_start_cursor(every_plan(), automata@schedule@ast:boundary()) -> automata@schedule@ast:date_time().
every_start_cursor(Plan, Boundary) ->
    case Boundary of
        {inclusive, Valid} ->
            Target = automata@schedule@ast:valid_datetime_value(Valid),
            case automata@internal@calendar:less_or_equal(
                Target,
                erlang:element(2, Plan)
            ) of
                true ->
                    erlang:element(2, Plan);

                false ->
                    every_align_cursor(Plan, Target, false)
            end;

        {exclusive, Valid@1} ->
            Target@1 = automata@schedule@ast:valid_datetime_value(Valid@1),
            case automata@internal@calendar:less_than(
                Target@1,
                erlang:element(2, Plan)
            ) of
                true ->
                    erlang:element(2, Plan);

                false ->
                    every_align_cursor(Plan, Target@1, true)
            end
    end.

-file("src/automata/schedule.gleam", 148).
?DOC(" Build an iterator over occurrences strictly satisfying `boundary`.\n").
-spec iterator_after(schedule(), automata@schedule@ast:boundary()) -> occurrence_iterator().
iterator_after(Schedule, Boundary) ->
    case Schedule of
        {cron_schedule, Plan} ->
            {cron_occurrences, automata@cron@iterator:'after'(Plan, Boundary)};

        {r_rule_schedule, Plan@1} ->
            {r_rule_occurrences,
                automata@rrule@iterator:'after'(Plan@1, Boundary)};

        {every_schedule, Plan@2} ->
            {every_occurrences, Plan@2, every_start_cursor(Plan@2, Boundary)};

        {once_schedule, When} ->
            {once_occurrences, When, once_eligible(When, Boundary)}
    end.

-file("src/automata/schedule.gleam", 200).
?DOC(
    " Return the next occurrence strictly after `after`, if any.\n"
    "\n"
    " Equivalent to stepping `iterator_after(schedule, Exclusive(after))`\n"
    " once.\n"
).
-spec next_after(schedule(), automata@schedule@ast:valid_date_time()) -> gleam@option:option(automata@schedule@ast:valid_date_time()).
next_after(Schedule, After) ->
    case step(iterator_after(Schedule, {exclusive, After})) of
        {yield, At, _} ->
            {some, At};

        done ->
            none
    end.