A resource — a named thing that can be allocated to a session.
People and rooms are the same kind of thing here; only their
attributes differ. A room has seats and video_conferencing, a
person has skills and access needs, and both sit somewhere in the
Agenda.Place tree.
Two fields carry more weight than they appear to:
requires— attributes this resource demands of whatever it is allocated alongside.step_free_access: trueon a person is not a fact about their availability; it is a constraint they impose on the room. Modelling it here means accessibility cannot be forgotten at the call site.concurrency— how many sessions may hold this resource at once. This is notseats. A 200-seat lecture hall isseats: 200, concurrency: 1; a bank of twenty identical lockers isseats: 1, concurrency: 20. Conflating the two is what lets a hall accept two simultaneous lectures.limits— how much it may be claimed over a period, as[day: 1, week: 5]. Concurrency asks how many claims may overlap at an instant; a limit asks how much falls inside a stretch of calendar, however far apart. A nurse who may work one shift a day and five a week isconcurrency: 1, limits: [day: 1, week: 5]— the concurrency stops two at once, the limits make it a contract.A limit may measure time instead of claims, and may set a floor as well as a ceiling —
limits: [day: ~o"PT7H36M", week: [at_least: ~o"PT38H"]]. SeeAgenda.Limit, which also explains why only ceilings constrain the search.
Summary
Functions
The value of name on resource, or nil when the resource does
not carry that attribute.
The resource in pool with this name.
Every resource in pool named by names, in the order given.
Build a resource.
How far apart two resources are in the place tree, in levels.
true when resource sits inside place, at any depth.
Types
@type t() :: %Agenda.Resource{ attributes: %{optional(atom()) => term()}, avoids: term(), buffer_after: Tempo.Duration.t() | nil, buffer_before: Tempo.Duration.t() | nil, concurrency: pos_integer(), limits: [Agenda.Limit.t()], name: String.t(), open: Tempo.Interval.t() | Tempo.IntervalSet.t() | nil, prefers: term(), requires: %{optional(atom()) => term()}, within: Agenda.Place.t() | nil }
A named, allocatable thing.
Functions
The value of name on resource, or nil when the resource does
not carry that attribute.
Arguments
resourceis at/0.nameis the attribute name.
Returns
- the attribute value, or
nil.
Examples
iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8)
iex> {Agenda.Resource.attribute(boardroom, :seats),
...> Agenda.Resource.attribute(boardroom, :projector)}
{8, nil}
The resource in pool with this name.
A resource's name is its identity: the arranger, the ledger and the
fixpoint bridge all decide "is this the same resource?" by comparing
names. Looking one up is therefore an ordinary thing to want, and
doing it by hand — Enum.find/2, or a map built at the call site —
is how a mistyped name becomes a silently missing resource instead
of an error.
Arguments
poolis a list oft/0.nameis the resource name to find.
Returns
{:ok, resource}; or{:error, {:unknown_resource, name}}.
Examples
iex> pool = [Agenda.Resource.new("Boardroom"), Agenda.Resource.new("Annexe")]
iex> {:ok, resource} = Agenda.Resource.fetch(pool, "Annexe")
iex> resource.name
"Annexe"
iex> pool = [Agenda.Resource.new("Boardroom")]
iex> Agenda.Resource.fetch(pool, "Baordroom")
{:error, {:unknown_resource, "Baordroom"}}
Every resource in pool named by names, in the order given.
All or nothing: an unknown name fails the call rather than returning a shorter list. A roster that quietly loses a member is the failure this exists to prevent — the session still runs, without the person it named. Every unknown name is reported, not just the first.
Arguments
poolis a list oft/0.namesis a list of resource names.
Returns
{:ok, resources}in the order the names were given; or{:error, {:unknown_resources, names}}listing every name that is not in the pool, in the order given.
Examples
iex> pool = [Agenda.Resource.new("Ann"), Agenda.Resource.new("Bo")]
iex> {:ok, resources} = Agenda.Resource.fetch_all(pool, ["Bo", "Ann"])
iex> Enum.map(resources, & &1.name)
["Bo", "Ann"]
iex> pool = [Agenda.Resource.new("Ann")]
iex> Agenda.Resource.fetch_all(pool, ["Ann", "Bo", "Cee"])
{:error, {:unknown_resources, ["Bo", "Cee"]}}
Build a resource.
Any option that is not reserved becomes an attribute, so attributes
read as themselves at the call site rather than being nested inside
an attributes: keyword.
Arguments
nameis the resource's name.
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
- a
t/0.
Examples
iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8, video_conferencing: true)
iex> boardroom.attributes
%{seats: 8, video_conferencing: true}
iex> alice = Agenda.Resource.new("Alice", requires: [step_free_access: true])
iex> alice.requires
%{step_free_access: true}
@spec separation(t(), t()) :: Agenda.Place.separation()
How far apart two resources are in the place tree, in levels.
Delegates to Agenda.Place.separation/2; a resource with no
place is :disjoint from everything, itself included, because
nothing can be said about the journey.
Arguments
aandbare each at/0.
Returns
0, a positive integer, or:disjoint— seeAgenda.Place.separation/2.
Examples
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> level_2 = Agenda.Place.new("Level 2", within: sydney)
iex> level_3 = Agenda.Place.new("Level 3", within: sydney)
iex> boardroom = Agenda.Resource.new("Boardroom", within: level_2)
iex> annexe = Agenda.Resource.new("Annexe", within: level_3)
iex> Agenda.Resource.separation(boardroom, annexe)
1
@spec within?(t(), Agenda.Place.t()) :: boolean()
true when resource sits inside place, at any depth.
A resource with no place is inside nothing.
Arguments
resourceis at/0.placeis aAgenda.Place.t/0.
Returns
trueorfalse.
Examples
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> level_2 = Agenda.Place.new("Level 2", within: sydney)
iex> boardroom = Agenda.Resource.new("Boardroom", within: level_2)
iex> Agenda.Resource.within?(boardroom, sydney)
true
iex> sydney = Agenda.Place.new("Sydney Convention Centre")
iex> nowhere = Agenda.Resource.new("Nowhere")
iex> Agenda.Resource.within?(nowhere, sydney)
false