One instance of a recurring object (RFC 8984 §4.3.5).
A recurring object is a base plus a rule plus a table of exceptions. Expanding the rule gives you a list of recurrence ids — local date-times — and this module turns one of those back into an object: the base, shifted to that moment, with any override applied.
JSCalendar.Occurrence.at(standup, ~N[2026-06-04 09:00:00])
#=> {:ok, %JSCalendar.Event{start: ~N[2026-06-04 09:00:00], ...}}Three things happen that a plain patch would not do:
The anchor moves. An occurrence inherits everything except its
start— or, for a task with no start, itsdue— which becomes the recurrence id. A patch may then override even that, and the patch wins.The recurrence machinery is stripped. §4.3.1 forbids an object with a
recurrenceIdfrom also carryingrecurrenceRulesorrecurrenceOverrides, so an occurrence is a single object rather than a recurring one that recurs again.Some pointers are ignored rather than rejected. An override may not change a
uid, aprodId, or the rules that generated it; the RFC's word is ignored, so such a patch still applies — minus those pointers — rather than failing.
Exclusion is a separate answer, not an empty one:
JSCalendar.Occurrence.at(standup, ~N[2026-06-05 09:00:00])
#=> :excludedA caller expanding a rule has to distinguish "this occurrence was cancelled" from "this occurrence could not be built", and a tuple that could mean either would make that impossible.
Summary
Functions
Build the occurrence of object at recurrence_id.
Whether the occurrence at recurrence_id is excluded.
The recurrence ids object overrides, in chronological order.
Functions
@spec at( struct(), NaiveDateTime.t() ) :: {:ok, struct()} | :excluded | {:error, term()}
Build the occurrence of object at recurrence_id.
Arguments
objectis a recurringJSCalendar.Event.t/0orJSCalendar.Task.t/0.recurrence_idis aNaiveDateTime.t/0produced by the object's recurrence rules, or a key of itsrecurrence_overridesthat the rules did not produce — an additional occurrence, the equivalent of an iCalendarRDATE.
Returns
{:ok, occurrence}— the same struct type asobject, carryingrecurrence_idandrecurrence_id_time_zone; or:excludedwhen the override setsexcludedtotrue; or{:error, reason}when the override is not a valid patch.
Examples
iex> standup = %JSCalendar.Event{
...> uid: "a", start: ~N[2026-06-01 09:00:00], time_zone: "Australia/Sydney",
...> recurrence_overrides: %{~N[2026-06-03 09:00:00] => %{"title" => "Retro"}}}
iex> {:ok, occurrence} = JSCalendar.Occurrence.at(standup, ~N[2026-06-03 09:00:00])
iex> {occurrence.start, occurrence.title, occurrence.recurrence_id}
{~N[2026-06-03 09:00:00], "Retro", ~N[2026-06-03 09:00:00]}
iex> standup = %JSCalendar.Event{
...> start: ~N[2026-06-01 09:00:00],
...> recurrence_overrides: %{~N[2026-06-03 09:00:00] => %{"excluded" => true}}}
iex> JSCalendar.Occurrence.at(standup, ~N[2026-06-03 09:00:00])
:excluded
@spec excluded?( struct(), NaiveDateTime.t() ) :: boolean()
Whether the occurrence at recurrence_id is excluded.
Arguments
objectis aJSCalendar.Event.t/0orJSCalendar.Task.t/0.recurrence_idis aNaiveDateTime.t/0.
Returns
trueorfalse.
Examples
iex> event = %JSCalendar.Event{
...> recurrence_overrides: %{~N[2026-06-05 09:00:00] => %{"excluded" => true}}}
iex> JSCalendar.Occurrence.excluded?(event, ~N[2026-06-05 09:00:00])
true
iex> JSCalendar.Occurrence.excluded?(%JSCalendar.Event{}, ~N[2026-06-05 09:00:00])
false
@spec overridden(struct()) :: [NaiveDateTime.t()]
The recurrence ids object overrides, in chronological order.
Includes both variations on rule-generated occurrences and additional ones the rules do not produce — telling those apart needs the rules expanded, which is the caller's business, not this module's.
Arguments
objectis aJSCalendar.Event.t/0orJSCalendar.Task.t/0.
Returns
- a list of
NaiveDateTime.t/0.
Examples
iex> event = %JSCalendar.Event{recurrence_overrides: %{
...> ~N[2026-06-05 09:00:00] => %{"excluded" => true},
...> ~N[2026-06-03 09:00:00] => %{"title" => "Retro"}}}
iex> JSCalendar.Occurrence.overridden(event)
[~N[2026-06-03 09:00:00], ~N[2026-06-05 09:00:00]]