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.
Summary
Functions
A recurrence from a rule in the shared vocabulary (rule/0), as
Tempo.Cron and Tempo.JSCalendar build one.
from_rule/2, returning its answer or raising the error.
Reads an RRULE into a recurrence.
parse/2, returning the recurrence or raising.
Writes a recurrence as an RRULE.
to_string/1, returning the text or raising.
Types
@type rule() :: %{ optional(:freq) => Tempo.unit(), optional(:interval) => pos_integer(), optional(:count) => pos_integer(), optional(:until) => Tempo.t(), optional(:bymonth) => [integer() | {integer(), :leap}], optional(:bymonthday) => [integer()], optional(:byyearday) => [integer()], optional(:byweekno) => [integer()], optional(:byday) => [{integer() | nil, 1..7}], optional(:byhour) => [integer()], optional(:byminute) => [integer()], optional(:bysecond) => [integer()], optional(:bysetpos) => [integer()], optional(:wkst) => 1..7, optional(:rscale) => String.t(), optional(:skip) => :omit | :forward | :backward }
A rule as the vocabularies share it: the keys RFC 5545 names, lower-cased.
Functions
@spec from_rule(rule(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
A recurrence from a rule in the shared vocabulary (rule/0), as
Tempo.Cron and Tempo.JSCalendar build one.
Arguments
ruleis arule/0with a frequency.optionsis a keyword list of options.
Options
:from,:durationand:metadataare as forparse/2.
Returns
{:ok, recurrence}or{:error, exception}, as forparse/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/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/2returns 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"
@spec parse(String.t(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
Reads an RRULE into a recurrence.
Arguments
textis the rule, with or without a leadingRRULE:.optionsis a keyword list of options.
Options
:fromis the recurrence's start (DTSTART), aTempo.t/0. Without one the recurrence has an open start, which writes back to text but gives no occurrences.:durationis each occurrence's length, aTempo.Duration.t/0(iCalendar'sDURATION, orDTENDlessDTSTART); without one an occurrence is one unit of the resolution the rule selects at.:metadatais a map carried on the recurrence and every occurrence.
Returns
{:ok, recurrence}, aTempo.t/0of the:everyform.{:error, %ArgumentError{}}for text that is not a rule: noFREQ, an unknown frequency or part, a malformed value, bothCOUNTandUNTIL, anRSCALEthat names no calendar, aSKIPor a leap month with noRSCALE.
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/2, returning the recurrence or raising.
Arguments
textandoptionsare as forparse/2.
Options
- The options of
parse/2.
Returns
Examples
iex> Tempo.RRule.parse!("FREQ=DAILY;COUNT=10", from: ~o"2026-06-01") |> Enum.count()
10
@spec to_string(Tempo.t()) :: {:ok, String.t()} | {:error, Exception.t()}
Writes a recurrence as an RRULE.
Arguments
recurrenceis aTempo.t/0of the:everyform 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/1, returning the text or raising.
Arguments
recurrenceis as forto_string/1.
Returns
- The RRULE text, or raises the error
to_string/1returns.
Examples
iex> Tempo.RRule.to_string!(~o"R10/2026-06-01/P1D")
"FREQ=DAILY;COUNT=10"