YmerNode.Schedules.Cron (Ymer Node v0.5.0)

Copy Markdown View Source

A schedule's cron expression: which ones the node accepts, and when one comes due next in the node's time zone. The crontab package parses and walks them; this module decides only what that package leaves open.

What is accepted

Exactly five fields — minute, hour, day of month, month, day of week — with numbers, ranges, steps, lists, day and month names, and L, W and #; or one of the nicknames @hourly, @daily, @midnight, @weekly, @monthly, @yearly and @annually. Five fields fire at most once a minute, which is as often as a schedule may.

Four shapes the package would parse are refused — the first three before it sees them, the fourth on the values it reads. @reboot names an event, not a cadence. A sixth, year field would be a second way for a schedule to end, and its lifetime is the one bound. Fewer than five fields is a slip the package would silently pad with *: * * * typed for 0 7 * * * would fire every minute for as long as the schedule lives. And a day of week 0 carrying L or # never comes due in the package's walk — 0L walks without end and 0#2 answers that no date exists — so Sunday is written 7 or SUN with them.

A clock that jumps

Every expression is walked with on_ambiguity: [:prior], set after the parse, because the package's nicknames come back without it. When the clock falls back and an hour occurs twice, a time in that hour comes due once, on its first pass, and a cadence of any density keeps the first pass of the hour and skips the second. When the clock springs forward, a time inside the missing hour does not come due that day. Both are small-hours misses on two days a year, met with the simplest rule that never fires anything twice.

The package alone keeps that rule only for a walk that starts before the repeated hour. Its search includes the moment it starts from, so a walk that starts on a matching minute of the second pass answers that very minute, and * * * * * walked on a minute at a time would come due all through the second pass. So next_firing/2 checks every answer the package gives: one that is the second pass of a repeated local time is walked past, a minute at a time, until the answer is a first pass or a time that does not repeat. Whoever asks, from whichever minute — YmerNode.Schedules.Scheduler after a restart or a changed expression, or YmerNode.Schedules.list/0 — gets the same answer.

A walk that gives out

A walk runs in a task of its own under a deadline, and one that raises or runs past it answers :none and logs a warning naming the expression. The package has walks that never return, and one schedule's expression must not hold up the minute every other schedule fires in, or an answer that lists them.

Summary

Functions

The first moment at or after from that the expression comes due, in from's zone — :none when it never does, or when the walk gives out. A from with seconds past its minute has left that minute, so the answer is a later one, and the answer is never the second pass of a repeated local time.

Parses a cron expression the node accepts, set to fire only the first pass of a repeated hour.

Functions

next_firing(cron, from)

The first moment at or after from that the expression comes due, in from's zone — :none when it never does, or when the walk gives out. A from with seconds past its minute has left that minute, so the answer is a later one, and the answer is never the second pass of a repeated local time.

Examples

iex> {:ok, cron} = YmerNode.Schedules.Cron.parse("0 7 * * *")
iex> YmerNode.Schedules.Cron.next_firing(cron, ~U[2026-09-26 07:00:00Z])
~U[2026-09-26 07:00:00Z]
iex> YmerNode.Schedules.Cron.next_firing(cron, ~U[2026-09-26 07:00:30Z])
~U[2026-09-27 07:00:00Z]

parse(expression)

Parses a cron expression the node accepts, set to fire only the first pass of a repeated hour.

Examples

iex> {:ok, cron} = YmerNode.Schedules.Cron.parse("0 7 * * MON-FRI")
iex> cron.on_ambiguity
[:prior]

iex> YmerNode.Schedules.Cron.parse("* * *")
{:error, {:invalid_cron_expression, "* * * has 3 fields; a cron expression has exactly five — minute, hour, day of month, month, day of week"}}

iex> YmerNode.Schedules.Cron.parse("@reboot")
{:error, {:invalid_cron_expression, "@reboot names an event, not a cadence; give five fields or one of @hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually"}}

iex> YmerNode.Schedules.Cron.parse("0 7 * * 0L")
{:error, {:invalid_cron_expression, "0 7 * * 0L: write Sunday as 7 or SUN with L or # — a day of week 0 with them never comes due"}}

iex> YmerNode.Schedules.Cron.parse("0 7 * * +0#2")
{:error, {:invalid_cron_expression, "0 7 * * +0#2: write Sunday as 7 or SUN with L or # — a day of week 0 with them never comes due"}}