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 — every instant in a that is NOT in b,
returned as one or more trimmed intervals.
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 — every instant present in both operands, returned as one or more trimmed intervals.
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).
Member-preserving anti-overlap filter — the members of a
that do NOT overlap any member of b, kept whole with their
original metadata.
Member-preserving overlap filter — the members of a that
overlap any member of b, kept as distinct intervals with
their original metadata.
true when a and b share at least one instant.
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 — every instant in exactly one
of the operands, returned as trimmed intervals. Derived as
(a \ b) ∪ (b \ a) using the instant-level difference/3.
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 — 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.
@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 — 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.
@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.
@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.
@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.
@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 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 — 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.
@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.