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
:boundoption 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.
Summary
Functions
Normalise two operands to the same anchor class, resolution,
and calendar, and return them both as %Tempo.IntervalSet{}.
Complement of set within bound — the instants in bound
that are NOT covered by any member of set.
true when every instant covered by b is also covered by
a. Alias for subset?(b, a, opts).
Difference a \ b — the members of a that do NOT
overlap any member of b.
true when a and b share no instants — no member of a
overlaps any member of b.
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 of two operands — the members of a that
overlap any member of b, kept as distinct intervals with
their original metadata.
Instant-level intersection — each result interval is the
portion of an a member that overlaps some b member.
Members of a can be split into multiple fragments if b
covers only part of them.
true when a and b share at least one instant.
Instant-level difference — each member of a is trimmed to
its portions that don't overlap any member of b. A member
can be split into multiple fragments if b covers only the
middle.
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 a △ b — members of either operand that
don't overlap any member of the other. Derived as
(a \ b) ∪ (b \ a) using the member-preserving difference.
Union of two operands — every member of either operand, kept as a distinct interval with its original metadata.
Functions
@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
aandbare 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 whenaandbbelong 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.).
@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.
@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).
@spec difference(any(), any(), keyword()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Difference a \ b — the members of a that do NOT
overlap any member of b.
Member-preserving: each surviving member is kept whole, with
its original metadata. A member of a is dropped entirely if
any member of b overlaps it, even partially.
For the instant-level form (trim each member of a to its
non-overlapping portion of b, splitting if necessary), use
split_difference/3.
@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.
@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.
@spec intersection(any(), any(), keyword()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Intersection of two operands — 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 instant-level overlap form (each survivor trimmed to
its overlap with b), use overlap_trim/3.
@spec overlap_trim(any(), any(), keyword()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Instant-level intersection — each result interval is the
portion of an a member that overlaps 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. Use this when you need the overlapping time range
rather than the overlapping members — e.g. "the parts of
my meetings that fall inside business hours".
@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.
@spec split_difference(any(), any(), keyword()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Instant-level difference — each member of a is trimmed to
its portions that don't overlap any member of b. A member
can be split into multiple fragments if b covers only the
middle.
Use this when you need the surviving time range rather than the surviving members — e.g. "the parts of my meetings that aren't in overlap with another booking".
@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.
@spec symmetric_difference(any(), any(), keyword()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}
Symmetric difference a △ b — members of either operand that
don't overlap any member of the other. Derived as
(a \ b) ∪ (b \ a) using the member-preserving difference.
@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.