Tempo.Interval (Tempo v0.20.0)

Copy Markdown View Source

An explicit bounded span on the time line.

Every Tempo value is an interval at some resolution; a bare %Tempo{} materialises to an %Tempo.Interval{} via Tempo.to_interval/1. %Tempo.Interval{} carries explicit from and to endpoints plus optional recurrence metadata (recurrence, duration, repeat_rule) for RRULE-style values.

Tempo uses the half-open [from, to) convention: from is inclusive, to is exclusive. Adjacent intervals concatenate cleanly — [a, b) ++ [b, c) == [a, c).

Comparing intervals

relation/2 classifies two intervals by Allen's interval algebra, returning one of 13 mutually-exclusive relations. See the function docs for the full table.

Domain semantics: continuous time, discrete-style intervals

Tempo's underlying time line is continuous: endpoints project to gregorian seconds (a real number, via Erlang's :calendar.datetime_to_gregorian_seconds/1) and cross-zone comparison uses real-number ordering. The set of representable endpoint positions is dense within Tempo's resolution range (currently down to one-second granularity).

Interval boundary semantics are nonetheless treated in the discrete style — the :to endpoint is exclusive, so two intervals [a, b) and [b, c) meet at the shared boundary b with empty geometric intersection. This is the same convention Rust's allen-intervals crate uses for its discrete integer domain, and matches Hayes' open-connected-subsets model that Grüninger and Li cite (TIME 2017, §2.2). It contrasts with the closed-interval / continuous-domain convention (e.g., Allen's original 1983 paper, which treats intervals as closed and lets meets happen at a single shared point of inclusion); Tempo's half-open choice keeps adjacency unambiguous and lets coalescing be a pure operation on endpoints without a "do these touch?" predicate.

In practical terms: if you need to model an event that includes both endpoint moments (a true closed interval), encode it as [a, b + ε) where ε is one unit at the value's resolution. Library code never imposes this — the half-open convention is the contract.

Summary

Types

A three-valued relation certainty.

Anything relation/2 can reduce to a single bounded interval.

One of Allen's 13 interval relations — jointly exhaustive and pairwise disjoint under the half-open [from, to) convention.

t()

Functions

true when the two intervals touch at a single boundary — either a meets b or b meets a (Allen's :meets | :met_by).

true when a starts strictly after b ends, with a gap (Allen's :preceded_by).

true when the interval is at least as long as the given duration.

true when the interval is at most as long as the given duration.

true when a ends strictly before b starts, with a gap (Allen's :precedes). Use adjacent?/2 to include the no-gap case.

true when both endpoints are concrete %Tempo{} values — neither :undefined nor nil. Useful as a guard before set operations or duration checks.

true when a starts after b ends, with a gap (Allen :preceded_by), for every placement of their ± margins. Crisp counterpart: after?/2.

true when a ends before b starts, with a gap (Allen :precedes), for every placement of their ± margins. Crisp counterpart: before?/2.

true when a and b intersect for every placement of their ± margins — overlap_certainty(a, b) == :certain. Crisp counterpart: overlaps?/2.

true when a falls within b for every placement of their ± margins. Crisp counterpart: within?/2.

Compose two Allen relations — the relations possible from A to C given A r1 B and B r2 C.

Return the interval's length as a %Tempo.Duration{} in seconds. Returns :infinity for unbounded intervals (one or both endpoints :undefined).

true when a is strictly inside b — both endpoints of a lie strictly within b (Allen's :during). Shared- endpoint cases (:starts, :finishes) return false; use within?/2 for the inclusive version.

true when the interval has zero or negative length — from == to (degenerate instant) or from > to (inverted span).

Return the interval's endpoints as a {from, to} tuple.

true when two intervals describe the same temporal extent, regardless of calendar, zone display, or attached metadata.

true when the interval's length equals the given duration exactly.

Return the interval's from endpoint.

The inverse Allen relation.

Return the list of IERS leap-second dates that fall inside [from, to).

true when the interval's length is strictly greater than the given duration.

true when a's end coincides exactly with b's start (Allen's :meets). Under the half-open convention this means the intervals share no point but have no gap.

Return the metadata map attached to the interval.

Construct a Tempo.Interval.t/0 from a keyword list of options.

Construct a bounded t/0 from two concrete endpoints.

Bang variant of new/1. Raises on invalid input.

Bang variant of new/2. Raises on invalid input.

Given a fully-resolved %Tempo{}, compute the two endpoints of its implicit span under the half-open [from, to) convention.

The certainty that a and b intersect, given their ± margins.

true when a could start after b ends for some placement of their ± margins.

true when a could end before b starts for some placement of their ± margins.

true when a and b could intersect for some placement of their ± margins — overlap_certainty/2 is :certain or :possible.

true when a could fall within b for some placement of their ± margins.

Classify the Allen relation between two interval-like values.

The certainty that relation(a, b) is (one of) target.

Return the interval's span resolution — the coarsest unit at which from and to differ.

true when the interval's length is strictly less than the given duration.

Return true when the interval [from, to) contains at least one IERS-announced positive leap second.

Return the interval's to endpoint.

true when a lies inside b inclusive of shared endpoints (Allen's :equals | :starts | :during | :finishes). The canonical "does this fit inside that window?" predicate.

The certainty that a falls within b, given any uncertainty in either.

Types

certainty()

@type certainty() :: :certain | :possible | :impossible

A three-valued relation certainty.

interval_like()

@type interval_like() :: Tempo.t() | t() | Tempo.IntervalSet.t()

Anything relation/2 can reduce to a single bounded interval.

relation()

@type relation() ::
  :precedes
  | :meets
  | :overlaps
  | :finished_by
  | :contains
  | :starts
  | :equals
  | :started_by
  | :during
  | :finishes
  | :overlapped_by
  | :met_by
  | :preceded_by

One of Allen's 13 interval relations — jointly exhaustive and pairwise disjoint under the half-open [from, to) convention.

t()

@type t() :: %Tempo.Interval{
  direction: 1 | -1,
  duration: Tempo.Duration.t() | nil,
  from: Tempo.t() | Tempo.Duration.t() | :undefined | nil,
  metadata: map(),
  recurrence: pos_integer() | :infinity,
  repeat_rule: Tempo.t() | nil,
  to: Tempo.t() | :undefined | nil
}

Functions

adjacent?(a, b)

@spec adjacent?(interval_like(), interval_like()) :: boolean()

true when the two intervals touch at a single boundary — either a meets b or b meets a (Allen's :meets | :met_by).

Examples

iex> Tempo.Interval.adjacent?(~o"2026-06-15", ~o"2026-06-16")
true

iex> Tempo.Interval.adjacent?(~o"2026-06-15", ~o"2026-06-17")
false

after?(a, b)

@spec after?(interval_like(), interval_like()) :: boolean()

true when a starts strictly after b ends, with a gap (Allen's :preceded_by).

at_least?(interval, d)

@spec at_least?(t(), Tempo.Duration.t()) :: boolean()

true when the interval is at least as long as the given duration.

Unbounded intervals (:undefined endpoint) satisfy any finite minimum — an infinite span is trivially "at least" any duration.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T11"}
iex> Tempo.Interval.at_least?(iv, ~o"PT1H")
true
iex> Tempo.Interval.at_least?(iv, ~o"PT3H")
false

at_most?(interval, d)

@spec at_most?(t(), Tempo.Duration.t()) :: boolean()

true when the interval is at most as long as the given duration.

Unbounded intervals return false — an infinite span exceeds any finite maximum.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T10"}
iex> Tempo.Interval.at_most?(iv, ~o"PT1H")
true
iex> Tempo.Interval.at_most?(iv, ~o"PT30M")
false

before?(a, b)

@spec before?(interval_like(), interval_like()) :: boolean()

true when a ends strictly before b starts, with a gap (Allen's :precedes). Use adjacent?/2 to include the no-gap case.

Returns false on any error or non-matching relation.

bounded?(interval)

@spec bounded?(t()) :: boolean()

true when both endpoints are concrete %Tempo{} values — neither :undefined nor nil. Useful as a guard before set operations or duration checks.

Examples

iex> Tempo.Interval.bounded?(%Tempo.Interval{from: ~o"2026-06-01", to: ~o"2026-06-10"})
true

iex> Tempo.Interval.bounded?(%Tempo.Interval{from: ~o"2026-06-01", to: :undefined})
false

certainly_after?(a, b)

@spec certainly_after?(interval_like(), interval_like()) :: boolean()

true when a starts after b ends, with a gap (Allen :preceded_by), for every placement of their ± margins. Crisp counterpart: after?/2.

Examples

iex> Tempo.Interval.certainly_after?(~o"2010±1Y", ~o"2000±1Y")
true

certainly_before?(a, b)

@spec certainly_before?(interval_like(), interval_like()) :: boolean()

true when a ends before b starts, with a gap (Allen :precedes), for every placement of their ± margins. Crisp counterpart: before?/2.

Examples

iex> Tempo.Interval.certainly_before?(~o"2000±1Y", ~o"2010±1Y")
true

iex> Tempo.Interval.certainly_before?(~o"2000±1Y", ~o"2001±1Y")
false

certainly_overlaps?(a, b)

@spec certainly_overlaps?(interval_like(), interval_like()) :: boolean()

true when a and b intersect for every placement of their ± margins — overlap_certainty(a, b) == :certain. Crisp counterpart: overlaps?/2.

Examples

iex> Tempo.Interval.certainly_overlaps?(~o"2000Y", ~o"2000Y")
true

iex> Tempo.Interval.certainly_overlaps?(~o"2000±1Y", ~o"2001±1Y")
false

certainly_within?(a, b)

@spec certainly_within?(interval_like(), interval_like()) :: boolean()

true when a falls within b for every placement of their ± margins. Crisp counterpart: within?/2.

Examples

iex> Tempo.Interval.certainly_within?(~o"2000Y6M", ~o"2000Y")
true

compose(relation1, relation2)

@spec compose(relation(), relation()) ::
  [relation()] | {:error, {:invalid_relation, term()}}

Compose two Allen relations — the relations possible from A to C given A r1 B and B r2 C.

This is Allen's interval-algebra composition (Allen 1983): a constant-time read of the 13×13 table. Where relation/2 compares two intervals you hold, compose/2 takes one qualitative step with no interval in hand — "if A precedes B and B is during C, how can A relate to C?" — and returns every relation some arrangement of the three intervals allows. It is the same reasoning Tempo.Network.Solver.relation/3 applies across a whole network, reduced to a single step.

Arguments

  • relation1 is the Allen relation from A to B — one of the 13 atoms relation/2 returns.

  • relation2 is the Allen relation from B to C.

Returns

  • A list of the relations possible from A to C, in Allen's canonical order — one element when the composition is determined, up to all 13 when the step is fully ambiguous.

  • {:error, {:invalid_relation, term}} when either argument is not one of the 13 relation atoms.

Examples

iex> Tempo.Interval.compose(:precedes, :during)
[:precedes, :meets, :overlaps, :starts, :during]

iex> Tempo.Interval.compose(:equals, :overlaps)
[:overlaps]

iex> Tempo.Interval.compose(:precedes, :nonsense)
{:error, {:invalid_relation, :nonsense}}

duration(interval, opts \\ [])

@spec duration(
  t(),
  keyword()
) :: Tempo.Duration.t() | :infinity

Return the interval's length as a %Tempo.Duration{} in seconds. Returns :infinity for unbounded intervals (one or both endpoints :undefined).

The result is calendar- and zone-aware — it goes through Tempo.Compare.to_utc_seconds/1 so cross-zone intervals compute a correct wall-clock delta.

Options

  • :leap_seconds — when true, adds one second to the returned duration for each IERS leap-second insertion that falls inside [from, to). Defaults to false so behaviour matches DateTime, Time, and :calendar from Elixir/OTP (none of which count leap seconds). See Tempo.Interval.spans_leap_second?/1 and leap_seconds_spanned/1 for detection without arithmetic.

Examples

iex> Tempo.Interval.duration(%Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T10"})
~o"PT3600S"

iex> Tempo.Interval.duration(%Tempo.Interval{from: ~o"2026-06-15", to: :undefined})
:infinity

iex> iv = %Tempo.Interval{from: ~o"2016-12-31T23:59:00Z", to: ~o"2017-01-01T00:01:00Z"}
iex> Tempo.Interval.duration(iv)
~o"PT120S"
iex> Tempo.Interval.duration(iv, leap_seconds: true)
~o"PT121S"

during?(a, b)

@spec during?(interval_like(), interval_like()) :: boolean()

true when a is strictly inside b — both endpoints of a lie strictly within b (Allen's :during). Shared- endpoint cases (:starts, :finishes) return false; use within?/2 for the inclusive version.

empty?(interval)

@spec empty?(t()) :: boolean()

true when the interval has zero or negative length — from == to (degenerate instant) or from > to (inverted span).

Under the half-open [from, to) convention, an interval with from >= to contains no real instants. Empty intervals pass bounded?/1 but have no span; inverted intervals are treated as empty rather than as a span with "negative" duration.

Examples

iex> Tempo.Interval.empty?(%Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-15"})
true

iex> Tempo.Interval.empty?(%Tempo.Interval{from: ~o"2026-06-20", to: ~o"2026-06-15"})
true

iex> Tempo.Interval.empty?(%Tempo.Interval{from: ~o"2026-06-01", to: ~o"2026-06-10"})
false

endpoints(interval)

@spec endpoints(t()) :: {Tempo.t() | :undefined, Tempo.t() | :undefined}

Return the interval's endpoints as a {from, to} tuple.

A named helper so callers never have to reach into the struct fields in user-facing code.

Arguments

  • interval is a t/0.

Returns

  • {from, to} where each endpoint is a Tempo.t/0 or :undefined for open-ended intervals.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-20"}
iex> {from, to} = Tempo.Interval.endpoints(iv)
iex> {Tempo.day(from), Tempo.day(to)}
{15, 20}

equivalent?(a, b)

@spec equivalent?(t(), t()) :: boolean()

true when two intervals describe the same temporal extent, regardless of calendar, zone display, or attached metadata.

Standard == compares all struct fields, including :metadata, :calendar, and the zone-display details on the endpoints, so the same span shown in two different zones compares unequal. equivalent?/2 projects endpoints to UTC and compares only the temporal positions — matching the equivalence notion of the T_bounded_meeting ontology of Grüninger and Li (TIME 2017), under which intervals are individuated by their position in the structure of meets, not by labels.

Recurrence-related fields (:recurrence, :direction, :repeat_rule, :duration) must match structurally: two recurring intervals with different rules describe different extents.

Arguments

  • a and b are t/0 values.

Returns

  • true if both intervals occupy the same temporal extent under UTC projection.

  • false otherwise.

Examples

iex> a = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"}
iex> b = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"}
iex> Tempo.Interval.equivalent?(a, b)
true

iex> a = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"}
iex> b = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-17"}
iex> Tempo.Interval.equivalent?(a, b)
false

exactly?(interval, d)

@spec exactly?(t(), Tempo.Duration.t()) :: boolean()

true when the interval's length equals the given duration exactly.

Unbounded intervals always return false.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T10"}
iex> Tempo.Interval.exactly?(iv, ~o"PT1H")
true
iex> Tempo.Interval.exactly?(iv, ~o"PT2H")
false

from(interval)

@spec from(t()) :: Tempo.t() | :undefined

Return the interval's from endpoint.

A named helper so callers never have to reach into the struct fields in user-facing code. Compose with Tempo.day/1, Tempo.year/1, etc. to extract components of the starting point.

Arguments

  • interval is a t/0.

Returns

  • The from endpoint as a Tempo.t/0 or :undefined for open-ended intervals.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-20"}
iex> Tempo.Interval.from(iv) |> Tempo.day()
15

inverse_relation(atom)

@spec inverse_relation(relation()) :: relation()

The inverse Allen relation.

If relation(a, b) returns r, then relation(b, a) returns inverse_relation(r).

Examples

iex> Tempo.Interval.inverse_relation(:contains)
:during

iex> Tempo.Interval.inverse_relation(:precedes)
:preceded_by

iex> Tempo.Interval.inverse_relation(:equals)
:equals

leap_seconds_spanned(iv)

@spec leap_seconds_spanned(t()) :: [{integer(), 1..12, 1..31}]

Return the list of IERS leap-second dates that fall inside [from, to).

Arguments

  • interval is a t/0 with both endpoints present.

Returns

  • A list of {year, month, day} tuples, each entry drawn from Tempo.LeapSeconds.dates/0. Empty list when no leap second falls inside the span, or when either endpoint is :undefined.

Examples

iex> iv = %Tempo.Interval{from: ~o"2015-01-01", to: ~o"2017-12-31"}
iex> Tempo.Interval.leap_seconds_spanned(iv)
[{2015, 6, 30}, {2016, 12, 31}]

longer_than?(interval, d)

@spec longer_than?(t(), Tempo.Duration.t()) :: boolean()

true when the interval's length is strictly greater than the given duration.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T11"}
iex> Tempo.Interval.longer_than?(iv, ~o"PT1H")
true
iex> Tempo.Interval.longer_than?(iv, ~o"PT2H")
false

meets?(a, b)

@spec meets?(interval_like(), interval_like()) :: boolean()

true when a's end coincides exactly with b's start (Allen's :meets). Under the half-open convention this means the intervals share no point but have no gap.

metadata(interval)

@spec metadata(t()) :: map()

Return the metadata map attached to the interval.

A named helper so callers never have to reach into the struct fields in user-facing code. Metadata is free-form and is preserved across set operations — intervals that survive a union, intersection, or difference inherit the surviving operand's metadata, so this accessor is the intended way to read iCal SUMMARY, LOCATION, event UIDs, and any other application-attached per-interval data.

Arguments

  • interval is a t/0.

Returns

  • The metadata map. An interval constructed without metadata returns %{}.

Examples

iex> iv = Tempo.Interval.new!(
...>   from: ~o"2026-06-15T09",
...>   to:   ~o"2026-06-15T10",
...>   metadata: %{summary: "Stand-up"}
...> )
iex> Tempo.Interval.metadata(iv)
%{summary: "Stand-up"}

iex> iv = Tempo.Interval.new!(from: ~o"2026-06-15", to: ~o"2026-06-20")
iex> Tempo.Interval.metadata(iv)
%{}

new(options)

@spec new(keyword()) :: {:ok, t()} | {:error, Exception.t()}

Construct a Tempo.Interval.t/0 from a keyword list of options.

The companion to ~o interval sigils and Tempo.to_interval/1. Use this when you have the endpoints as runtime values (e.g. two %Tempo{} structs) rather than an ISO 8601 string.

At least one of :from, :to, or :duration must be supplied.

Arguments

  • options is a keyword list of construction options (see below).

Options

Returns

  • {:ok, t()} on success.

  • {:error, reason} when endpoints are invalid, :from is not strictly earlier than :to (a zero-extent interval is not a valid interval under the half-open convention; see Tempo.Interval.empty?/1 for the predicate that detects malformed struct literals), or required fields are missing.

Examples

iex> {:ok, iv} = Tempo.Interval.new(
...>   from: Tempo.new!(year: 2026, month: 6, day: 15, hour: 9),
...>   to:   Tempo.new!(year: 2026, month: 6, day: 15, hour: 17)
...> )
iex> iv.from.time
[year: 2026, month: 6, day: 15, hour: 9]

iex> {:ok, iv} = Tempo.Interval.new(
...>   from: Tempo.new!(year: 1985),
...>   to: :undefined
...> )
iex> iv.to
:undefined

new(from, to)

@spec new(Tempo.t(), Tempo.t()) :: {:ok, t()} | {:error, Exception.t()}

Construct a bounded t/0 from two concrete endpoints.

The positional companion to new/1 for the common case — a closed interval [from, to) from two %Tempo{} values. Equivalent to new(from: from, to: to), but reads as the two nouns it is. For the :duration, :recurrence, :repeat_rule, or :metadata forms — or an open-ended endpoint (:undefined) — use the keyword new/1, where the labels earn their place.

Arguments

  • from is the start Tempo.t/0 (inclusive).

  • to is the end Tempo.t/0 (exclusive), and must be strictly later than from.

Returns

  • {:ok, interval} on success.

  • {:error, reason} when the endpoints are of incompatible calendars, or from is not strictly earlier than to.

Examples

iex> {:ok, iv} = Tempo.Interval.new(~o"2026-06-15T09", ~o"2026-06-15T17")
iex> iv.from.time
[year: 2026, month: 6, day: 15, hour: 9]

new!(options)

@spec new!(keyword()) :: t()

Bang variant of new/1. Raises on invalid input.

new!(from, to)

@spec new!(Tempo.t(), Tempo.t()) :: t()

Bang variant of new/2. Raises on invalid input.

Examples

iex> iv = Tempo.Interval.new!(~o"2026-06-15T09", ~o"2026-06-15T17")
iex> iv.to.time
[year: 2026, month: 6, day: 15, hour: 17]

next_unit_boundary(tempo)

Given a fully-resolved %Tempo{}, compute the two endpoints of its implicit span under the half-open [from, to) convention.

Returns {:ok, {lower, upper}} where both are %Tempo{} values, or {:error, reason} when the input has no finer unit that could produce a bounded span (e.g. a fully-specified second-resolution datetime).

The lower bound is the input's time extended with the minimum of the next-finer unit (so [year: 2022] becomes [year: 2022, month: 1] on a month-based calendar). The upper bound is the lower bound incremented by one unit at the input's own resolution, carrying via the calendar module. Masked values widen to the coarsest un-masked prefix and use the internal mask-bounds helper to determine the enclosing span.

overlap_certainty(a, b)

@spec overlap_certainty(interval_like(), interval_like()) ::
  certainty() | {:error, term()}

The certainty that a and b intersect, given their ± margins.

The three-valued counterpart of overlaps?/2. Each margin-bearing endpoint is widened into a range, and the result reports whether intersection holds for every consistent placement (:certain), some (:possible), or none (:impossible). Crisp operands degrade exactly to overlaps?/2 (only :certain/:impossible occur).

Arguments

Returns

  • :certain, :possible, or :impossible.

  • {:error, reason} when either operand is open-ended or a multi-member set.

Examples

iex> Tempo.Interval.overlap_certainty(~o"2000±1Y", ~o"2010±1Y")
:impossible

iex> Tempo.Interval.overlap_certainty(~o"2000±1Y", ~o"2001±1Y")
:possible

iex> Tempo.Interval.overlap_certainty(~o"2000Y", ~o"2000Y")
:certain

possibly_after?(a, b)

@spec possibly_after?(interval_like(), interval_like()) :: boolean()

true when a could start after b ends for some placement of their ± margins.

Examples

iex> Tempo.Interval.possibly_after?(~o"2001±1Y", ~o"2000±1Y")
true

possibly_before?(a, b)

@spec possibly_before?(interval_like(), interval_like()) :: boolean()

true when a could end before b starts for some placement of their ± margins.

Examples

iex> Tempo.Interval.possibly_before?(~o"2000±1Y", ~o"2001±1Y")
true

possibly_overlaps?(a, b)

@spec possibly_overlaps?(interval_like(), interval_like()) :: boolean()

true when a and b could intersect for some placement of their ± margins — overlap_certainty/2 is :certain or :possible.

Examples

iex> Tempo.Interval.possibly_overlaps?(~o"2000±1Y", ~o"2001±1Y")
true

iex> Tempo.Interval.possibly_overlaps?(~o"2000±1Y", ~o"2010±1Y")
false

possibly_within?(a, b)

@spec possibly_within?(interval_like(), interval_like()) :: boolean()

true when a could fall within b for some placement of their ± margins.

Examples

iex> Tempo.Interval.possibly_within?(~o"2000±1Y", ~o"2000Y")
true

relation(a, b)

@spec relation(interval_like(), interval_like()) :: relation() | {:error, term()}

Classify the Allen relation between two interval-like values.

Returns one of 13 mutually exclusive relations from Allen's interval algebra — a richer answer than stdlib's ternary compare/2 (:lt / :eq / :gt), which collapses intervals to their start points and loses the containment and overlap distinctions that interval algebra captures. Hence the name relation rather than compare.

For intervals X = [x₁, x₂) and Y = [y₁, y₂) under Tempo's half-open convention:

RelationShape (X relative to Y)Condition
:precedesX ends strictly before Y startsx₂ < y₁
:meetsX ends exactly at Y's startx₂ = y₁
:overlapsX starts before Y, ends insidex₁ < y₁ < x₂ < y₂
:finished_byX contains Y, shared endx₁ < y₁ ∧ x₂ = y₂
:containsX strictly contains Yx₁ < y₁ ∧ x₂ > y₂
:startsShared start, X ends earlierx₁ = y₁ ∧ x₂ < y₂
:equalsIdentical endpointsx₁ = y₁ ∧ x₂ = y₂
:started_byShared start, X ends laterx₁ = y₁ ∧ x₂ > y₂
:duringX strictly inside Yx₁ > y₁ ∧ x₂ < y₂
:finishesX starts after Y, shared endx₁ > y₁ ∧ x₂ = y₂
:overlapped_byY starts before X, ends inside Xy₁ < x₁ < y₂ < x₂
:met_byX starts exactly at Y's endx₁ = y₂
:preceded_byX starts strictly after Y's endx₁ > y₂

Every pair of non-empty bounded intervals stands in exactly one of these relations.

Arguments

Returns

  • One of the 13 relation atoms.

  • {:error, reason} when either operand is a multi-member IntervalSet, an open-ended interval, or otherwise can't be reduced to a single bounded interval. For multi-member sets use Tempo.IntervalSet.relation_matrix/2.

Examples

iex> a = Tempo.Interval.new!(from: ~o"2026-06-01", to: ~o"2026-06-10")
iex> b = Tempo.Interval.new!(from: ~o"2026-06-05", to: ~o"2026-06-15")
iex> Tempo.Interval.relation(a, b)
:overlaps

iex> Tempo.Interval.relation(~o"2026Y", ~o"2026-06-15")
:contains

relation_certainty(a, b, target)

@spec relation_certainty(interval_like(), interval_like(), relation() | [relation()]) ::
  certainty() | {:error, term()}

The certainty that relation(a, b) is (one of) target.

Certainty is containment of the possible relations in target. The possible relations account for any ± margin and for underspecification: an unspecified-digit operand (~o"20XXY") is read over every grounding its mask admits, and two un-anchored operands compare on a shared leading unit.

Arguments

  • a and b are bounded interval-like values.

  • target is a single Allen relation atom (e.g. :during) or a list of relation atoms.

Returns

  • :certain, :possible, or :impossible.

  • {:error, reason} for open-ended or multi-member operands, or a Tempo.RequiresAnchorError.t/0 when an un-anchored operand is compared across resolution axes.

Examples

iex> Tempo.Interval.relation_certainty(~o"2000±1Y", ~o"2010±1Y", :precedes)
:certain

iex> Tempo.Interval.relation_certainty(~o"2000Y", ~o"2000Y", :equals)
:certain

iex> # some year in 2000–2099 may precede 2050, may follow it
iex> Tempo.Interval.relation_certainty(~o"20XXY", ~o"2050Y", :precedes)
:possible

resolution(interval)

@spec resolution(t()) :: Tempo.time_unit() | :undefined

Return the interval's span resolution — the coarsest unit at which from and to differ.

Under the half-open [from, to) convention, this is the unit that "ticks forward" across the span. [2026-06-15, 2026-06-16) ticks at the day; [2026-06-01, 2026-07-01) ticks at the month; [2026, 2027) ticks at the year.

Unlike Tempo.resolution/1 on a filled endpoint (which would report the finest unit present on the time keyword list after Tempo.to_interval/1 has padded missing units with their minimums), this function reports the span's resolution — the authoritative scale of the interval itself.

Arguments

  • interval is a t/0. Must be bounded (both endpoints present) — :undefined endpoints return :undefined.

Returns

  • A unit atom (:year, :month, :day, :hour, :minute, :second, …), or :undefined for open-ended intervals.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"}
iex> Tempo.Interval.resolution(iv)
:day

iex> iv = %Tempo.Interval{from: ~o"2026-06", to: ~o"2026-07"}
iex> Tempo.Interval.resolution(iv)
:month

shorter_than?(interval, d)

@spec shorter_than?(t(), Tempo.Duration.t()) :: boolean()

true when the interval's length is strictly less than the given duration.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T10"}
iex> Tempo.Interval.shorter_than?(iv, ~o"PT2H")
true
iex> Tempo.Interval.shorter_than?(iv, ~o"PT1H")
false

spans_leap_second?(iv)

@spec spans_leap_second?(t()) :: boolean()

Return true when the interval [from, to) contains at least one IERS-announced positive leap second.

A historical predicate: it doesn't affect any other Tempo operation. Use it when you want to know if an elapsed-time calculation needs leap-second correction, or to flag intervals for a scientific/astronomy pipeline.

Arguments

  • interval is a t/0 with both endpoints present. Unbounded intervals always return false (open-ended to :undefined) and pre-Unix-era intervals pre-1972 return false (IERS leap seconds started in 1972).

Returns

Examples

iex> iv = %Tempo.Interval{from: ~o"2016-12-31T23:00:00Z", to: ~o"2017-01-01T01:00:00Z"}
iex> Tempo.Interval.spans_leap_second?(iv)
true

iex> iv = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-16"}
iex> Tempo.Interval.spans_leap_second?(iv)
false

to(interval)

@spec to(t()) :: Tempo.t() | :undefined

Return the interval's to endpoint.

Under half-open [from, to) semantics, this is the exclusive upper bound — the first instant outside the span.

Arguments

  • interval is a t/0.

Returns

  • The to endpoint as a Tempo.t/0 or :undefined for open-ended intervals.

Examples

iex> iv = %Tempo.Interval{from: ~o"2026-06-15", to: ~o"2026-06-20"}
iex> Tempo.Interval.to(iv) |> Tempo.day()
20

within?(a, b)

@spec within?(interval_like(), interval_like()) :: boolean()

true when a lies inside b inclusive of shared endpoints (Allen's :equals | :starts | :during | :finishes). The canonical "does this fit inside that window?" predicate.

Examples

iex> a = %Tempo.Interval{from: ~o"2026-06-15T10", to: ~o"2026-06-15T11"}
iex> window = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T17"}
iex> Tempo.Interval.within?(a, window)
true
iex> # Candidate shares the window's start — still inside
iex> a2 = %Tempo.Interval{from: ~o"2026-06-15T09", to: ~o"2026-06-15T10"}
iex> Tempo.Interval.within?(a2, window)
true

within_certainty(a, b)

@spec within_certainty(interval_like(), interval_like()) ::
  certainty() | {:error, term()}

The certainty that a falls within b, given any uncertainty in either.

The three-valued counterpart of within?/2 (Allen :equals | :starts | :during | :finishes). Uncertainty may be a ± margin or underspecification — an unspecified-digit value (~o"20XXY") is read over the set of years its mask admits, so :possible reports that some but not all groundings fall within b. Crisp operands degrade exactly to within?/2.

Arguments

Returns

  • :certain, :possible, or :impossible.

  • {:error, reason} for open-ended or multi-member operands, or a Tempo.RequiresAnchorError.t/0 when an un-anchored operand is compared across resolution axes.

Examples

iex> Tempo.Interval.within_certainty(~o"2000Y6M", ~o"2000Y")
:certain

iex> Tempo.Interval.within_certainty(~o"2000±1Y", ~o"2000Y")
:possible

iex> # ~o"20XXY" is some year in [2000, 2100); 2000 escapes [2001, 2101)
iex> Tempo.Interval.within_certainty(~o"20XXY", ~o"2001Y/2101Y")
:possible