Tempo.RRule (Tempo v2.1.1)

Copy Markdown View Source

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

Types

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

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

rule()

@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

from_rule(rule, options)

@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

  • rule is a 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!(rule, options)

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

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

Arguments

Options

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(text, options \\ [])

@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 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 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 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!(text, options \\ [])

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

parse/2, returning the recurrence or raising.

Arguments

  • text and options are as for parse/2.

Options

Returns

Examples

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

to_string(r)

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

Writes a recurrence as an RRULE.

Arguments

  • recurrence is a 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!(recurrence)

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

to_string/1, returning the text or raising.

Arguments

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"