Tempo.RRule.Expander (Tempo v0.5.0)

Copy Markdown View Source

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:

  1. Input normalisation — any of %Tempo.RRule.Rule{}, %ICal.Recurrence{}, or (in future) parsed AST get canonicalised into %Tempo.RRule.Rule{}.

  2. AST projectionto_ast/3 builds the canonical %Tempo.Interval{} that Tempo's interpreter understands.

  3. DelegationTempo.to_interval/2 materialises 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

expand(rule, dtstart, options \\ [])

@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

  • rule is any of:

    • %Tempo.RRule.Rule{} — the canonical form.

    • %ICal.Recurrence{} — mapped via from_ical_recurrence/1 (only when the ical library is loadable).

  • dtstart is a Tempo.t/0 anchor for the first occurrence.

Options

  • :duration is a %Tempo.Duration{} giving each occurrence's span. Defaults to the natural span of dtstart (a day for day-resolution, an hour for hour-resolution, etc.).

  • :bound is any Tempo value whose upper endpoint limits the expansion. Required when the rule has neither COUNT nor UNTIL. The expansion stops when an occurrence would start at or after the bound's upper edge.

  • :metadata is 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]

from_ical_recurrence(r)

@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_rule is an %ICal.Recurrence{}.

Returns

  • {:ok, %Tempo.RRule.Rule{}} on success.

  • {:error, reason} when a field cannot be mapped.

to_ast(rule, dtstart, options \\ [])

@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

  • rule is a %Tempo.RRule.Rule{}.

  • dtstart is a %Tempo{} anchor for the first occurrence.

Options

  • :duration is a %Tempo.Duration{} override for each occurrence's span. When supplied, it is attached to the AST via metadata.occurrence_duration so the interpreter can emit occurrences whose span differs from the cadence.

  • :base_to is 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 by Tempo.ICal where DTEND − DTSTART defines the event span independently of the RRULE cadence.

  • :metadata is 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]}