Materialise an RFC 5545 RRULE into concrete occurrences by forming the right AST and handing it to Tempo's interpreter.
Design — adapter, not engine
The iCalendar RRULE, ISO 8601-2 recurring intervals
(R5/DTSTART/P1D), and hand-built Tempo.RRule.Rule structs
all express the same thing: a recurrence anchored at a point
in time, repeated at a cadence, optionally filtered by BY-rule
selections. Tempo's %Tempo.Interval{} struct already has
every field needed to represent this shape: recurrence,
duration, from, to (for UNTIL), and repeat_rule
(carrying BY-rule filters as selection tokens).
Rather than build a parallel engine, this module is a thin adapter:
Input normalisation — any of
%Tempo.RRule.Rule{},%ICal.Recurrence{}, or (in future) parsed AST get canonicalised into%Tempo.RRule.Rule{}.AST projection —
to_ast/3builds the canonical%Tempo.Interval{}that Tempo's interpreter understands.Delegation —
Tempo.to_interval/2materialises the AST into the occurrence set.
Extending RRULE support (BY* rules, BYSETPOS, RDATE, EXDATE)
happens by extending the interpreter — selection-resolution
in the enumeration module and Tempo.to_interval/2 — never by
adding expansion logic here.
See plans/rrule-full-expansion.md for the roadmap.
Summary
Functions
Expand a rule into a list of concrete %Tempo.Interval{}
occurrences.
Convert an %ICal.Recurrence{} into a %Tempo.RRule.Rule{}.
Convert a %Tempo.RRule.Rule{} (plus an anchor) to the
canonical %Tempo.Interval{} AST.
Functions
@spec expand(Tempo.RRule.Rule.t() | term(), Tempo.t(), keyword()) :: {:ok, [Tempo.Interval.t()]} | {:error, term()}
Expand a rule into a list of concrete %Tempo.Interval{}
occurrences.
Arguments
ruleis any of:%Tempo.RRule.Rule{}— the canonical form.%ICal.Recurrence{}— mapped viafrom_ical_recurrence/1(only when theicallibrary is loadable).
dtstartis aTempo.t/0anchor for the first occurrence.
Options
:durationis a%Tempo.Duration{}giving each occurrence's span. Defaults to the natural span ofdtstart(a day for day-resolution, an hour for hour-resolution, etc.).:boundis any Tempo value whose upper endpoint limits the expansion. Required when the rule has neitherCOUNTnorUNTIL. The expansion stops when an occurrence would start at or after the bound's upper edge.:metadatais a map of per-occurrence metadata attached to every materialised interval.
Returns
{:ok, [%Tempo.Interval{}]}on success.{:error, reason}when the rule is unbounded and no bound is supplied, or the input cannot be converted to the canonical AST.
Examples
iex> rule = %Tempo.RRule.Rule{freq: :day, interval: 1, count: 3}
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"2022-06-01")
iex> length(occurrences)
3
iex> Enum.map(occurrences, & &1.from.time[:day])
[1, 2, 3]
@spec from_ical_recurrence(ICal.Recurrence.t()) :: {:ok, Tempo.RRule.Rule.t()} | {:error, term()}
Convert an %ICal.Recurrence{} into a %Tempo.RRule.Rule{}.
Used by Tempo.ICal and the polymorphic expand/3 above.
Requires the ical dependency at compile time; the function
is only defined when ical is loadable.
Arguments
ical_ruleis an%ICal.Recurrence{}.
Returns
{:ok, %Tempo.RRule.Rule{}}on success.{:error, reason}when a field cannot be mapped.
@spec to_ast(Tempo.RRule.Rule.t(), Tempo.t(), keyword()) :: {:ok, Tempo.Interval.t()}
Convert a %Tempo.RRule.Rule{} (plus an anchor) to the
canonical %Tempo.Interval{} AST.
The AST has the same shape as Tempo.RRule.parse/2's output
and as the ISO 8601-2 R<n>/DTSTART/P… interval grammar, so
Tempo.to_interval/2 handles all three inputs uniformly.
Arguments
ruleis a%Tempo.RRule.Rule{}.dtstartis a%Tempo{}anchor for the first occurrence.
Options
:durationis a%Tempo.Duration{}override for each occurrence's span. When supplied, it is attached to the AST viametadata.occurrence_durationso the interpreter can emit occurrences whose span differs from the cadence.:base_tois a%Tempo{}representing occurrence #0's upper endpoint. The interpreter shifts it by one cadence per iteration so each occurrence preserves the original event's span. Used byTempo.ICalwhereDTEND − DTSTARTdefines the event span independently of the RRULE cadence.:metadatais a map merged into the interval's metadata.
Returns
{:ok, %Tempo.Interval{}}.
Examples
iex> rule = %Tempo.RRule.Rule{freq: :week, interval: 1, count: 5}
iex> {:ok, ast} = Tempo.RRule.Expander.to_ast(rule, ~o"2022-06-01")
iex> {ast.recurrence, ast.duration.time}
{5, [week: 1]}