Resource-constrained scheduling for Tempo.
Tempo answers when is this free? This library answers what
should I book, and where? — it adds the named resources, the
attributes describing them, the places containing them, and the
requirements a session places on them.
The vocabulary
Resource — a named thing that can be allocated. People and rooms differ only in their attributes. See
Agenda.Resource.Place — a container of resources and other places, forming a tree. Travel between resources is derived from the tree rather than configured. See
Agenda.Place.Requirement — what a session demands, written in the predicate vocabulary of
Agenda.Predicate. SeeAgenda.Requirement.Session — something to be held: how long it runs, the window it must fall inside, and what it demands of resources. A session is a request, not a booking. See
Agenda.Session.Programme — sessions to be laid out together, with whatever relates them: tracks, orderings, preferences and interests. See
Agenda.Programme.Track — sessions that may not overlap each other, and that somebody following them must be able to travel between. Not overlapping is intrinsic to a track rather than an option on one: a set of sessions that may collide is already expressible, as a list. See
Agenda.Track.Interest — one resource would like a session with another. The demand side of the model: where interest is returned,
meetings/3writes the sessions. SeeAgenda.Interest.Arrangement — one workable way to hold a session: a particular interval, and the particular resources it would take. A proposal rather than a commitment —
plan/3returns many, and none of them is booked. SeeAgenda.Arrangement.Allocation — one resource, held for one session, over one interval. The unit a ledger stores, and what a
:taglabels. SeeAgenda.Allocation.Ledger — the record of what is allocated and to whom, and the only authority on it. Nothing stores availability:
free/2is open hours minus what the ledger holds, computed when asked, so a booking cannot drift out of step with the calendar it came from.A ledger is an ordinary immutable value rather than a running store, and that is deliberate — it is what lets
arrange/3fork one, try a layout, abandon it and try another. Durable shared state belongs in whatever persists the answer, not here. SeeAgenda.Ledger.
Reading a match aloud
iex> import Agenda.Predicate
iex> boardroom = Agenda.resource("Boardroom", seats: 8, video_conferencing: true)
iex> small = Agenda.resource("Meeting room 2", seats: 4)
iex> needs_a_room = Agenda.needs(:room, seats: at_least(8), video_conferencing: true)
iex> Agenda.eligible(needs_a_room, [boardroom, small])
...> |> Enum.map(& &1.name)
["Boardroom"]"Of the two rooms, only the boardroom seats eight and has video conferencing."
And when a resource does not qualify, the reason is a sentence:
iex> import Agenda.Predicate
iex> small = Agenda.resource("Meeting room 2", seats: 4)
iex> needs_a_room = Agenda.needs(:room, seats: at_least(8))
iex> Agenda.explain(needs_a_room, small)
"Meeting room 2: seats is 4 — needs at least 8"Status
First release. Resources and places, matching by description or by name, derived availability, single-session planning, whole-programme arrangement, ordering, tracks, load limits, preferences, the allocation ledger, recurrence, reconciliation, and meetings generated from mutual interest are all here, and each has a guide.
Two things are deliberately outside it. Booking — durable shared state, a clock, and concurrent claims — belongs in a sibling library, because the arranger's freedom to fork and abandon ledgers depends on a ledger being an ordinary value. And holiday calendars are accepted as data rather than resolved here, since jurisdiction rules have far broader use than scheduling. An adapter for Ash is planned; see the plan.
Summary
Functions
Record an arrangement. Delegates to Agenda.Ledger.allocate/3.
Find a placement for every session in a programme. Delegates to
Agenda.Arranger.arrange/3.
Rebuild what a ledger holds as arrangements, ready to pin. Delegates
to Agenda.Ledger.arrangements/3.
What each resource is already claimed for. Delegates to
Agenda.Ledger.busy/2.
Cancel a session, freeing everything it holds, whether held or
booked. Delegates to Agenda.Ledger.cancel/2.
Book one resource over one interval, refusing what it cannot honour.
Delegates to Agenda.Ledger.claim/4.
Turn a session's hold into a firm allocation. Delegates to
Agenda.Ledger.confirm/2.
The smallest set of things that cannot hold together — sessions for a programme, demands for a session.
How many allocations a ledger holds. Delegates to
Agenda.Ledger.count/1.
Keep only free time inside any of constraints. Delegates to
Agenda.Refine.during_any/2.
The resources satisfying requirement. Delegates to
Agenda.Requirement.eligible/2.
Expand a repeating session into its occurrences. Delegates to
Agenda.Series.expand/3.
Drop every hold that has lapsed by a moment. Delegates to
Agenda.Ledger.expire/2.
Describe an arrangement, a partial layout, or an infeasible result as a sentence.
Why resource does or does not satisfy requirement, as a sentence.
What each preference contributed, as sentences.
When a resource is open and not already taken. Delegates to
Agenda.Availability.free/2.
What each resource is already claimed for, and whether each claim is
firm (:busy) or only held (:tentative). Delegates to
Agenda.Ledger.free_busy/2.
Read open hours from an RFC 7953 VAVAILABILITY. Delegates to
Agenda.Availability.from_ical/1.
Claim resources tentatively, until a moment. Delegates to
Agenda.Ledger.hold/3.
Every allocation that is still only a hold. Delegates to
Agenda.Ledger.holds/1.
Register that one resource would like a session with another.
Delegates to Agenda.Programme.interest/3.
Invite resources who may attend but are not required. Delegates to
Agenda.Session.invite/3.
Keep only windows of at least duration. Delegates to
Agenda.Refine.lasting_at_least/2.
An empty ledger. Delegates to Agenda.Ledger.new/0.
Turn returned interest into sessions. Delegates to
Agenda.Programme.meetings/3.
Build an attribute requirement. Delegates to
Agenda.Requirement.new/2.
Keep only free time inside constraint. Delegates to
Agenda.Refine.only_during/2.
Set when a resource is open. Delegates to
Agenda.Availability.open/2.
Open hours on a resource, raising on a pattern it cannot read.
Delegates to Agenda.Availability.open!/2.
Build a place. Delegates to Agenda.Place.new/2.
Rank the ways a session could be held. Delegates to
Agenda.Planner.plan/3.
Require that one session finishes before another starts. Delegates
to Agenda.Programme.precede/4.
Add a soft constraint to a programme. Delegates to
Agenda.Programme.prefer/3.
Build a programme. Delegates to Agenda.Programme.new/2.
Require that a delegate can get between consecutive track sessions.
Delegates to Agenda.Track.reachable/2.
What would change if a session moved. Delegates to
Agenda.Ledger.diff/3.
Whether a resource's claims account for the time it owed. Delegates
to Agenda.Reconciliation.reconcile/3.
Record one resource over one interval, checking nothing. Delegates
to Agenda.Ledger.record/4.
Free everything a session holds. Delegates to
Agenda.Ledger.release/2.
Free everything held by a whole series. Delegates to
Agenda.Ledger.release_series/3.
Build a resource. Delegates to Agenda.Resource.new/2.
Build a requirement naming specific resources. Delegates to
Agenda.Requirement.roster/2.
What a layout costs against a programme's preferences.
Build a session. Delegates to Agenda.Session.new/2.
Build a track — sessions that cannot clash with each other.
Delegates to Agenda.Track.new/2.
How long it takes to get from one resource to another.
Functions
@spec allocate(Agenda.Ledger.t(), Agenda.Arrangement.t(), keyword()) :: {:ok, Agenda.Ledger.t()}
Record an arrangement. Delegates to Agenda.Ledger.allocate/3.
Arguments
ledgeris aAgenda.Ledger.t/0.arrangementis aAgenda.Arrangement.t/0.optionsis a keyword list of options.
Options
:tagis what the claim was for, as aAgenda.Allocation.tag/0—{:project, "ACME-2026-01"},{:leave, :annual}. Recorded on every allocation the arrangement implies, and grouped on byAgenda.reconcile/3. The default isnil.
Returns
{:ok, ledger}.
Examples
iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...> session: "Review",
...> interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...> allocations: %{room: [Agenda.resource("Boardroom")]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement)
iex> Agenda.count(ledger)
1Tagging what the time was for:
iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...> session: "Discovery",
...> interval: ~o"2026-06-16T09:00:00/2026-06-16T17:00:00",
...> allocations: %{consultant: [Agenda.resource("Dana")]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement, tag: {:project, "ACME"})
iex> ledger
...> |> Agenda.Ledger.to_list()
...> |> Enum.map(& &1.tag)
[project: "ACME"]
@spec arrange(Agenda.Programme.t(), [Agenda.Resource.t()], keyword()) :: Agenda.Arranger.result()
Find a placement for every session in a programme. Delegates to
Agenda.Arranger.arrange/3.
Arguments
programmeis aAgenda.Programme.t/0.poolis the list ofAgenda.Resource.t/0to choose from.optionsis a keyword list of options.
Options
:busyis a map of resource name to what already claims it, asAgenda.Ledger.busy/2returns. The default is%{}. It must not include the claims of:pinnedsessions — those are added for you, so passAgenda.Ledger.busy/2the pinned session names as:except.:pinnedis a list ofAgenda.Arrangement.t/0whose placements are fixed. Those sessions are not searched for, every constraint still applies to them, and they are returned alongside the sessions that were placed. The default is[].:unplaceddecides what happens when a session cannot be held —:error(the default) fails the whole programme,:allowleaves out as few sessions as the search can manage and returns{:partial, layout}.:candidatescaps how many placements are considered per session. The default scales with the programme —40, or ten more than the number of sessions, whichever is larger. Interchangeable sessions are offered the same ranked placements, so a cap below the session count makes a satisfiable programme unsatisfiable.:nodescaps how many search steps are taken across the whole call, including every round of theunplaced: :allowsearch and every independent subproblem the programme splits into. The default scales with the programme —10_000, or 250 per session, whichever is larger — because the work per session grows with the programme and a fixed cap reports a satisfiable one as impossible.:score_nodescaps the search steps of the second pass, which looks for a better-scoring layout once the first has proved how many sessions can be placed. The default is2_000; a layout that stops at it reportsscore_proven?: false.:concurrencyis how many processes may work at once, defaulting toSystem.schedulers_online/0. Candidate enumeration and independent subproblems are both spread across them. Pass1to stay on the calling process — what you want when the caller already runs this inside a pool of its own. The answer does not depend on it: results are collected in order, so the same programme arranges the same way at any setting.:travelis passed toAgenda.travel_time/3for the reachability check — use it to supply per-pair overrides.
Returns
{:ok, arrangements}— one per session, mutually consistent, in programme order, or{:partial, t:Agenda.Layout.t/0}underunplaced: :allow, when some sessions could not be held, or{:error, t:Agenda.Infeasible.t/0}naming the session that could not be placed, reporting a bad pin, or reporting that the cap was hit.
Examples
iex> room = Agenda.resource("Hall", seats: 100)
iex> {:ok, room} = Agenda.open(room, "2026-09-15T09:00:00/2026-09-15T11:00:00")
iex> talk = Agenda.session("Keynote", duration: "PT1H")
...> |> Agenda.Session.needs(:room, seats: 100)
iex> programme =
...> Agenda.programme("Conf", across: "2026-09-15/2026-09-15")
...> |> Agenda.Programme.add_session(talk)
iex> {:ok, [only]} = Agenda.arrange(programme, [room])
iex> only.session
"Keynote"
@spec arrangements(Agenda.Ledger.t(), [Agenda.Resource.t()], keyword()) :: {:ok, [Agenda.Arrangement.t()]} | {:error, Agenda.Infeasible.t()}
Rebuild what a ledger holds as arrangements, ready to pin. Delegates
to Agenda.Ledger.arrangements/3.
Arguments
ledgeris aAgenda.Ledger.t/0.poolis the list ofAgenda.Resource.t/0the allocations were made from.optionsis a keyword list of options.
Options
:onlyis a session name, or a list of them, to rebuild. The default is every session in the ledger.
Returns
{:ok, arrangements}— one per session, ordered by session name, or{:error, t:Agenda.Infeasible.t/0}naming the resources the pool does not have.
Examples
iex> import Tempo.Sigils
iex> boardroom = Agenda.resource("Boardroom")
iex> arrangement = %Agenda.Arrangement{
...> session: "Review",
...> interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...> allocations: %{room: [boardroom]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement)
iex> {:ok, [pinned]} = Agenda.arrangements(ledger, [boardroom])
iex> pinned.session
"Review"
@spec busy(Agenda.Ledger.t(), keyword()) :: %{optional(String.t()) => [Tempo.t()]}
What each resource is already claimed for. Delegates to
Agenda.Ledger.busy/2.
Arguments
ledgeris aAgenda.Ledger.t/0.optionsis a keyword list of options.
Options
:exceptis a session name, or a list of them, whose allocations are ignored — use it when re-planning a session so it does not collide with the copy of itself it is about to replace, or when pinning sessions inAgenda.arrange/3, which claims what its pins hold and so must not be told about them twice.
Returns
- a map of resource name to the intervals claiming it.
Examples
iex> Agenda.busy(Agenda.ledger())
%{}
@spec cancel(Agenda.Ledger.t(), String.t()) :: {:ok, Agenda.Ledger.t()}
Cancel a session, freeing everything it holds, whether held or
booked. Delegates to Agenda.Ledger.cancel/2.
hold/3, confirm/2 and cancel/2 are a session's lifecycle as a
calendar names it; release/2 is the same freeing, named from the
resources' side.
Arguments
ledgeris aAgenda.Ledger.t/0.sessionis the session's name.
Returns
{:ok, ledger}. Cancelling a session that holds nothing is a no-op, so this is safe to call twice.
Examples
iex> {:ok, ledger} = Agenda.cancel(Agenda.ledger(), "Review")
iex> Agenda.count(ledger)
0
@spec claim( Agenda.Ledger.t(), Agenda.Resource.t(), Agenda.Availability.pattern(), keyword() ) :: {:ok, Agenda.Ledger.t()} | {:error, term()}
Book one resource over one interval, refusing what it cannot honour.
Delegates to Agenda.Ledger.claim/4.
record/4 writes down what happened; claim/4 books what is to
happen, and checks. See Agenda.Ledger.claim/4 for why a timesheet
wants the first and a schedule wants the second.
Arguments
ledgeris aAgenda.Ledger.t/0.resourceis theAgenda.Resource.t/0being claimed.intervalis when — a Tempo value or an ISO 8601 string.optionsis a keyword list of options.
Options
:tagis what the claim is for, as aAgenda.Allocation.tag/0.:requirementis the requirement the resource is filling. The default is:resource.:sessionnames the claim, and is whatAgenda.Ledger.release/2takes. The default is the interval's ISO 8601 form, which is unique per resource per span.
Returns
{:ok, ledger}, or{:error, reason}— a sentence naming the time that could not be claimed and why, when the resource is not open then or is already claimed for part of it.
Examples
iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> {:ok, ledger} = Agenda.claim(Agenda.ledger(), dana, ~o"2026-08-10T09:00:00/2026-08-10T12:00:00", tag: {:project, "ACME"})
iex> ledger
...> |> Agenda.Ledger.to_list()
...> |> Enum.map(& &1.tag)
[project: "ACME"]
iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> Agenda.claim(Agenda.ledger(), dana, ~o"2026-08-10T16:00:00/2026-08-10T19:00:00")
{:error, "Dana is not open for 2026Y8M10DT17H0M0S/T19H0M0S"}
@spec confirm(Agenda.Ledger.t(), String.t()) :: {:ok, Agenda.Ledger.t()}
Turn a session's hold into a firm allocation. Delegates to
Agenda.Ledger.confirm/2.
Arguments
ledgeris aAgenda.Ledger.t/0.sessionis the session's name.
Returns
{:ok, ledger}. Confirming a session that holds nothing, or one already firm, is a no-op.
Examples
iex> {:ok, ledger} = Agenda.confirm(Agenda.ledger(), "Review")
iex> Agenda.count(ledger)
0
@spec conflict( Agenda.Programme.t() | Agenda.Session.t(), [Agenda.Resource.t()], keyword() ) :: {:ok, [term()]} | :none
The smallest set of things that cannot hold together — sessions for a programme, demands for a session.
Delegates to Agenda.Arranger.conflict/3 or
Agenda.Planner.conflict/3 according to what it is given. Reach
for it when arrange/3 or plan/3 has failed and the question is
what to change.
Arguments
subjectis aAgenda.Programme.t/0or aAgenda.Session.t/0.poolis the list ofAgenda.Resource.t/0to choose from.optionsis a keyword list of options.
Options
For a programme,
:busy,:pinned,:candidates,:nodes,:score_nodes,:concurrencyand:travelare as forarrange/3.For a session,
:busy,:limitand:spreadare as forplan/3.
Returns
:nonewhen there is nothing to explain, or{:ok, conflict}— a minimal set of session names, or of demands.
Examples
iex> room = Agenda.resource("Hall", seats: 100)
iex> {:ok, room} = Agenda.open(room, "2026-09-15T09:00:00/2026-09-15T10:00:00")
iex> talk = fn name ->
...> Agenda.session(name, duration: "PT1H", window: "2026-09-15/2026-09-15")
...> |> Agenda.Session.needs(:room, seats: 100)
...> end
iex> programme =
...> Agenda.programme("Conf")
...> |> Agenda.Programme.add_session(talk.("Keynote"))
...> |> Agenda.Programme.add_session(talk.("Deep dive"))
iex> Agenda.conflict(programme, [room])
{:ok, ["Keynote", "Deep dive"]}
@spec count(Agenda.Ledger.t()) :: non_neg_integer()
How many allocations a ledger holds. Delegates to
Agenda.Ledger.count/1.
Arguments
ledgeris aAgenda.Ledger.t/0.
Returns
- the count.
Examples
iex> Agenda.count(Agenda.ledger())
0
@spec during_any(Agenda.Refine.refinable(), [Agenda.Availability.pattern()]) :: {:ok, Tempo.t()} | {:error, term()}
Keep only free time inside any of constraints. Delegates to
Agenda.Refine.during_any/2.
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> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, staffed} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-02")
...> |> Agenda.during_any(["2027-03-02T09:00:00/2027-03-02T10:00:00"])
iex> Tempo.IntervalSet.count(staffed)
1
@spec eligible(Agenda.Requirement.t(), [Agenda.Resource.t()]) :: [Agenda.Resource.t()]
The resources satisfying requirement. Delegates to
Agenda.Requirement.eligible/2.
Arguments
requirementis aAgenda.Requirement.t/0.candidatesis a list ofAgenda.Resource.t/0.
Returns
- the eligible resources, in the order given.
Examples
iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> Agenda.eligible(Agenda.needs(:room, seats: 8), [boardroom])
...> |> Enum.map(& &1.name)
["Boardroom"]
@spec every(Agenda.Session.t(), Agenda.Availability.pattern(), keyword()) :: {:ok, [Agenda.Session.t()]} | {:error, term()}
Expand a repeating session into its occurrences. Delegates to
Agenda.Series.expand/3.
Arguments
sessionis theAgenda.Session.t/0to repeat. Its own window, if any, is replaced by each occurrence's.patternis a recurrence — an ISO 8601 repeating interval (preferred) or an RFC 5545RRULE.optionsis a keyword list of options.
Options
:boundbounds an otherwise unbounded recurrence: the occurrences that overlap it are expanded. Required whenpatterndoes not state its own count or end.
Returns
{:ok, sessions}, one per occurrence, in time order, or{:error, reason}when the pattern cannot be read or is unbounded and unbounded by:bound.
Examples
iex> standup = Agenda.session("Stand-up", duration: "PT15M")
iex> {:ok, occurrences} = Agenda.every(standup, "R3/2027-03-02T09:00:00/P1W")
iex> length(occurrences)
3
@spec expire(Agenda.Ledger.t(), Agenda.Availability.pattern()) :: {:ok, Agenda.Ledger.t()} | {:error, term()}
Drop every hold that has lapsed by a moment. Delegates to
Agenda.Ledger.expire/2.
Nothing expires on its own — this is what advances time, and it takes the moment as an argument so that no function in this library reads a clock.
Arguments
ledgeris aAgenda.Ledger.t/0.nowis the current moment — a Tempo value or an ISO 8601 string.
Returns
{:ok, ledger}with lapsed holds removed, or{:error, reason}whennowcannot be read as a moment.
Examples
iex> {:ok, ledger} = Agenda.expire(Agenda.ledger(), "2026-06-15T10:15:00")
iex> Agenda.count(ledger)
0
@spec explain(Agenda.Arrangement.t() | Agenda.Layout.t() | Agenda.Infeasible.t()) :: String.t()
Describe an arrangement, a partial layout, or an infeasible result as a sentence.
Arguments
resultis aAgenda.Arrangement.t/0, aAgenda.Layout.t/0, or aAgenda.Infeasible.t/0.
Returns
- a sentence.
Examples
iex> reason = Agenda.Infeasible.new("Review", ["no room seats 8"])
iex> Agenda.explain(reason)
"Review cannot be held: no room seats 8"
iex> reason = Agenda.Infeasible.new("Workshop", ["no room seats 8"])
iex> Agenda.explain(Agenda.Layout.new("Conf", [], [reason]))
"Conf: 0 of 1 sessions placed. Workshop cannot be held: no room seats 8"
@spec explain(Agenda.Requirement.t(), Agenda.Resource.t()) :: String.t()
Why resource does or does not satisfy requirement, as a sentence.
Arguments
requirementis aAgenda.Requirement.t/0.resourceis aAgenda.Resource.t/0.
Returns
- a sentence naming the resource and every unmet attribute, or stating that it qualifies.
Examples
iex> import Agenda.Predicate
iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> Agenda.explain(Agenda.needs(:room, seats: at_least(8)), boardroom)
"Boardroom qualifies"
@spec explain_score( [Agenda.Arrangement.t()] | Agenda.Layout.t(), Agenda.Programme.t(), keyword() ) :: [ String.t() ]
What each preference contributed, as sentences.
A score is a number, and a number says a layout is worse without saying how. This says how.
Arguments
arrangementsis a list ofAgenda.Arrangement.t/0, or aAgenda.Layout.t/0.programmeis theAgenda.Programme.t/0whose preferences to score against.optionsis a keyword list of options.
Options
:poolis the resources the layout drew on. The default is[].
Returns
- one sentence per preference, in the order they were declared.
Examples
iex> {:ok, programme} =
...> Agenda.Programme.prefer(Agenda.programme("Conf"), :room_changes, weight: 10)
iex> Agenda.explain_score([], programme)
["room_changes: 0 × 10 = 0"]
@spec free(Agenda.Resource.t(), keyword()) :: {:ok, Tempo.t()} | {:error, term()}
When a resource is open and not already taken. Delegates to
Agenda.Availability.free/2.
Arguments
resourceis aAgenda.Resource.t/0.optionsis a keyword list of options.
Options
:withinis the query window — a Tempo value or ISO 8601 string. Required: an unbounded recurrence has no materialisation without one.:busyis what already claims the resource — a Tempo value, a string, aTempo.t/0interval 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> boardroom = Agenda.resource("Boardroom")
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
iex> {:ok, free} = Agenda.free(boardroom, within: "2026-06-15/2026-06-15")
iex> Tempo.IntervalSet.count(free)
1
@spec free_busy(Agenda.Ledger.t(), keyword()) :: %{ optional(String.t()) => [{Agenda.Ledger.busy_type(), Tempo.t()}] }
What each resource is already claimed for, and whether each claim is
firm (:busy) or only held (:tentative). Delegates to
Agenda.Ledger.free_busy/2.
Arguments
ledgeris aAgenda.Ledger.t/0.optionsis a keyword list of options.
Options
:exceptis a session name, or a list of them, whose allocations are ignored, as forbusy/2.
Returns
- a map of resource name to
{busy_type, interval}pairs, each resource's ordered by when they begin. SeeAgenda.Ledger.busy_type/0.
Examples
iex> Agenda.free_busy(Agenda.ledger())
%{}
@spec from_ical(String.t()) :: {:ok, Agenda.Availability.imported()} | {:error, :ical_not_available | String.t()}
Read open hours from an RFC 7953 VAVAILABILITY. Delegates to
Agenda.Availability.from_ical/1.
This is what a CalDAV server hands you when asked when someone is
available. The result is a pattern for open/2.
Hours imported in UTC (Z) or in a zone are on that time line, and a
claim, a refinement or a session's window combined with them is written
on it too: a floating one beside them is a Tempo.FrameError, as Tempo
has it. A within: window alone may be floating, and is read on the
hours' clock.
Arguments
icsis iCalendar data as a string.
Returns
{:ok, pattern}to hand toAgenda.Availability.open/2, or{:error, reason}when the data cannot be read, or whenicalis not available.
Examples
iex> ics = """
...> BEGIN:VCALENDAR
...> VERSION:2.0
...> BEGIN:VAVAILABILITY
...> UID:consulting-room
...> DTSTAMP:20260601T000000Z
...> BEGIN:AVAILABLE
...> UID:tuesdays
...> DTSTAMP:20260601T000000Z
...> DTSTART:20260602T090000Z
...> DTEND:20260602T170000Z
...> RRULE:FREQ=WEEKLY;BYDAY=TU
...> END:AVAILABLE
...> END:VAVAILABILITY
...> END:VCALENDAR
...> """
iex> {:ok, hours} = Agenda.from_ical(ics)
iex> {:ok, room} = Agenda.open(Agenda.resource("Consulting room"), hours)
iex> {:ok, free} = Agenda.free(room, within: "2026-06-01/2026-06-30")
iex> Tempo.IntervalSet.count(free)
5
@spec hold(Agenda.Ledger.t(), Agenda.Arrangement.t(), keyword()) :: {:ok, Agenda.Ledger.t()} | {:error, term()}
Claim resources tentatively, until a moment. Delegates to
Agenda.Ledger.hold/3.
Arguments
ledgeris aAgenda.Ledger.t/0.arrangementis aAgenda.Arrangement.t/0.optionsis a keyword list of options.
Options
:untilis when the hold lapses — a Tempo value or an ISO 8601 string. Required.:tagis what the claim was for, as aAgenda.Allocation.tag/0. A hold occupies a resource exactly as a booking does, so it carries the same tag. The default isnil.
Returns
{:ok, ledger}, or{:error, reason}when:untilcannot be read as a moment.
Examples
iex> import Tempo.Sigils
iex> boardroom = Agenda.resource("Boardroom")
iex> arrangement = %Agenda.Arrangement{
...> session: "Review",
...> interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...> allocations: %{room: [boardroom]}
...> }
iex> {:ok, ledger} = Agenda.hold(Agenda.ledger(), arrangement,
...> until: "2026-06-15T10:15:00")
iex> Agenda.holds(ledger)
...> |> length()
1
@spec holds(Agenda.Ledger.t()) :: [Agenda.Allocation.t()]
Every allocation that is still only a hold. Delegates to
Agenda.Ledger.holds/1.
Arguments
ledgeris aAgenda.Ledger.t/0.
Returns
- the held allocations, ordered as
Agenda.Ledger.to_list/1orders them.
Examples
iex> Agenda.holds(Agenda.ledger())
[]
@spec interest( Agenda.Programme.t(), Agenda.Resource.t() | String.t(), Agenda.Resource.t() | String.t() ) :: {:ok, Agenda.Programme.t()} | {:error, term()}
Register that one resource would like a session with another.
Delegates to Agenda.Programme.interest/3.
Interest is one-directional. A meeting is held where it is returned,
so both parties must say so — see meetings/3.
Arguments
programmeis aAgenda.Programme.t/0.fromis the resource registering the interest, as aAgenda.Resource.t/0or its name.tois the resource it would like to meet, in the same two forms.
Returns
{:ok, programme}, or{:error, :self_interest}when a resource names itself.
Examples
iex> {:ok, programme} = Agenda.interest(Agenda.programme("Show"), "Kim", "Harbour")
iex> {:ok, programme} = Agenda.interest(programme, "Harbour", "Kim")
iex> Agenda.Interest.mutual(programme.interests)
[{"Harbour", "Kim"}]
@spec invite(Agenda.Session.t(), atom(), [Agenda.Resource.t()]) :: Agenda.Session.t()
Invite resources who may attend but are not required. Delegates to
Agenda.Session.invite/3.
The counterpart to roster/2. An invitee never affects whether a
session can be held, only which time is best: Agenda.plan/3 scores
a placement higher when more of them are free, and the arrangement
records which of them it suits. They are not allocated — an optional
attendee that could cost some other session its placement would not
be optional.
Arguments
sessionis aAgenda.Session.t/0.namenames what the invitees would be there as, the same wayAgenda.Session.roster/3does.resourcesis a list ofAgenda.Resource.t/0.
Returns
- The session, with the invitees added.
Examples
iex> bob = Agenda.resource("Bob")
iex> session = Agenda.session("Review", duration: "PT1H")
iex> Agenda.invite(session, :optional, [bob]).invitees
...> |> Keyword.keys()
[:optional]
@spec lasting_at_least(Agenda.Refine.refinable(), Agenda.Availability.pattern()) :: {:ok, Tempo.t()} | {:error, term()}
Keep only windows of at least duration. Delegates to
Agenda.Refine.lasting_at_least/2.
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> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T09:30:00")
iex> {:ok, usable} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-02")
...> |> Agenda.lasting_at_least("PT1H")
iex> Tempo.empty?(usable)
true
@spec ledger() :: Agenda.Ledger.t()
An empty ledger. Delegates to Agenda.Ledger.new/0.
Returns
- a
Agenda.Ledger.t/0holding no allocations.
Examples
iex> Agenda.count(Agenda.ledger())
0
@spec meetings(Agenda.Programme.t(), [Agenda.Resource.t()], keyword()) :: {:ok, Agenda.Programme.t()} | {:error, term()}
Turn returned interest into sessions. Delegates to
Agenda.Programme.meetings/3.
One session per mutually interested pair, each rostering both parties, so nobody being in two places at once is the constraint the library already enforces rather than one anybody writes.
Arguments
programmeis aAgenda.Programme.t/0carrying the interests.poolis the resources the interests name.optionsis a keyword list of options.
Options
:durationis how long each meeting runs. Required.:windowis when meetings may be held. Defaults to the programme's own window.:needsis what every meeting requires beyond its two parties, as[name: predicates]— a table, most often.:asis the requirement both parties are rostered under. The default is:party, which says neither of them is substitutable.:nameis a two-argument function naming a meeting from the pair. The default reads"Kim with Harbour Tours".
Returns
{:ok, programme}with one session added per mutual pair, or{:error, {:missing_option, :duration}}, or{:error, {:unknown_resources, names}}when an interest names a resource the pool does not hold.
Examples
iex> kim = Agenda.resource("Kim")
iex> harbour = Agenda.resource("Harbour")
iex> programme = Agenda.programme("Show", across: "2027-06-15/2027-06-16")
iex> {:ok, programme} = Agenda.interest(programme, kim, harbour)
iex> {:ok, programme} = Agenda.interest(programme, harbour, kim)
iex> {:ok, programme} = Agenda.meetings(programme, [kim, harbour], duration: "PT15M")
iex> length(programme.sessions)
1
@spec needs(atom(), keyword()) :: Agenda.Requirement.t()
Build an attribute requirement. Delegates to
Agenda.Requirement.new/2.
Arguments
nameis the requirement's name, such as:room.predicatesis a keyword list of attribute predicates.
Returns
Examples
iex> Agenda.needs(:room, seats: 8).name
:room
@spec only_during(Agenda.Refine.refinable(), Agenda.Availability.pattern()) :: {:ok, Tempo.t()} | {:error, term()}
Keep only free time inside constraint. Delegates to
Agenda.Refine.only_during/2.
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> {:ok, room} = Agenda.open(Agenda.resource("R"), "2027-03-02T09:00:00/2027-03-02T17:00:00")
iex> {:ok, clinic} =
...> room
...> |> Agenda.free(within: "2027-03-02/2027-03-02")
...> |> Agenda.only_during("2027-03-02T13:00:00/2027-03-02T16:00:00")
iex> Tempo.IntervalSet.count(clinic)
1
@spec open(Agenda.Resource.t(), Agenda.Availability.pattern()) :: {:ok, Agenda.Resource.t()} | {:error, term()}
Set when a resource is open. Delegates to
Agenda.Availability.open/2.
Arguments
resourceis aAgenda.Resource.t/0.patternis a Tempo value, an ISO 8601 string (preferred), or an RFC 5545RRULEstring.
Returns
{:ok, resource}with its open hours set, or{:error, reason}whenpatterncannot be read as a span.
Examples
iex> boardroom = Agenda.resource("Boardroom")
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T17:00:00")
iex> is_nil(boardroom.open)
false
@spec open!(Agenda.Resource.t(), Agenda.Availability.pattern()) :: Agenda.Resource.t()
Open hours on a resource, raising on a pattern it cannot read.
Delegates to Agenda.Availability.open!/2.
The ! companion to open/2, for a pattern that is a literal in the
source — a guide, a notebook, a fixture — where the tuple is ceremony
around what cannot fail except by typo.
Arguments
resourceis aAgenda.Resource.t/0.patternis as forAgenda.Availability.open/2.
Returns
- the resource with its open hours set; or raises
ArgumentError.
Examples
iex> import Tempo.Sigils
iex> Agenda.open!(Agenda.resource("Boardroom"), ~o"2026-06-15T09:00:00/2026-06-15T17:00:00").open
~o"2026Y6M15DT9H0M0S/2026Y6M15DT17H0M0S"
@spec place(String.t(), keyword()) :: Agenda.Place.t()
Build a place. Delegates to Agenda.Place.new/2.
Arguments
nameis the place's name.optionsis a keyword list of options.
Options
:withinis the enclosingAgenda.Place.t/0. The default isnil, making this place a root.:location_typeis what kind of place it is, iCalendar'sLOCATION-TYPE, as a list of atoms such as[:office]. The default is[].
Returns
Examples
iex> Agenda.place("Level 2").name
"Level 2"
@spec plan(Agenda.Session.t(), [Agenda.Resource.t()], keyword()) :: {:ok, [Agenda.Arrangement.t()]} | {:error, Agenda.Infeasible.t()}
Rank the ways a session could be held. Delegates to
Agenda.Planner.plan/3.
Arguments
sessionis aAgenda.Session.t/0with a duration and a window.poolis the list ofAgenda.Resource.t/0available to choose from.optionsis a keyword list of options.
Options
:busyis a map of resource name to what already claims it — any valueAgenda.Availability.free/2accepts. The default is%{}.:limitis the most arrangements to return. The default is20. Truncation is reported rather than silent — see:truncated?on the result.:spreadchooses how the list is truncated.false, the default, keeps the best:limitplacements, which is what a caller picking one time wants.truesamples start moments evenly across the window instead and round-robins between them, trading depth at the front of the window for coverage of all of it — which is whatAgenda.Arranger.arrange/3needs, since sessions handed identical placements collide. The coverage is what makes a cap safe to set: with:spread,limit: 4over two days offers both days rather than four times on the first.
Returns
{:ok, arrangements}ranked best-first, or{:error, t:Agenda.Infeasible.t/0}carrying the reasons — which includes a session that never said when it may be held.
Examples
iex> boardroom = Agenda.resource("Boardroom", seats: 8)
iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T11:00:00")
iex> session =
...> Agenda.session("Review", duration: "PT1H", window: "2026-06-15/2026-06-15")
...> |> Agenda.Session.needs(:room, seats: 8)
iex> {:ok, [best | _]} = Agenda.plan(session, [boardroom])
iex> Agenda.explain(best)
"2026Y6M15DT9H0M0S/T10H0M0S — room: Boardroom"
@spec precede(Agenda.Programme.t(), String.t(), String.t(), keyword()) :: {:ok, Agenda.Programme.t()} | {:error, term()}
Require that one session finishes before another starts. Delegates
to Agenda.Programme.precede/4.
Arguments
programmeis aAgenda.Programme.t/0.firstis the name of the session that must finish first.thenis the name of the session that follows it.optionsis a keyword list of options.
Options
:gapis the least time that must pass between them.:withinis the most, measured from the end offirst.
Returns
{:ok, programme}, or{:error, reason}naming a session the programme does not have.
Examples
iex> programme =
...> Agenda.programme("Job")
...> |> Agenda.Programme.add_session(Agenda.session("Survey"))
...> |> Agenda.Programme.add_session(Agenda.session("Quote"))
iex> {:ok, programme} = Agenda.precede(programme, "Survey", "Quote", gap: "PT30M")
iex> length(programme.precedences)
1
@spec prefer(Agenda.Programme.t(), atom() | {atom(), function()}, keyword()) :: {:ok, Agenda.Programme.t()} | {:error, term()}
Add a soft constraint to a programme. Delegates to
Agenda.Programme.prefer/3.
Arguments
programmeis aAgenda.Programme.t/0.preferenceis a built-in name —:room_changesor:room_spread— or a{name, function}pair of your own.optionsis a keyword list of options.
Options
:weightis how much each violation costs. The default is1.
Returns
{:ok, programme}, or{:error, reason}when the preference is not recognised.
Examples
iex> {:ok, programme} = Agenda.prefer(Agenda.programme("Conf"), :room_spread)
iex> Enum.map(programme.preferences, & &1.name)
[:room_spread]
@spec programme(String.t(), keyword()) :: Agenda.Programme.t()
Build a programme. Delegates to Agenda.Programme.new/2.
Arguments
nameis the programme's name.optionsis a keyword list of options.
Options
:acrossis the window every session must fall inside.
Returns
Examples
iex> Agenda.programme("ElixirConf AU").name
"ElixirConf AU"
@spec reachable(Agenda.Track.t(), keyword()) :: {:ok, Agenda.Track.t()} | {:error, {:missing_option, :within}}
Require that a delegate can get between consecutive track sessions.
Delegates to Agenda.Track.reachable/2.
Arguments
trackis aAgenda.Track.t/0.optionsis a keyword list of options.
Options
:withinis the longest journey a delegate is expected to make, a duration.
Returns
{:ok, track}with the reachability requirement set, or{:error, {:missing_option, :within}}.
Examples
iex> import Tempo.Sigils
iex> {:ok, track} =
...> Agenda.track("Elixir")
...> |> Agenda.reachable(within: ~o"PT10M")
iex> track.reachable_within
~o"PT10M"
@spec rearrange(Agenda.Ledger.t(), String.t(), Agenda.Arrangement.t()) :: [ Agenda.Ledger.change() ]
What would change if a session moved. Delegates to
Agenda.Ledger.diff/3.
Arguments
ledgeris aAgenda.Ledger.t/0.sessionis the session's name.arrangementis theAgenda.Arrangement.t/0it should move to.
Returns
- a list of
Agenda.Ledger.change/0, ordered:keep, then:release, then:allocate.
Examples
iex> import Tempo.Sigils
iex> arrangement = %Agenda.Arrangement{
...> session: "Review",
...> interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
...> allocations: %{room: [Agenda.resource("Boardroom")]}
...> }
iex> Agenda.rearrange(Agenda.ledger(), "Review", arrangement)
...> |> Enum.map(&elem(&1, 0))
[:allocate]
@spec reconcile(Agenda.Ledger.t(), Agenda.Resource.t(), keyword()) :: {:ok, Agenda.Reconciliation.t()} | {:error, term()}
Whether a resource's claims account for the time it owed. Delegates
to Agenda.Reconciliation.reconcile/3.
The schedule and the timesheet are the same ledger read forwards and
backwards, so reconciling is a set difference rather than a sum:
unaccounted names which time is missing, not merely how much.
Holidays are not this library's business — pass them in through
:excluding as an ordinary interval set, from wherever they are
resolved.
Arguments
ledgeris aAgenda.Ledger.t/0.resourceis aAgenda.Resource.t/0.optionsis a keyword list of options.
Options
:withinis the window to reconcile — a Tempo value or an ISO 8601 string. Required, because open hours may be an unbounded recurrence and have no materialisation without one.:excludingis time the resource did not owe — public holidays, a shutdown — as aTempo.t/0interval set, any Tempo value that denotes a span (a bare day,~o"2026-08-12", is the natural way to write a holiday), a string, or a list of any of those. Subtracted from the expectation before any claim is compared against it. The default is none.:expectedstates the owed time outright, as anTempo.t/0interval set, instead of deriving it from the resource's open hours.:excludingstill applies to it.
Returns
{:ok, reconciliation}, or{:error, reason}when the window or a set cannot be read.
Examples
iex> import Tempo.Sigils
iex> {:ok, dana} =
...> Agenda.open(Agenda.resource("Dana"), ~o"2026-06-16T09:00:00/2026-06-16T17:00:00")
iex> arrangement = %Agenda.Arrangement{
...> session: "Acme",
...> interval: ~o"2026-06-16T09:00:00/2026-06-16T12:00:00",
...> allocations: %{consultant: [dana]}
...> }
iex> {:ok, ledger} = Agenda.allocate(Agenda.ledger(), arrangement, tag: {:project, "ACME"})
iex> {:ok, report} = Agenda.reconcile(ledger, dana, within: ~o"2026-06-16/2026-06-16")
iex> Agenda.Reconciliation.explain(report)
["Dana: 5 hours unaccounted — 2026Y6M16DT12H0M0S/T17H0M0S"]
@spec record( Agenda.Ledger.t(), Agenda.Resource.t(), Agenda.Availability.pattern(), keyword() ) :: {:ok, Agenda.Ledger.t()} | {:error, term()}
Record one resource over one interval, checking nothing. Delegates
to Agenda.Ledger.record/4.
The unchecked half of the pair claim/4 completes: a schedule is a
request and must be validated, a timesheet is a record and the world
is not obliged to match the contract. See Agenda.Ledger.record/4.
Arguments
ledgeris aAgenda.Ledger.t/0.resourceis theAgenda.Resource.t/0whose time is recorded.intervalis when — a Tempo value or an ISO 8601 string.optionsis a keyword list of options.
Options
As Agenda.Ledger.claim/4: :tag, :requirement and :session.
Returns
{:ok, ledger}, or{:error, reason}whenintervalcannot be read as a span. Never for a reason of availability — that isAgenda.Ledger.claim/4's business.
Examples
iex> import Tempo.Sigils
iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
iex> {:ok, ledger} = Agenda.record(Agenda.ledger(), dana, ~o"2026-08-15T09:00:00/2026-08-15T12:00:00", tag: {:project, "ACME"})
iex> ledger
...> |> Agenda.Ledger.to_list()
...> |> Enum.map(& &1.tag)
[project: "ACME"]
@spec release(Agenda.Ledger.t(), String.t()) :: {:ok, Agenda.Ledger.t()}
Free everything a session holds. Delegates to
Agenda.Ledger.release/2.
Arguments
ledgeris aAgenda.Ledger.t/0.sessionis the session's name.
Returns
{:ok, ledger}. Releasing a session that holds nothing is a no-op, so this is safe to call twice.
Examples
iex> {:ok, ledger} = Agenda.release(Agenda.ledger(), "Review")
iex> Agenda.count(ledger)
0
@spec release_series(Agenda.Ledger.t(), String.t(), keyword()) :: {:ok, Agenda.Ledger.t()} | {:error, term()}
Free everything held by a whole series. Delegates to
Agenda.Ledger.release_series/3.
Arguments
ledgeris aAgenda.Ledger.t/0.seriesis the series name — the original session's name, before it was expanded.optionsis a keyword list of options.
Options
:fromreleases only the occurrences starting at or after this point, a Tempo value or ISO 8601 string. Cancelling the rest of a term should not unpick the sessions already held.
Returns
{:ok, ledger}, or{:error, reason}when:fromcannot be read.
Examples
iex> Agenda.release_series(Agenda.ledger(), "Stand-up")
{:ok, Agenda.ledger()}
@spec resource(String.t(), keyword()) :: Agenda.Resource.t()
Build a resource. Delegates to Agenda.Resource.new/2.
Arguments
nameis the resource's name.optionsis a keyword list of options.
Options
:withinis the enclosingAgenda.Place.t/0. The default isnil.:requiresis a keyword list of attributes this resource demands of resources allocated alongside it. The default is[].:concurrencyis how many sessions may hold this resource simultaneously. The default is1.:limitsbudgets the resource over a period, as[day: 1, week: 5]— at most one shift a day and five a week. Recognised periods are:day,:weekand:month. A value may be a count, a duration (~o"PT7H36M"), or a keyword list carrying:at_mostand:at_least. This is not concurrency: concurrency is how many claims may overlap at one instant, a limit is how much falls inside a stretch of calendar however far apart. Parsed on construction, so a malformed limit raises here rather than being silently unenforced. The default is none. SeeAgenda.Limit.:avoidsis when the resource would rather not be used — a Tempo value, an ISO 8601 string, or a recurrence. Unlike:openthis is a wish, not a rule: it makes a placement worse rather than invalid, and only counts when the programme declares the:resource_wishespreference. The default is none.:prefersis the mirror — when the resource would rather be used. A placement outside it is a violation. The default is none.:openis when the resource is available at all — seeAgenda.open/2, which validates it. The default isnil, meaning the resource is never open.:buffer_beforeis turnaround needed before each claim — set-up, travel in, a room being unlocked. ATempo.Duration.t/0; the default is none.:buffer_afteris turnaround needed after each claim — cleaning, resetting the room, a machine cooling down. ATempo.Duration.t/0; the default is none.every other option is taken as an attribute.
Returns
Examples
iex> Agenda.resource("Boardroom", seats: 8).attributes
%{seats: 8}
@spec roster(atom(), [Agenda.Resource.t()]) :: Agenda.Requirement.t()
Build a requirement naming specific resources. Delegates to
Agenda.Requirement.roster/2.
Arguments
nameis the requirement's name, such as:attendees.resourcesis the list of requiredAgenda.Resource.t/0.
Returns
Examples
iex> alice = Agenda.resource("Alice")
iex> Agenda.roster(:attendees, [alice]).name
:attendees
@spec score( [Agenda.Arrangement.t()] | Agenda.Layout.t(), Agenda.Programme.t(), keyword() ) :: number()
What a layout costs against a programme's preferences.
arrange/3 already prefers a lower score, so this is for comparing
layouts you are choosing between yourself — or for reading the score
of an {:ok, arrangements} result, which returns the placements
without one.
Arguments
arrangementsis a list ofAgenda.Arrangement.t/0, or aAgenda.Layout.t/0.programmeis theAgenda.Programme.t/0whose preferences to score against.optionsis a keyword list of options.
Options
:poolis the resources the layout drew on, for preferences that consult them. The default is[].
Returns
- the total penalty, where
0is ideal.
Examples
iex> {:ok, programme} = Agenda.Programme.prefer(Agenda.programme("Conf"), :room_changes)
iex> Agenda.score([], programme)
0
@spec session(String.t(), keyword()) :: Agenda.Session.t()
Build a session. Delegates to Agenda.Session.new/2.
Arguments
nameis the session's name.optionsis a keyword list of options.
Options
:durationis how long the session runs, aTempo.Duration.t/0.:windowis the interval it must fall inside, aTempo.t/0interval.
Returns
Examples
iex> Agenda.session("Review", duration: "PT1H").name
"Review"
@spec track(String.t(), keyword()) :: Agenda.Track.t()
Build a track — sessions that cannot clash with each other.
Delegates to Agenda.Track.new/2.
Arguments
nameis the track's name.optionsis a keyword list of options.
Options
:ofis the list ofAgenda.Session.t/0in the track. The default is[].
Returns
Examples
iex> Agenda.track("Elixir", of: [Agenda.session("Keynote")]).name
"Elixir"
@spec travel_time(Agenda.Resource.t(), Agenda.Resource.t(), keyword()) :: {:ok, Tempo.Duration.t()} | {:error, :unknown}
How long it takes to get from one resource to another.
Derived from the resources' separation in the place tree — the
further up you must climb to get from one to the other, the longer
the journey. Resources in unrelated trees, or with no place at all,
return {:error, :unknown} rather than a guess: nothing can be said
about a journey between two places that are not related.
Arguments
fromandtoare each aAgenda.Resource.t/0.optionsis a keyword list of options.
Options
:betweenis a keyword-style list of{{from_name, to_name}, duration}overrides, consulted in either direction before the table. Use it wherever the building disagrees with the geometry — two adjacent rooms separated by a locked fire door, for instance.:levelsis a map of separation to duration, replacing the default table (0→PT0M,1→PT5M,2→PT10M).:distantis the duration used for separations beyond the table. The default isPT20M.
Returns
{:ok, duration}wheredurationis aTempo.Duration.t/0, or{:error, :unknown}when the two resources share no place.
Examples
iex> import Tempo.Sigils
iex> sydney = Agenda.place("Sydney Convention Centre")
iex> level_2 = Agenda.place("Level 2", within: sydney)
iex> level_3 = Agenda.place("Level 3", within: sydney)
iex> boardroom = Agenda.resource("Boardroom", within: level_2)
iex> annexe = Agenda.resource("Annexe", within: level_3)
iex> Agenda.travel_time(boardroom, annexe)
{:ok, ~o"PT5M"}
iex> here = Agenda.resource("Here")
iex> there = Agenda.resource("There")
iex> Agenda.travel_time(here, there)
{:error, :unknown}