# `Tempo.RRule.Expander`
[🔗](https://github.com/kipcole9/tempo/blob/v0.5.0/lib/tempo/rrule/expander.ex#L1)

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 projection** — `to_ast/3` builds the canonical
   `%Tempo.Interval{}` that Tempo's interpreter understands.

3. **Delegation** — `Tempo.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.

# `expand`

```elixir
@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 `t: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`

```elixir
@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`

```elixir
@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]}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
