Agenda.Resource (Agenda v0.1.0)

Copy Markdown View Source

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: true on 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 not seats. A 200-seat lecture hall is seats: 200, concurrency: 1; a bank of twenty identical lockers is seats: 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 is concurrency: 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"]]. See Agenda.Limit, which also explains why only ceilings constrain the search.

Summary

Types

t()

A named, allocatable thing.

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

t()

@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

attribute(resource, name)

@spec attribute(t(), atom()) :: term()

The value of name on resource, or nil when the resource does not carry that attribute.

Arguments

  • resource is a t/0.

  • name is 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}

fetch(pool, name)

@spec fetch([t()], String.t()) ::
  {:ok, t()} | {:error, {:unknown_resource, String.t()}}

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

  • pool is a list of t/0.

  • name is 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"}}

fetch_all(pool, names)

@spec fetch_all([t()], [String.t()]) ::
  {:ok, [t()]} | {:error, {:unknown_resources, [String.t()]}}

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

  • pool is a list of t/0.

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

new(name, options \\ [])

@spec new(
  String.t(),
  keyword()
) :: t()

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

  • name is the resource's name.

Options

  • :within is the enclosing Agenda.Place.t/0. The default is nil.

  • :requires is a keyword list of attributes this resource demands of resources allocated alongside it. The default is [].

  • :concurrency is how many sessions may hold this resource simultaneously. The default is 1.

  • :limits budgets the resource over a period, as [day: 1, week: 5] — at most one shift a day and five a week. Recognised periods are :day, :week and :month. A value may be a count, a duration (~o"PT7H36M"), or a keyword list carrying :at_most and :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. See Agenda.Limit.

  • :avoids is when the resource would rather not be used — a Tempo value, an ISO 8601 string, or a recurrence. Unlike :open this is a wish, not a rule: it makes a placement worse rather than invalid, and only counts when the programme declares the :resource_wishes preference. The default is none.

  • :prefers is the mirror — when the resource would rather be used. A placement outside it is a violation. The default is none.

  • :open is when the resource is available at all — see Agenda.open/2, which validates it. The default is nil, meaning the resource is never open.

  • :buffer_before is turnaround needed before each claim — set-up, travel in, a room being unlocked. A Tempo.Duration.t/0; the default is none.

  • :buffer_after is turnaround needed after each claim — cleaning, resetting the room, a machine cooling down. A Tempo.Duration.t/0; the default is none.

  • every other option is taken as an attribute.

Returns

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}

separation(resource1, resource2)

@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

  • a and b are each a t/0.

Returns

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

within?(resource, place)

@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

Returns

  • true or false.

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