Agenda.Refine (Agenda v0.1.0)

Copy Markdown View Source

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

refinable()

@type refinable() ::
  Tempo.IntervalSet.t() | {:ok, Tempo.IntervalSet.t()} | {:error, term()}

Free time, or the result of the step that produced it.

Functions

during_any(free, list)

@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

  • free is an interval set, or the {:ok, set} from a previous step.

  • constraints is 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"]

lasting_at_least(free, duration)

@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

  • free is an interval set, or the {:ok, set} from a previous step.

  • duration is the shortest useful window, a Tempo.Duration.t/0 or 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"]

only_during(free, constraint)

@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

  • free is an interval set, or the {:ok, set} from a previous step.

  • constraint is 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"]