Narrowing free time — the verbs that turn "when is this open?" into "when is this actually bookable?"
Each function takes free time and gives back less of it, and each is a single Tempo set operation wearing a domain name. That is the whole design: Tempo already knows how to intersect, union, and filter intervals correctly across calendars, zones, and daylight saving, so a scheduling vocabulary is naming those operations rather than reimplementing them.
They compose in a pipeline
Every function accepts either an interval set or the {:ok, set} a
previous step returned, and an {:error, reason} passes straight
through untouched. So a refinement reads as one sentence and still
short-circuits on failure:
iex> import Tempo.Sigils
iex> {:ok, boardroom} =
...> Agenda.resource("Boardroom")
...> |> Agenda.open("2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, bookable} =
...> boardroom
...> |> Agenda.free(within: "2027-03-02/2027-03-03", busy: "2027-03-02T12:00:00/2027-03-02T13:00:00")
...> |> Agenda.only_during("2027-03-02T10:00:00/2027-03-02T16:00:00")
...> |> Agenda.lasting_at_least("PT2H")
iex> Tempo.IntervalSet.members(bookable)
[~o"2027Y3M2DT10H0M0S/T12H0M0S",
~o"2027Y3M2DT13H0M0S/T16H0M0S"]"The boardroom's free time, only between ten and four, in windows lasting at least two hours."
The lunch hour splits the day in two and the ten-to-four bound trims both ends; what survives is a two-hour morning and a three-hour afternoon.
Summary
Types
Free time, or the result of the step that produced it.
Functions
Keep only the free time falling inside any of constraints.
Keep only the windows long enough to be worth offering.
Keep only the free time that falls inside constraint.
Types
@type refinable() :: Tempo.IntervalSet.t() | {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Free time, or the result of the step that produced it.
Functions
@spec during_any(refinable(), [Agenda.Availability.pattern()]) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Keep only the free time falling inside any of constraints.
Where only_during/2 is a single condition every window must meet,
this is the grouped alternative — bookable while any of these
venues is open, or while any of a roster is on duty. An empty
list constrains nothing, matching the identity of Tempo's own n-ary
set operations.
Arguments
freeis an interval set, or the{:ok, set}from a previous step.constraintsis a list of Tempo values or ISO 8601 strings.
Returns
{:ok, interval_set}; or{:error, reason}.
Examples
iex> import Tempo.Sigils
iex> {:ok, room} =
...> Agenda.resource("Room")
...> |> Agenda.open("2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, staffed} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-03")
...> |> Agenda.during_any([
...> "2027-03-02T09:00:00/2027-03-02T10:00:00",
...> "2027-03-02T15:00:00/2027-03-02T17:00:00"
...> ])
iex> Tempo.IntervalSet.members(staffed)
[~o"2027Y3M2DT9H0M0S/T10H0M0S",
~o"2027Y3M2DT15H0M0S/T17H0M0S"]
@spec lasting_at_least(refinable(), Agenda.Availability.pattern()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Keep only the windows long enough to be worth offering.
A three-minute gap between two meetings is free time nobody can use.
Arguments
freeis an interval set, or the{:ok, set}from a previous step.durationis the shortest useful window, aTempo.Duration.t/0or ISO 8601 string.
Returns
{:ok, interval_set}; or{:error, reason}.
Examples
iex> import Tempo.Sigils
iex> {:ok, room} =
...> Agenda.resource("Room")
...> |> Agenda.open("2027-03-02T09:00:00/2027-03-02T12:00:00")
iex> {:ok, usable} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-03",
...> busy: ["2027-03-02T09:20:00/2027-03-02T09:30:00",
...> "2027-03-02T10:00:00/2027-03-02T10:30:00"])
...> |> Agenda.lasting_at_least("PT1H")
iex> Tempo.IntervalSet.members(usable)
[~o"2027Y3M2DT10H30M0S/T12H0M0S"]
@spec only_during(refinable(), Agenda.Availability.pattern()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Keep only the free time that falls inside constraint.
This is how a resource is made subject to something else's hours — a consulting room bookable only during clinic hours, a court only while the building is open. The constraint is ordinary free time itself, so constraints compose by applying them in turn.
Arguments
freeis an interval set, or the{:ok, set}from a previous step.constraintis a Tempo value or ISO 8601 string bounding it.
Returns
{:ok, interval_set}; or{:error, reason}, including any error passed in.
Examples
iex> import Tempo.Sigils
iex> {:ok, room} =
...> Agenda.resource("Room")
...> |> Agenda.open("2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, clinic} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-03")
...> |> Agenda.only_during("2027-03-02T13:00:00/2027-03-02T16:00:00")
iex> Tempo.IntervalSet.members(clinic)
[~o"2027Y3M2DT13H0M0S/T16H0M0S"]