Tempo.Operations (Tempo v0.4.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 — 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

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

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

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

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

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.

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

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

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 — 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(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.