# `Agenda.Programme`
[🔗](https://github.com/elixir-tempo/agenda/blob/v0.3.0/lib/agenda/programme.ex#L1)

Everything being laid out at once — the tracks, the standalone
sessions, and the span they all fall inside.

Where `Agenda.Planner.plan/3` answers *"when could this one
session be held?"*, a programme asks the harder question: *"is there
a placement for **every** session such that nothing clashes?"* Those
are different problems. The first enumerates; the second searches,
and a choice made for one session forecloses choices for another.

The word is deliberately not "schedule" — that is triple-booked
already (`Tempo.Schedule` is critical-path planning,
elsewhere a `Schedule` is the bookable resource itself, and
colloquially it is this output).

`new/2` builds one and `across/2` sets the window every session falls
inside. `add_track/2`, `add_session/2` and `add_sessions/2` add what
is to be laid out; `precede/4` and `prefer/3` add hard and soft
constraints, and `interest/3` and `meetings/3` turn interest between
resources into sessions. `all_sessions/1`, `track_of/2` and
`restrict_to/2` read it back.

# `t`

```elixir
@type t() :: %Agenda.Programme{
  interests: [Agenda.Interest.t()],
  name: String.t(),
  precedences: [Agenda.Precedence.t()],
  preferences: [Agenda.Preference.t()],
  sessions: [Agenda.Session.t()],
  tracks: [Agenda.Track.t()],
  window: Agenda.Availability.pattern() | nil
}
```

A whole layout waiting to be arranged.

# `across`

```elixir
@spec across(t(), Agenda.Availability.pattern()) :: t()
```

Set the window every session must fall inside.

### Arguments

* `programme` is a `t:t/0`.

* `window` is a Tempo value or an ISO 8601 string.

### Returns

* the programme, bounded.

### Examples

    iex> programme = Agenda.Programme.new("Conf")
    iex> Agenda.Programme.across(programme, "2026-09-15/2026-09-16").window
    "2026-09-15/2026-09-16"

# `add_session`

```elixir
@spec add_session(t(), Agenda.Session.t()) :: t()
```

Add a session that belongs to no track.

### Arguments

* `programme` is a `t:t/0`.

* `session` is a `t:Agenda.Session.t/0`.

### Returns

* the programme, with the session added.

### Examples

    iex> programme = Agenda.Programme.new("Conf")
    iex> Agenda.Programme.add_session(programme, Agenda.session("Registration"))
    ...> |> Map.get(:sessions)
    ...> |> Enum.map(& &1.name)
    ["Registration"]

# `add_sessions`

```elixir
@spec add_sessions(t(), [Agenda.Session.t()]) :: t()
```

Add every session in a list.

A programme is usually built from a list of sessions rather than one
at a time, and folding `add_session/2` over that list means writing
the fold — with the arguments the other way round from the reduce —
at every such site.

### Arguments

* `programme` is a `t:t/0`.

* `sessions` is a list of `t:Agenda.Session.t/0`, added in order.

### Returns

* the programme, with the sessions added.

### Examples

    iex> programme = Agenda.Programme.new("Conf")
    iex> sessions = [Agenda.session("Registration"), Agenda.session("Keynote")]
    iex> Agenda.Programme.add_sessions(programme, sessions)
    ...> |> Map.get(:sessions)
    ...> |> Enum.map(& &1.name)
    ["Registration", "Keynote"]

# `add_track`

```elixir
@spec add_track(t(), Agenda.Track.t()) :: t()
```

Add a track.

### Arguments

* `programme` is a `t:t/0`.

* `track` is a `t:Agenda.Track.t/0`.

### Returns

* the programme, with the track added.

### Examples

    iex> programme = Agenda.Programme.new("Conf")
    iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
    iex> Agenda.Programme.add_track(programme, track).tracks
    ...> |> Enum.map(& &1.name)
    ["Elixir"]

# `all_sessions`

```elixir
@spec all_sessions(t()) :: [Agenda.Session.t()]
```

Every session in the programme, tracked or not.

### Arguments

* `programme` is a `t:t/0`.

### Returns

* the sessions, standalone ones first, then each track's in order.

### Examples

    iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
    iex> Agenda.programme("Conf")
    ...> |> Agenda.Programme.add_session(Agenda.session("Registration"))
    ...> |> Agenda.Programme.add_track(track)
    ...> |> Agenda.Programme.all_sessions()
    ...> |> Enum.map(& &1.name)
    ["Registration", "Keynote"]

# `interest`

```elixir
@spec interest(
  t(),
  Agenda.Resource.t() | String.t(),
  Agenda.Resource.t() | String.t()
) ::
  {:ok, t()} | {:error, term()}
```

Register that one resource would like a session with another.

Interest is stated in one direction at a time. A meeting is held
where it is returned — see `meetings/3` — so both parties must say
so, and interest that is never returned is reported rather than
scheduled.

### Arguments

* `programme` is a `t:t/0`.

* `from` is the resource registering the interest, as a
  `t:Agenda.Resource.t/0` or its name.

* `to` is 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> programme = Agenda.programme("Trade Show")
    iex> {:ok, programme} = Agenda.Programme.interest(programme, "Kim", "Harbour Tours")
    iex> length(programme.interests)
    1

# `meetings`

```elixir
@spec meetings(t(), [Agenda.Resource.t()], keyword()) :: {:ok, t()} | {:error, term()}
```

Turn returned interest into sessions.

One session per mutually interested pair, each rostering both
parties, so the constraint that nobody is in two places at once is
the one the library already enforces rather than a rule anybody
writes. Interest that was never returned adds nothing and is left
for `Agenda.Interest.one_sided/1` to report.

Adding meetings twice would double them, so this is a step that
produces a programme rather than something `arrange/3` does on the
way past.

### Arguments

* `programme` is a `t:t/0` carrying the interests.

* `pool` is the resources the interests name.

* `options` is a keyword list of options.

### Options

* `:duration` is how long each meeting runs. Required.

* `:window` is when meetings may be held. Defaults to the
  programme's own window.

* `:needs` is what every meeting requires beyond its two parties, as
  `[name: predicates]` — a table, most often.

* `:as` is the requirement both parties are rostered under. The default is
  `:party`, which says neither of them is substitutable.

* `:name` is 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 Tours")
    iex> programme = Agenda.programme("Trade Show", across: "2027-06-15/2027-06-16")
    iex> {:ok, programme} = Agenda.Programme.interest(programme, kim, harbour)
    iex> {:ok, programme} = Agenda.Programme.interest(programme, harbour, kim)
    iex> {:ok, programme} = Agenda.Programme.meetings(programme, [kim, harbour], duration: "PT15M")
    iex> Enum.map(programme.sessions, & &1.name)
    ["Harbour Tours with Kim"]

# `new`

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

Build a programme.

### Arguments

* `name` is the programme's name.

* `options` is a keyword list of options.

### Options

* `:across` is the window every session must fall inside.

### Returns

* a `t:t/0`.

### Examples

    iex> Agenda.Programme.new("ElixirConf AU").name
    "ElixirConf AU"

# `precede`

```elixir
@spec precede(t(), String.t(), String.t(), keyword()) :: {:ok, t()} | {:error, term()}
```

Require that one session finishes before another starts.

This is what makes a task graph out of a set of tasks. Both names
must be sessions in the programme; naming one that is not is an
error rather than a constraint silently doing nothing.

### Arguments

* `programme` is a `t:t/0`.

* `first` is the name of the session that must finish first.

* `then` is the name of the session that follows it.

* `options` is a keyword list of options.

### Options

* `:gap` is the least time that must pass between them.

* `:within` is the most, measured from the end of `first`.

### 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.Programme.precede(programme, "Survey", "Quote", gap: "PT30M")
    iex> Enum.map(programme.precedences, & &1.then)
    ["Quote"]

    iex> Agenda.Programme.precede(Agenda.programme("Job"), "Survey", "Quote")
    {:error, {:unknown_sessions, ["Survey", "Quote"]}}

# `prefer`

```elixir
@spec prefer(t(), atom() | {atom(), function()}, keyword()) ::
  {:ok, t()} | {:error, term()}
```

Add a soft constraint.

A preference never makes a layout invalid, only worse.
`Agenda.Arranger.arrange/3` places as many sessions as it can
first — that part is proven — and prefers a lower score only among
the layouts it reaches. See `Agenda.Preference` for what is and
is not promised.

Declaring even one preference changes how the search runs: it can no
longer stop at the first layout that places everything it can, since
a later one may score better. A programme with no preferences is
unaffected.

### Arguments

* `programme` is a `t:t/0`.

* `preference` is a built-in name — `:room_changes` or
  `:room_spread` — or a `{name, function}` pair of your own.

* `options` is a keyword list of options.

### Options

* `:weight` is how much each violation costs. The default is `1`.

### Returns

* `{:ok, programme}`, or

* `{:error, reason}` when the preference is not recognised.

### Examples

    iex> programme = Agenda.programme("Conf")
    iex> {:ok, programme} = Agenda.Programme.prefer(programme, :room_changes, weight: 10)
    iex> Enum.map(programme.preferences, & &1.name)
    [:room_changes]

    iex> Agenda.Programme.prefer(Agenda.programme("Conf"), :teleportation)
    {:error, {:unknown_preference, :teleportation}}

# `restrict_to`

```elixir
@spec restrict_to(t(), [String.t()]) :: t()
```

The same programme cut down to the named sessions.

Tracks keep only the sessions named, and a track left with none is
dropped rather than kept empty — an empty track constrains nothing
and would only be noise in the result.

This exists for `Agenda.Arranger.conflict/3`, which asks whether
smaller and smaller parts of a programme can be arranged in order to
find the part that cannot.

### Arguments

* `programme` is a `t:t/0`.

* `names` is the list of session names to keep.

### Returns

* the programme, holding only those sessions.

### Examples

    iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
    iex> Agenda.programme("Conf")
    ...> |> Agenda.Programme.add_session(Agenda.session("Registration"))
    ...> |> Agenda.Programme.add_track(track)
    ...> |> Agenda.Programme.restrict_to(["Registration"])
    ...> |> Agenda.Programme.all_sessions()
    ...> |> Enum.map(& &1.name)
    ["Registration"]

    iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
    iex> Agenda.programme("Conf")
    ...> |> Agenda.Programme.add_track(track)
    ...> |> Agenda.Programme.restrict_to([])
    ...> |> Map.get(:tracks)
    []

# `track_of`

```elixir
@spec track_of(t(), String.t()) :: Agenda.Track.t() | nil
```

The track a session belongs to, or `nil` when it stands alone.

### Arguments

* `programme` is a `t:t/0`.

* `session_name` is the session's name.

### Returns

* a `t:Agenda.Track.t/0`, or `nil`.

### Examples

    iex> track = Agenda.track("Elixir", of: [Agenda.session("Keynote")])
    iex> programme = Agenda.Programme.add_track(Agenda.programme("Conf"), track)
    iex> Agenda.Programme.track_of(programme, "Keynote").name
    "Elixir"

    iex> Agenda.Programme.track_of(Agenda.programme("Conf"), "Keynote")
    nil

---

*Consult [api-reference.md](api-reference.md) for complete listing*
