Tempo.Operations (Tempo v0.5.0)

Copy Markdown View Source

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.

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

align(a, b, opts \\ [])

@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(set, opts)

@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?(a, b, opts \\ [])

@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(a, b, 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.

disjoint?(a, b, opts \\ [])

@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?(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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?(a, b, opts \\ [])

@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?(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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(a, b, opts \\ [])

@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.