# `Tempo.Operations`
[🔗](https://github.com/kipcole9/tempo/blob/v0.5.0/lib/operations.ex#L1)

Set operations on Tempo values — union, intersection,
complement, difference, symmetric difference — plus the
companion predicates (`disjoint?/2`, `overlaps?/2`,
`subset?/2`, `contains?/2`, `equal?/2`).

Every operation accepts any Tempo value (implicit `%Tempo{}`,
`%Tempo.Interval{}`, `%Tempo.IntervalSet{}`, or all-of
`%Tempo.Set{}`) and routes through `align/2,3` — a single
preflight that normalises operands to a common anchor class,
resolution, calendar, and (where relevant) UTC reference frame.
Set-op results are always `%Tempo.IntervalSet{}`; predicate
results are booleans.

See `plans/set-operations.md` for the design rationale
including:

* why IntervalSet (not rule-algebra) is the operational form,
* how timezones and DST are handled,
* why the `:bound` option is required for some operand
  combinations,
* and the axis-compatibility rule (anchored vs non-anchored).

The top-level user API lives on `Tempo` via delegation — callers
should prefer `Tempo.union/2`, `Tempo.intersection/2`, etc. over
calling `Tempo.Operations` directly.

# `align`

```elixir
@spec align(operand, operand, keyword()) ::
  {:ok, {Tempo.IntervalSet.t(), Tempo.IntervalSet.t()}} | {:error, term()}
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

Normalise two operands to the same anchor class, resolution,
and calendar, and return them both as `%Tempo.IntervalSet{}`.

### Arguments

* `a` and `b` are any Tempo values that can be materialised to
  an interval set — `%Tempo{}`, `%Tempo.Interval{}`,
  `%Tempo.IntervalSet{}`, or `%Tempo.Set{type: :all}`.

### Options

* `:bound` — a Tempo value (any of the above types) that
  bounds non-anchored or otherwise unbounded operands. Required
  when `a` and `b` belong to different anchor classes.

### Returns

* `{:ok, {aligned_a, aligned_b}}` where both are `%Tempo.IntervalSet{}`.

* `{:error, reason}` when a preflight check fails (duration
  operand, one-of set operand, incompatible anchor classes
  without `:bound`, calendar mismatch, etc.).

# `complement`

```elixir
@spec complement(
  any(),
  keyword()
) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Complement of `set` within `bound` — the instants in `bound`
that are NOT covered by any member of `set`.

Unlike `difference/3` (which is member-preserving),
`complement/2` returns the **instant-set** form: one member
per gap in the covered region. This is the right semantics
for "find all free time in the workday" style queries.

The `:bound` option is required — an unbounded complement is
infinite, and Tempo refuses to pick a universe implicitly.

### Options

* `:bound` — the universe to complement within. Any Tempo
  value. Required.

# `contains?`

```elixir
@spec contains?(operand, operand, keyword()) :: boolean()
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

`true` when every instant covered by `b` is also covered by
`a`. Alias for `subset?(b, a, opts)`.

# `difference`

```elixir
@spec difference(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Difference `a \ b` — every instant in `a` that is NOT in `b`,
returned as one or more trimmed intervals.

Each member of `a` is trimmed to its portions that don't
overlap any member of `b`. A single `a` member can split into
multiple fragments if `b` covers only its middle. Each emitted
fragment carries the source `a` member's metadata.

This is the canonical set-theoretic difference: `A ∖ B`. Use
it when the question is about *covered time* — "the parts of
the workday that aren't lunch", "free time around a busy
schedule".

For the member-preserving filter (keep whole `a` members that
don't overlap any `b` member, drop the rest), use
`members_outside/3`.

# `disjoint?`

```elixir
@spec disjoint?(operand, operand, keyword()) :: boolean()
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

`true` when `a` and `b` share no instants — no member of `a`
overlaps any member of `b`.

# `equal?`

```elixir
@spec equal?(operand, operand, keyword()) :: boolean()
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

`true` when `a` and `b` cover the same instants — i.e. they
are mutual subsets at the instant-set level. Member identity
and metadata are ignored; only the covered instants matter.

# `intersection`

```elixir
@spec intersection(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Intersection of two operands — every instant present in both
operands, returned as one or more trimmed intervals.

Each result interval is the portion of an `a` member trimmed
to its overlap with some `b` member. Members of `a` can be
split into multiple fragments if `b` covers only part of them.
Each emitted fragment carries the source `a` member's metadata.

This is the canonical set-theoretic intersection: `A ∩ B`.
Use it when the question is about *covered time* — "the parts
of my meetings that fall inside business hours", "the overlap
between two date ranges".

For the member-preserving filter (return whole `a` members
that overlap any `b` member, untrimmed), use
`members_overlapping/3`.

# `members_in_exactly_one`

```elixir
@spec members_in_exactly_one(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Member-preserving symmetric-difference filter — the members
of either operand that do NOT overlap any member of the
other, kept whole with their original metadata. Derived as
`members_outside(a, b) ∪ members_outside(b, a)`.

This is the "which events appear on exactly one calendar?"
query. For the canonical instant-level form, use
`symmetric_difference/3`.

# `members_outside`

```elixir
@spec members_outside(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Member-preserving anti-overlap filter — the **members of `a`**
that do NOT overlap any member of `b`, kept whole with their
original metadata.

This is the "which workdays aren't holidays?" query. A member
of `a` is dropped entirely if any member of `b` overlaps it,
even partially.

For the canonical instant-level difference (trim each member
of `a` to its non-overlapping portion of `b`, splitting if
necessary), use `difference/3`.

# `members_overlapping`

```elixir
@spec members_overlapping(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Member-preserving overlap filter — the **members of `a`** that
overlap any member of `b`, kept as distinct intervals with
their original metadata.

This is the "which of these bookings hit the query window?"
query. Each surviving member is an entire member of `a` — not
a trimmed portion.

For the canonical instant-level intersection (each survivor
trimmed to its overlap with `b`), use `intersection/3`.

# `overlaps?`

```elixir
@spec overlaps?(operand, operand, keyword()) :: boolean()
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

`true` when `a` and `b` share at least one instant.

# `subset?`

```elixir
@spec subset?(operand, operand, keyword()) :: boolean()
when operand:
       Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t()
```

`true` when every instant covered by `a` is also covered by
`b`. Operates at the instant-set level (both operands
coalesced internally) — not member-by-member.

# `symmetric_difference`

```elixir
@spec symmetric_difference(any(), any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Symmetric difference `a △ b` — every instant in exactly one
of the operands, returned as trimmed intervals. Derived as
`(a \ b) ∪ (b \ a)` using the instant-level `difference/3`.

Use this when the question is about *covered time* — "the
hours that one of us has free but the other doesn't". For
the member-preserving filter (whole members of either
operand that don't overlap any member of the other), use
`members_in_exactly_one/3`.

# `union`

```elixir
@spec union(operand :: any(), operand :: any(), keyword()) ::
  {:ok, Tempo.IntervalSet.t()} | {:error, term()}
```

Union of two operands — every member of either operand, kept
as a distinct interval with its original metadata.

Under Tempo's member-preserving semantics, two inputs that
happen to cover the same time range produce **two** members in
the result, not one. If you want the canonical instant-set form
(touching members merged), call `Tempo.IntervalSet.coalesce/1`
on the result.

---

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