# `Tempo.RRule`
[🔗](https://github.com/elixir-tempo/tempo/blob/v2.1.0/lib/tempo/rrule.ex#L1)

Reads and writes RFC 5545 `RRULE` text: `parse/2` turns a rule into a
Tempo recurrence, the `%Tempo{}` an ISO 8601-2 repeat rule reads to, and
`to_string/1` writes one back.

A rule's `FREQ` and `INTERVAL` are the recurrence's cadence; its `BY*`
parts are the parts of the selection applied in each repeating interval
(`BYDAY=2MO` in a monthly rule is `L1K2IN`), `BYSETPOS` its positions,
`COUNT` and `UNTIL` its bound. What a rule leaves to `DTSTART` (the day of
the month of a monthly rule, the month and day of a yearly one, the
weekday of a weekly one, the time of day of any) is taken from the start,
as RFC 5545 §3.3.10 has it. The same lowering serves `Tempo.Cron` and
`Tempo.JSCalendar`, which build the rule from their own vocabularies.

RFC 7529 is read with it. `RSCALE` names the calendar a rule counts in,
which is then the calendar of the recurrence and of its occurrences;
`BYMONTH` may name a leap month of it (`5L`); and `SKIP` says what is
taken for a day or a month a year lacks, the next there is (`FORWARD`) or
the last before it (`BACKWARD`). A `WKST` other than Monday is the day
the recurrence's weeks begin on where it changes the weeks counted.

# `rule`

```elixir
@type rule() :: %{
  optional(:freq) =&gt; Tempo.unit(),
  optional(:interval) =&gt; pos_integer(),
  optional(:count) =&gt; pos_integer(),
  optional(:until) =&gt; Tempo.t(),
  optional(:bymonth) =&gt; [integer() | {integer(), :leap}],
  optional(:bymonthday) =&gt; [integer()],
  optional(:byyearday) =&gt; [integer()],
  optional(:byweekno) =&gt; [integer()],
  optional(:byday) =&gt; [{integer() | nil, 1..7}],
  optional(:byhour) =&gt; [integer()],
  optional(:byminute) =&gt; [integer()],
  optional(:bysecond) =&gt; [integer()],
  optional(:bysetpos) =&gt; [integer()],
  optional(:wkst) =&gt; 1..7,
  optional(:rscale) =&gt; String.t(),
  optional(:skip) =&gt; :omit | :forward | :backward
}
```

A rule as the vocabularies share it: the keys RFC 5545 names, lower-cased.

# `from_rule`

```elixir
@spec from_rule(rule(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
```

A recurrence from a rule in the shared vocabulary (`t:rule/0`), as
`Tempo.Cron` and `Tempo.JSCalendar` build one.

### Arguments

* `rule` is a `t:rule/0` with a frequency.

* `options` is a keyword list of options.

### Options

* `:from`, `:duration` and `:metadata` are as for `parse/2`.

### Returns

* `{:ok, recurrence}` or `{:error, exception}`, as for `parse/2`.

### Examples

    iex> Tempo.RRule.from_rule(%{freq: :week, count: 3}, from: ~o"2026-06-01")
    {:ok, ~o"R3/2026-06-01/P1W/FL1KN"}

# `from_rule!`

```elixir
@spec from_rule!(rule(), keyword()) :: Tempo.t()
```

`from_rule/2`, returning its answer or raising the error.

### Arguments

* The arguments are those of `from_rule/2`.

### Options

* The options of `from_rule/2`.

### Returns

* What `from_rule/2` returns as `{:ok, answer}`, the answer alone.
  The error it returns as `{:error, exception}` is raised.

### Examples

    iex> Tempo.RRule.from_rule!(%{freq: :week, count: 3}, from: ~o"2026-06-01")
    ~o"R3/2026-06-01/P1W/FL1KN"

# `parse`

```elixir
@spec parse(String.t(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
```

Reads an `RRULE` into a recurrence.

### Arguments

* `text` is the rule, with or without a leading `RRULE:`.

* `options` is a keyword list of options.

### Options

* `:from` is the recurrence's start (`DTSTART`), a `t:Tempo.t/0`. Without
  one the recurrence has an open start, which writes back to text but
  gives no occurrences.

* `:duration` is each occurrence's length, a `t:Tempo.Duration.t/0`
  (iCalendar's `DURATION`, or `DTEND` less `DTSTART`); without one an
  occurrence is one unit of the resolution the rule selects at.

* `:metadata` is a map carried on the recurrence and every occurrence.

### Returns

* `{:ok, recurrence}`, a `t:Tempo.t/0` of the `:every` form.

* `{:error, %ArgumentError{}}` for text that is not a rule: no `FREQ`, an
  unknown frequency or part, a malformed value, both `COUNT` and `UNTIL`,
  an `RSCALE` that names no calendar, a `SKIP` or a leap month with no
  `RSCALE`.

### Examples

    iex> {:ok, rule} = Tempo.RRule.parse("FREQ=MONTHLY;BYDAY=2MO", from: ~o"2025-01-01")
    iex> Tempo.to_iso8601(rule)
    {:ok, "R/2025Y1M1D/P1M/FL1K2IN"}

    iex> {:ok, rule} = Tempo.RRule.parse("FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25;COUNT=3", from: ~o"2026-01-01")
    iex> rule |> Tempo.to_interval!() |> Tempo.IntervalSet.map(&Tempo.year/1)
    [2026, 2027, 2028]

    iex> {:ok, rule} = Tempo.RRule.parse("FREQ=WEEKLY;INTERVAL=2;UNTIL=20221231", from: ~o"2022-12-05")
    iex> Tempo.to_iso8601(rule)
    {:ok, "R/2022Y12M5D/P2W/2022Y12M31D/FL1KN"}

    iex> {:error, %ArgumentError{}} = Tempo.RRule.parse("BYMONTH=6")

# `parse!`

```elixir
@spec parse!(String.t(), keyword()) :: Tempo.t()
```

`parse/2`, returning the recurrence or raising.

### Arguments

* `text` and `options` are as for `parse/2`.

### Options

* The options of `parse/2`.

### Returns

* The recurrence, a `t:Tempo.t/0`, or raises the error `parse/2` returns.

### Examples

    iex> Tempo.RRule.parse!("FREQ=DAILY;COUNT=10", from: ~o"2026-06-01") |> Enum.count()
    10

# `to_string`

```elixir
@spec to_string(Tempo.t()) :: {:ok, String.t()} | {:error, Exception.t()}
```

Writes a recurrence as an `RRULE`.

### Arguments

* `recurrence` is a `t:Tempo.t/0` of the `:every` form whose cadence is
  one unit and whose rule names only what RFC 5545 can: months, weeks,
  days of the year or month, weekdays, hours, minutes, seconds and
  positions.

### Returns

* `{:ok, text}`.

* `{:error, %Tempo.ConversionError{reason: {:no_rrule, what}}}` for a
  cadence of several units, an event or a selection within a window, or
  an expression that is no recurrence. It is also the error for a
  recurrence RFC 5545 would read as another: one of a calendar other than
  the Gregorian that counts in months, years or their days (`{:calendar,
  module}`), and one with no rule that steps by months from the 29th, 30th
  or 31st, or by years from the 29th of February (`:month_end`).

An occurrence's length (the `:duration` of `parse/2`) is no part of an
`RRULE` and is not written: iCalendar carries it beside the rule, as
`DURATION` or `DTEND`.

### Examples

    iex> Tempo.RRule.to_string(~o"R/2025Y1M1D/P1M/FL1K2IN")
    {:ok, "FREQ=MONTHLY;BYDAY=2MO"}

    iex> Tempo.RRule.to_string(~o"R3/2026-01-01/P1Y/FL12M25DN")
    {:ok, "FREQ=YEARLY;COUNT=3;BYMONTH=12;BYMONTHDAY=25"}

    iex> Tempo.RRule.to_string(~o"R/2026-06-01/P2W")
    {:ok, "FREQ=WEEKLY;INTERVAL=2"}

# `to_string!`

```elixir
@spec to_string!(Tempo.t()) :: String.t()
```

`to_string/1`, returning the text or raising.

### Arguments

* `recurrence` is as for `to_string/1`.

### Returns

* The RRULE text, or raises the error `to_string/1` returns.

### Examples

    iex> Tempo.RRule.to_string!(~o"R10/2026-06-01/P1D")
    "FREQ=DAILY;COUNT=10"

---

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