# `Tempo.ICal`
[🔗](https://github.com/elixir-tempo/tempo/blob/v2.1.0/lib/tempo/ical.ex#L2)

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`](https://hex.pm/packages/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)

# `available`

```elixir
@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

* `ics` is iCalendar text or an `%ICal{}`.

* `options` is a keyword list of options.

### Options

* `:within` is a `t:Tempo.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 for `parse/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!`

```elixir
@spec available!(binary() | ICal.t(), keyword()) :: Tempo.t()
```

`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/2` returns 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

# `from_ical`

```elixir
@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

* `calendar` is an `%ICal{}`.

* `options` is a keyword list of options.

### Options

* `:within` is as for `parse/2`.

### Returns

* `{:ok, set}` or `{:error, exception}`, as for `parse/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!`

```elixir
@spec from_ical!(ICal.t(), keyword()) :: Tempo.t()
```

`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/2` returns 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

# `parse`

```elixir
@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

* `ics` is iCalendar text.

* `options` is a keyword list of options.

### Options

* `:within` is a `t:Tempo.t/0`, the window whose events are wanted:
  every occurrence that overlaps it is kept. It is required when an
  event repeats with neither `COUNT` nor `UNTIL`.

### Returns

* `{:ok, set}`, a `t:Tempo.t/0` of the `:all` form, 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!`

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

`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/2` returns 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

# `parse_file`

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

Reads an iCalendar file as `parse/2` reads text.

### Arguments

* `path` is the path of an `.ics` file.

* `options` is a keyword list of options.

### Options

* `:within` is as for `parse/2`.

### Returns

* `{:ok, set}`, or `{:error, exception}`, a `File.Error` when the file
  cannot be read.

### Examples

    iex> {:error, %File.Error{}} = Tempo.ICal.parse_file("no-such-calendar.ics")

# `parse_file!`

```elixir
@spec parse_file!(Path.t(), keyword()) :: Tempo.t()
```

`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/2` returns 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

---

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