# `Tempo.IntervalSet`
[🔗](https://github.com/kipcole9/tempo/blob/v0.20.0/lib/tempo/interval_set.ex#L1)

A sorted, non-overlapping, coalesced list of `t:Tempo.Interval.t/0`
values — the multi-interval counterpart to `Tempo.Interval`.

`IntervalSet` is the operational form for set operations. Every
AST shape that expands to a disjoint list of bounded spans
(non-contiguous masks, stepped ranges, iterated groups, bounded
recurrences, all-of sets) materialises to an `IntervalSet` via
`Tempo.to_interval/1`.

## Invariants

The constructor `new/1` guarantees:

* Intervals are sorted ascending by `from`.

* Adjacent or overlapping intervals are coalesced. Half-open
  semantics means `[a, b) ++ [b, c) == [a, c)` — the coalesce
  pass merges both overlap and touch cases.

* No `:undefined` endpoints. (Open-ended intervals cannot
  participate in a set; the caller must bound them first.)

## Counting

A set answers three different "how much" questions, and they return
very different numbers — reach for the one that matches intent:

* **How many windows** (member spans)? — `count/1`, the set's
  cardinality.

* **How many sub-points** (days, hours, … at the members'
  resolution)? — `Enum.count/1`. The `Enumerable` protocol walks
  *into* each member and yields its stepped points, so this totals
  across every window, not the number of windows.

* **How long** (elapsed time)? — `Tempo.duration/1` on the whole
  set (the members' total) or on a member, or `Tempo.at_least?/2`
  to keep only windows of a given length. Never
  count sub-points for this: across a DST boundary the walk skips the
  spring-forward hour and emits the fall-back hour twice, so
  `Enum.count` deliberately diverges from elapsed time.

A two-window set — January and March — is `2` windows but `62`
sub-points (31 + 31 days):

    iex> {:ok, free} = Tempo.union(~o"2026Y1M", ~o"2026Y3M")
    iex> Tempo.IntervalSet.count(free)
    2
    iex> Enum.count(free)
    62

## Timezone handling

An IntervalSet preserves the wall-clock + zone form of its member
intervals on the struct. Any set operation that needs to compare
endpoints across zones derives a UTC projection on demand; no
UTC cache is stored on the struct. This keeps results stable when
`Tzdata` updates — re-running the operation simply uses whatever
zone rules are current at the time of the call.

See `guides/enumeration-semantics.md` for the full discussion of
wall-clock-vs-UTC authority.

# `t`

```elixir
@type t() :: %Tempo.IntervalSet{intervals: [Tempo.Interval.t()], metadata: map()}
```

# `coalesce`

```elixir
@spec coalesce(t()) :: t()
```

Merge touching or overlapping member intervals into larger
spans, returning a new `t:t/0` in **canonical instant-set
form**.

`IntervalSet` preserves member identity by default — each
interval stays a distinct member with its own metadata. That
shape is right for event management, bookings, and any query
that asks about individual members.

Some questions are about the *instants covered* by the set,
not the members: "is this point covered?", "what's the total
duration?", "are these two schedules equivalent?". For those,
the canonical instant-set form is the right shape — two
touching intervals merge into one, and the set has exactly
one member per contiguous covered region.

Under the half-open `[from, to)` convention, intervals merge
when the later one's `from` is at or before the earlier one's
`to`. Touching (`[a, b) ++ [b, c) == [a, c)`) and overlapping
cases both merge.

### Metadata

When two members merge, the earlier member's metadata is kept
on the merged span and the later member's is dropped. If
metadata matters for your query, filter or project before
coalescing.

### Arguments

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

### Returns

* A `t:t/0` with touching and overlapping intervals merged.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"},
    ...>   %Tempo.Interval{from: ~o"2026-06-16", to: ~o"2026-06-17"}
    ...> ])
    iex> Tempo.IntervalSet.count(set)
    2
    iex> coalesced = Tempo.IntervalSet.coalesce(set)
    iex> Tempo.IntervalSet.count(coalesced)
    1

# `count`

```elixir
@spec count(t()) :: non_neg_integer()
```

Return the number of member intervals in the set.

A named helper so callers never have to write
`length(set.intervals)` or `length(to_list(set))` in
user-facing code.

### Arguments

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

### Returns

* The count of member intervals as a non-negative integer.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-01", to: ~o"2026-06-10"},
    ...>   %Tempo.Interval{from: ~o"2026-07-01", to: ~o"2026-07-10"}
    ...> ])
    iex> Tempo.IntervalSet.count(set)
    2

# `covered?`

```elixir
@spec covered?(t(), Tempo.t()) :: boolean()
```

`true` when any member interval of `set` covers `point`.

Coalesces internally — a point is "covered" iff it falls inside
at least one member span. For the common booking/scheduling
question "is this slot occupied?", this is the right predicate.

### Arguments

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

* `point` is any `t:Tempo.t/0`.

### Returns

* `true` or `false`.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-20"}
    ...> ])
    iex> Tempo.IntervalSet.covered?(set, ~o"2026-06-17")
    true
    iex> Tempo.IntervalSet.covered?(set, ~o"2026-06-25")
    false

# `duration`

```elixir
@spec duration(t()) :: Tempo.Duration.t()
```

Total covered duration of the set — the sum of every member's
length.

Each member's length is measured on the UTC time line (the same
measurement as `Tempo.Interval.duration/1`), so DST transitions
inside a member are accounted for. Members are disjoint by
construction when produced by the set operations; if the set was
built with overlapping members deliberately preserved, the overlap
is counted once per member.

### Arguments

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

### Returns

* A `t:Tempo.Duration.t/0` holding the total in seconds. The empty
  set has a duration of zero seconds.

### Examples

    iex> {:ok, free} = Tempo.union(~o"2026-06-01T09/2026-06-01T12", ~o"2026-06-01T14/2026-06-01T17")
    iex> Tempo.IntervalSet.duration(free)
    ~o"PT21600S"

# `filter`

```elixir
@spec filter(t(), (Tempo.Interval.t() -&gt; as_boolean(any()))) :: t()
```

Keep only the member intervals for which `fun` returns `true`,
returning a new `t:t/0`.

### Arguments

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

* `fun` is a 1-arity predicate applied to each member
  `t:Tempo.Interval.t/0`.

### Returns

* A new `t:t/0` containing only the members where `fun`
  returned a truthy value. The input's invariants (sorted,
  coalesced) are preserved — filtering cannot create overlap.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"},
    ...>   %Tempo.Interval{from: ~o"2026-06-20", to: ~o"2026-06-25"}
    ...> ])
    iex> long = Tempo.IntervalSet.filter(set, &Tempo.at_least?(&1, ~o"P2D"))
    iex> Tempo.IntervalSet.count(long)
    1

# `map`

```elixir
@spec map(t(), (Tempo.Interval.t() -&gt; any())) :: [any()]
```

Apply `fun` to each member interval and return the results as
a plain list.

Unlike the `Enumerable` protocol for `IntervalSet` — which
walks each sub-point inside every interval at the next-finer
resolution — `map/2` operates on the **member intervals
themselves**. It's the set-as-sequence-of-spans view.

The result is a plain list, not an IntervalSet, because the
mapper may return anything (integers, tuples, arbitrary values).

### Arguments

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

* `fun` is a 1-arity function applied to each member
  `t:Tempo.Interval.t/0`.

### Returns

* A list of whatever `fun` returns, in the set's sort order.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"},
    ...>   %Tempo.Interval{from: ~o"2026-06-20", to: ~o"2026-06-21"}
    ...> ])
    iex> Tempo.IntervalSet.map(set, &Tempo.day/1)
    [15, 20]

# `new`

```elixir
@spec new(
  [Tempo.Interval.t()],
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Construct a `t:t/0` from a list of intervals.

The input list is sorted ascending by `from` endpoint and
coalesced — adjacent or overlapping intervals are merged under
the half-open `[from, to)` convention.

### Arguments

* `intervals` is a list of `t:Tempo.Interval.t/0` values. Open-
  ended intervals (`from: :undefined` or `to: :undefined`) are
  rejected.

### Returns

* `{:ok, interval_set}` where `interval_set` is a `t:t/0`, or

* `{:error, reason}` when an input interval is open-ended or
  otherwise cannot participate in a set.

### Examples

    iex> {:ok, a} = Tempo.to_interval(~o"2022Y1M")
    iex> {:ok, b} = Tempo.to_interval(~o"2022Y3M")
    iex> {:ok, set} = Tempo.IntervalSet.new([b, a])
    iex> length(set.intervals)
    2
    iex> hd(set.intervals).from.time
    [year: 2022, month: 1, day: 1]

# `new!`

```elixir
@spec new!(
  [Tempo.Interval.t()],
  keyword()
) :: t()
```

Raising version of `new/1`.

# `relation_matrix`

```elixir
@spec relation_matrix(
  t() | Tempo.Interval.t() | Tempo.t(),
  t() | Tempo.Interval.t() | Tempo.t()
) ::
  [
    {non_neg_integer(), non_neg_integer(),
     Tempo.Interval.relation() | {:error, term()}}
  ]
  | {:error, term()}
```

Build the Allen-relation matrix between every member of `a`
and every member of `b`.

Allen's algebra is defined on pairs of intervals, not sets —
two multi-member sets can relate several different ways
simultaneously. `relation_matrix/2` returns the complete
per-pair classification so you can reason about mixed
conflicts, merge logic, or scheduling visualisations.

### Arguments

* `a` and `b` are `t:t/0` (single intervals and Tempo points
  are coerced to single-member sets for convenience).

### Returns

* `[{a_index, b_index, relation}]` — one tuple per pair.
  Indexes are 0-based into each set's `.intervals` list. The
  relation is one of `t:Tempo.Interval.relation/0`.

* `{:error, reason}` when either input can't be reduced to an
  IntervalSet of bounded intervals.

### Examples

    iex> a = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-01", to: ~o"2026-06-03"},
    ...>   %Tempo.Interval{from: ~o"2026-06-05", to: ~o"2026-06-07"}
    ...> ], coalesce: false)
    iex> b = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-04", to: ~o"2026-06-06"}
    ...> ], coalesce: false)
    iex> Tempo.IntervalSet.relation_matrix(a, b)
    [{0, 0, :precedes}, {1, 0, :overlapped_by}]

# `slots`

```elixir
@spec slots(t() | Tempo.Interval.t(), Tempo.Duration.t(), keyword()) :: t()
```

Cut each member into consecutive fixed-length bookable slots.

Turns a region of free time into the discrete slots you could actually
book within it. Within each member interval, slots of length
`duration` are emitted starting at the member's start and spaced by
`:every` (a slot every `duration` by default — back-to-back,
non-overlapping); a slot is kept only if it fits entirely inside the
member. The members stay distinct (the result is *not* coalesced).

This is the slicing counterpart to the set operations: where
`difference`/`intersection` give you the free *regions*,
`slots/3` cuts those regions into the fixed-length windows a
booking UI offers.

### Arguments

* `set` is a `t:t/0` or a single `t:Tempo.Interval.t/0` (as returned
  by a set operation on a connected result).

* `duration` is the slot length, a `t:Tempo.Duration.t/0`.

### Options

* `:every` is the spacing between slot starts, a
  `t:Tempo.Duration.t/0`. Defaults to `duration` (adjacent slots).
  A smaller value yields overlapping slots; a larger value leaves
  gaps. Must be positive.

### Returns

* a `t:t/0` of the fitting slots, in order.

### Examples

    iex> work = Tempo.Interval.new!(from: ~o"2026-06-15T09:00:00", to: ~o"2026-06-15T12:00:00")
    iex> work |> Tempo.IntervalSet.slots(~o"PT1H") |> Tempo.IntervalSet.count()
    3

    iex> work = Tempo.Interval.new!(from: ~o"2026-06-15T09:00:00", to: ~o"2026-06-15T12:00:00")
    iex> work |> Tempo.IntervalSet.slots(~o"PT1H", every: ~o"PT30M") |> Tempo.IntervalSet.count()
    5

# `to_list`

```elixir
@spec to_list(t()) :: [Tempo.Interval.t()]
```

Return the member intervals as a plain list.

The `Enumerable` protocol implementation for an IntervalSet
walks every sub-point inside each interval (consistent with
the Tempo and Tempo.Interval `Enumerable` implementations —
every Tempo value is a span, iteration walks its sub-points at
the next-finer resolution).

When you want to operate on the **member intervals** instead
— filter them, count them, map them — `to_list/1` gives you
a plain list you can pipe into `Enum`.

### Examples

    iex> {:ok, set} = Tempo.IntervalSet.new([
    ...>   %Tempo.Interval{from: ~o"2026-06-01", to: ~o"2026-06-10"},
    ...>   %Tempo.Interval{from: ~o"2026-07-01", to: ~o"2026-07-10"}
    ...> ])
    iex> set |> Tempo.IntervalSet.to_list() |> length()
    2

Pair with the interval predicates for expressive scheduling:

    set
    |> Tempo.IntervalSet.to_list()
    |> Enum.filter(&Tempo.at_least?(&1, ~o"PT1H"))

# `total_duration`

```elixir
@spec total_duration(t()) :: Tempo.Duration.t()
```

Total duration covered by the set's members, as a
`t:Tempo.Duration.t/0`.

Coalesces internally so overlapping members are not
double-counted — the returned duration is the length of the
union of covered instants, not the sum of individual member
durations. For the "sum of member durations" semantics, use
`map(set, &Tempo.Interval.duration/1) |> Enum.sum()` with
explicit arithmetic.

### Arguments

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

### Returns

* A `t:Tempo.Duration.t/0`.

### Examples

    iex> set = Tempo.IntervalSet.new!([
    ...>   %Tempo.Interval{from: ~o"2026-06-15T09:00:00", to: ~o"2026-06-15T10:00:00"},
    ...>   %Tempo.Interval{from: ~o"2026-06-15T11:00:00", to: ~o"2026-06-15T12:00:00"}
    ...> ])
    iex> Tempo.IntervalSet.total_duration(set)
    ~o"PT7200S"

---

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