Ephemeris

Recurrence rules that read and write both RFC 5545 RRULE and plain English.

An ephemeris is a table of recurring positions over time. This one holds recurrence rules: parse them from either syntax into one structure, compute occurrences from it, and render it back out in either syntax.

{:ok, rule} = Ephemeris.parse("FREQ=MONTHLY;BYDAY=-1SU")
{:ok, rule} = Ephemeris.parse("every last sunday of the month")
Ephemeris.to_rrule(rule) #=> "FREQ=MONTHLY;BYDAY=-1SU"
Ephemeris.to_sentence(rule) #=> "every last Sunday of the month"
Ephemeris.next(rule, ~U[2026-01-01 09:00:00Z])
#=> {:ok, ~U[2026-01-25 09:00:00Z]}

Why both syntaxes

RRULE is the standard: portable, precise, and what a calendar application speaks. It is also unreadable in a configuration file. Plain English is the opposite on every count. Holding one structure and rendering either means a rule can be written the readable way, stored the portable way, and shown back either way โ€” without two implementations that disagree.

Status

The RRULE reader and writer, the English reader and writer, and the occurrence engine are complete: 44 tests and 13 doctests, no runtime dependencies.

The occurrence engine supports FREQ, INTERVAL, COUNT, UNTIL, WKST, BYSECOND, BYMINUTE, BYHOUR, BYDAY including ordinals such as -1SU, BYMONTHDAY, BYYEARDAY, BYWEEKNO, BYMONTH and BYSETPOS.

A date that does not exist in a period is skipped, not clamped, as RFC 5545 ยง3.3.10 requires: FREQ=MONTHLY;BYMONTHDAY=31 yields 31 January and 31 March, never 28 February.

Scope

A rule says when something recurs and nothing else โ€” no process, no callback, no timer, no implicit current time. Every calculation takes an explicit reference DateTime. Scheduling belongs to whatever uses this.

Licence

MIT. See LICENSE.