Reads iCalendar (RFC 5545) data into Tempo: each VEVENT an interval
carrying the event's summary, location and the rest as metadata, each
recurring event its occurrences, and RFC 7953 VAVAILABILITY components
the time they offer.
The text is parsed by the optional ical
dependency, and this module is compiled only when it is present. An
event's RRULE is lowered to a Tempo recurrence by Tempo.RRule, so its
occurrences are those any Tempo recurrence gives; RDATE adds
occurrences of the event's own length and EXDATE removes the one that
starts at it. An all-day event (DTSTART a DATE) is a span of days,
written to its last day; BYHOUR, BYMINUTE and BYSECOND are ignored
on one, as RFC 5545 §3.3.10 has it.
Events say what is taken and availability what is offered, and the two compose:
{:ok, free} = Tempo.ICal.available(ics, within: week)
{:ok, busy} = Tempo.ICal.parse(ics, within: week)
{:ok, bookable} = Tempo.difference(free, busy)
Summary
Functions
The time an iCalendar's VAVAILABILITY components offer (RFC 7953).
available/2, returning its answer or raising the error.
The events of a calendar the ical library has already parsed, as
parse/2 gives them.
from_ical/2, returning its answer or raising the error.
Reads iCalendar text into a set of intervals, one for each event or occurrence of an event, in order of start.
parse/2, returning its answer or raising the error.
Reads an iCalendar file as parse/2 reads text.
parse_file/2, returning its answer or raising the error.
Functions
@spec available(binary() | ICal.t(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
The time an iCalendar's VAVAILABILITY components offer (RFC 7953).
Each component covers a period, DTSTART to DTEND, and within it each
AVAILABLE subcomponent offers time, repeating under its own RRULE.
Where components overlap the one of higher PRIORITY (1 the highest,
0 or none the lowest) decides its whole period.
Arguments
icsis iCalendar text or an%ICal{}.optionsis a keyword list of options.
Options
:withinis aTempo.t/0, the window the time offered is taken in. It is required.
Returns
{:ok, set}of the time offered within the window, merged.{:error, exception}with no window, or as forparse/2.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VAVAILABILITY
...> UID:office-hours
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260601T000000Z
...> DTEND:20260608T000000Z
...> BEGIN:AVAILABLE
...> UID:weekday-mornings
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260601T090000Z
...> DTEND:20260601T120000Z
...> RRULE:FREQ=DAILY;COUNT=5
...> END:AVAILABLE
...> END:VAVAILABILITY
...> END:VCALENDAR
...> """
iex> {:ok, free} = Tempo.ICal.available(ics, within: ~o"2026-06-01T00Z/2026-06-08T00Z")
iex> Tempo.IntervalSet.count(free)
5
available/2, returning its answer or raising the error.
Arguments
- The arguments are those of
available/2.
Options
- The options of
available/2.
Returns
- What
available/2returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VAVAILABILITY
...> UID:office-hours
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260601T000000Z
...> DTEND:20260608T000000Z
...> BEGIN:AVAILABLE
...> UID:weekday-mornings
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260601T090000Z
...> DTEND:20260601T120000Z
...> RRULE:FREQ=DAILY;COUNT=5
...> END:AVAILABLE
...> END:VAVAILABILITY
...> END:VCALENDAR
...> """
iex> ics |> Tempo.ICal.available!(within: ~o"2026-06-01T00Z/2026-06-08T00Z") |> Tempo.IntervalSet.count()
5
@spec from_ical(ICal.t(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
The events of a calendar the ical library has already parsed, as
parse/2 gives them.
Arguments
calendaris an%ICal{}.optionsis a keyword list of options.
Options
:withinis as forparse/2.
Returns
{:ok, set}or{:error, exception}, as forparse/2.
Examples
iex> calendar =
...> ICal.from_ics("""
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VEVENT
...> UID:offsite
...> DTSTART;VALUE=DATE:20260615
...> END:VEVENT
...> END:VCALENDAR
...> """)
iex> {:ok, events} = Tempo.ICal.from_ical(calendar)
iex> Tempo.IntervalSet.map(events, &Tempo.to_iso8601!/1)
["2026Y6M15D/2026Y6M15D"]
from_ical/2, returning its answer or raising the error.
Arguments
- The arguments are those of
from_ical/2.
Options
- The options of
from_ical/2.
Returns
- What
from_ical/2returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VEVENT
...> UID:offsite
...> DTSTART;VALUE=DATE:20260615
...> END:VEVENT
...> END:VCALENDAR
...> """
iex> ics |> ICal.from_ics() |> Tempo.ICal.from_ical!() |> Tempo.IntervalSet.count()
1
@spec parse(binary(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
Reads iCalendar text into a set of intervals, one for each event or occurrence of an event, in order of start.
Arguments
icsis iCalendar text.optionsis a keyword list of options.
Options
:withinis aTempo.t/0, the window whose events are wanted: every occurrence that overlaps it is kept. It is required when an event repeats with neitherCOUNTnorUNTIL.
Returns
{:ok, set}, aTempo.t/0of the:allform, its metadata the calendar's (:prodid,:version,:scale,:method,:name) and each member's its event's (:uid,:summary,:description,:location,:status, and so on).{:error, exception}when a recurring event has no end and no window is given, or a value cannot be read.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> PRODID:-//Test//Test//EN
...> BEGIN:VEVENT
...> UID:test-1
...> DTSTAMP:20220101T000000Z
...> DTSTART:20220615T100000Z
...> DTEND:20220615T110000Z
...> SUMMARY:Test meeting
...> LOCATION:Paris
...> END:VEVENT
...> END:VCALENDAR
...> """
iex> {:ok, events} = Tempo.ICal.parse(ics)
iex> [meeting] = Tempo.IntervalSet.members(events)
iex> {Tempo.metadata(meeting).summary, Tempo.duration(meeting)}
{"Test meeting", ~o"PT3600S"}
parse/2, returning its answer or raising the error.
Arguments
- The arguments are those of
parse/2.
Options
- The options of
parse/2.
Returns
- What
parse/2returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VEVENT
...> UID:offsite
...> DTSTART;VALUE=DATE:20260615
...> END:VEVENT
...> END:VCALENDAR
...> """
iex> ics |> Tempo.ICal.parse!() |> Tempo.IntervalSet.count()
1
@spec parse_file(Path.t(), keyword()) :: {:ok, Tempo.t()} | {:error, Exception.t()}
Reads an iCalendar file as parse/2 reads text.
Arguments
pathis the path of an.icsfile.optionsis a keyword list of options.
Options
:withinis as forparse/2.
Returns
{:ok, set}, or{:error, exception}, aFile.Errorwhen the file cannot be read.
Examples
iex> {:error, %File.Error{}} = Tempo.ICal.parse_file("no-such-calendar.ics")
parse_file/2, returning its answer or raising the error.
Arguments
- The arguments are those of
parse_file/2.
Options
- The options of
parse_file/2.
Returns
- What
parse_file/2returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> Tempo.ICal.parse_file!("no-such-calendar.ics")
** (File.Error) could not read file "no-such-calendar.ics": no such file or directory