# `Agenda.Availability`
[🔗](https://github.com/elixir-tempo/agenda/blob/v0.3.0/lib/agenda/availability.ex#L1)

When a resource is open, and what of that is still free.

Two ideas carry the whole module:

* **Open hours are a Tempo value**, so a resource is open for a single
  interval, a recurring pattern, or any set algebra over them — and
  the calendar, timezone, and DST correctness comes from Tempo rather
  than from anything written here.

* **Free time is derived, never stored.** `free/2` is
  `open − busy`, computed on demand. There is no free/busy record to
  go stale, and no release step that can be forgotten: a resource
  stops being busy the moment nothing claims it.

### Stating open hours

ISO 8601 is the preferred spelling — it is what Tempo stores and what
`inspect/1` returns — but `open/2` accepts an RFC 5545 `RRULE` too,
since that is what most calendar systems emit:

    iex> boardroom = Agenda.Resource.new("Boardroom")
    iex> {:ok, boardroom} = Agenda.Availability.open(boardroom, "R5/2026-06-15/P1D")
    iex> {:ok, free} = Agenda.Availability.free(boardroom, within: "2026-06-15/2026-06-16")
    iex> Tempo.IntervalSet.count(free)
    2

> *"The boardroom is open for five days from the 15th; across the
> 15th and 16th that leaves two free days."*

### In iCalendar

Open hours are RFC 7953's `AVAILABLE` components inside a
`VAVAILABILITY`: periods of free time, each with a `DTSTART`, a
`DTEND` or `DURATION`, and an `RRULE` when it repeats.
`from_ical/1` reads them. An export writes them the same way, one
`AVAILABLE` for each span or recurrence of a resource's open hours,
and leaves the time outside them to the `VAVAILABILITY`'s default
`BUSYTYPE`, `BUSY-UNAVAILABLE`.

What is taken is not written here: the ledger's claims are
`FREEBUSY` time, `BUSY` or `BUSY-TENTATIVE`, as
`Agenda.Ledger.free_busy/2` gives them.

# `imported`

```elixir
@type imported() :: {:vavailability, term()}
```

Availability imported from an RFC 7953 `VAVAILABILITY`, held
unmaterialised until a query window is known — exactly as a
recurrence is.

# `pattern`

```elixir
@type pattern() :: span() | String.t() | imported()
```

Anything that can be read as a span: a Tempo value, a string in
ISO 8601 (preferred) or RFC 5545 `RRULE` form, or the result of
`from_ical/1`.

# `span`

```elixir
@type span() :: Tempo.t() | Tempo.t() | Tempo.Duration.t() | Tempo.t()
```

A Tempo value that already denotes a span.

# `free`

```elixir
@spec free(Agenda.Resource.t(), keyword()) :: {:ok, Tempo.t()} | {:error, term()}
```

The time `resource` is open and not already taken.

`free = open − busy`, clipped to the query window. Computed on
demand, so it cannot go stale.

### Arguments

* `resource` is a `t:Agenda.Resource.t/0`.

* `options` is a keyword list of options.

### Options

* `:within` is the query window — a Tempo value or ISO 8601 string.
  Required: an unbounded recurrence has no materialisation without
  one.

* `:busy` is what already claims the resource — a Tempo value, a
  string, a `t:Tempo.t/0` interval set, or a list of them. The
  default is `[]`.

### Returns

* `{:ok, interval_set}` of the free spans, or

* `{:error, reason}` when the window or a pattern cannot be read.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.Resource.new("Boardroom")
    iex> {:ok, boardroom} = Agenda.Availability.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
    iex> {:ok, free} = Agenda.Availability.free(boardroom,
    ...>   within: "2026-06-15/2026-06-15",
    ...>   busy: "2026-06-15T12:00:00/2026-06-15T13:00:00")
    iex> Tempo.IntervalSet.members(free)
    [~o"2026Y6M15DT9H0M0S/T12H0M0S",
     ~o"2026Y6M15DT13H0M0S/T17H0M0S"]

A resource with no open hours is never free:

    iex> Agenda.Resource.new("Boardroom")
    ...> |> Agenda.Availability.free(within: "2026-06-15/2026-06-15")
    ...> |> then(fn {:ok, free} -> Tempo.empty?(free) end)
    true

# `from_ical`

```elixir
@spec from_ical(String.t()) ::
  {:ok, imported()} | {:error, :ical_not_available | String.t()}
```

Read a resource's open hours from an RFC 7953 `VAVAILABILITY`.

This is how a calendar system states availability, and it is what a
CalDAV server will hand you. The result is a pattern for `open/2`,
held unmaterialised until a query window is known — a
`VAVAILABILITY` whose `AVAILABLE` subcomponents recur has no extent
of its own, exactly as an ISO 8601 recurrence has none.

`VEVENT`s in the same document are ignored. They are what is
*taken*, not what is *offered*, and belong in `free/2`'s `:busy`
rather than in a resource's open hours.

Requires the optional `ical` dependency.

### Arguments

* `ics` is iCalendar data as a string.

### Returns

* `{:ok, pattern}` to hand to `open/2`, or

* `{:error, reason}` when the data cannot be read, or when `ical`
  is not available.

### Examples

    iex> ics = """
    ...> BEGIN:VCALENDAR
    ...> VERSION:2.0
    ...> BEGIN:VAVAILABILITY
    ...> UID:clinic
    ...> DTSTAMP:20260601T000000Z
    ...> BEGIN:AVAILABLE
    ...> UID:weekday-clinic
    ...> DTSTAMP:20260601T000000Z
    ...> DTSTART:20260601T090000Z
    ...> DTEND:20260601T170000Z
    ...> RRULE:FREQ=DAILY;COUNT=5
    ...> END:AVAILABLE
    ...> END:VAVAILABILITY
    ...> END:VCALENDAR
    ...> """
    iex> {:ok, hours} = Agenda.Availability.from_ical(ics)
    iex> {:ok, clinic} = Agenda.open(Agenda.resource("Clinic"), hours)
    iex> {:ok, free} = Agenda.free(clinic, within: "2026-06-01/2026-06-07")
    iex> Tempo.IntervalSet.count(free)
    5

# `normalise`

```elixir
@spec normalise(term()) :: {:ok, span() | imported()} | {:error, :unreadable_pattern}
```

Read `pattern` as a Tempo value.

Tempo values pass through untouched. Strings are tried as ISO 8601
first — the preferred spelling — and then as an RFC 5545 `RRULE`, so
a calendar system's own recurrence rule is accepted at the interface
without the caller translating it.

### Arguments

* `pattern` is a Tempo value or a string.

### Returns

* `{:ok, value}`, or

* `{:error, :unreadable_pattern}` when it is neither.

### Examples

    iex> {:ok, value} = Agenda.Availability.normalise("R5/2026-06-15/P1D")
    iex> value
    ...> |> Tempo.to_interval!()
    ...> |> Tempo.IntervalSet.count()
    5

    iex> {:ok, value} = Agenda.Availability.normalise("FREQ=DAILY;COUNT=3")
    iex> Tempo.to_iso8601!(value)
    "R3/../P1D"

# `open`

```elixir
@spec open(Agenda.Resource.t(), pattern()) ::
  {:ok, Agenda.Resource.t()} | {:error, term()}
```

Set when `resource` is open.

### Arguments

* `resource` is a `t:Agenda.Resource.t/0`.

* `pattern` is a Tempo value, an ISO 8601 string (preferred), or an
  RFC 5545 `RRULE` string.

### Returns

* `{:ok, resource}` with its open hours set, or

* `{:error, reason}` when `pattern` cannot be read as a span.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.Resource.new("Boardroom")
    iex> {:ok, boardroom} = Agenda.Availability.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
    iex> boardroom.open
    ~o"2026Y6M15DT9H0M0S/T17H0M0S"

    iex> boardroom = Agenda.Resource.new("Boardroom")
    iex> Agenda.Availability.open(boardroom, "not a time")
    {:error, :unreadable_pattern}

# `open!`

```elixir
@spec open!(Agenda.Resource.t(), pattern()) :: Agenda.Resource.t()
```

Open hours on a resource, raising on a pattern it cannot read.

The `!` companion to `open/2`, for the case where the pattern is a
literal in the source rather than data: a guide, a notebook, or a
fixture. There the tuple is pure ceremony — an unreadable literal is
a typo, not a condition to handle — and unwrapping it at every setup
line buries what the example is actually about.

Reach for `open/2` wherever the pattern comes from outside the
program, which is most of an application.

### Arguments

* `resource` is a `t:Agenda.Resource.t/0`.

* `pattern` is as for `open/2`.

### Returns

* the resource with its open hours set; or raises `ArgumentError`.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.Availability.open!(Agenda.resource("Boardroom"), "2026-06-15T09:00:00/2026-06-15T17:00:00")
    iex> boardroom.open
    ~o"2026Y6M15DT9H0M0S/2026Y6M15DT17H0M0S"

    iex> Agenda.Availability.open!(Agenda.resource("Boardroom"), "not a pattern")
    ** (ArgumentError) Boardroom: cannot read :unreadable_pattern as open hours

---

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