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.
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
@type certainty() :: :certain | :possible | :impossible
A three-valued relation certainty.
@type interval_like() :: Tempo.t() | t() | Tempo.IntervalSet.t()
Anything relation/2 can reduce to a single bounded interval.
@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.
@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
@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
@spec after?(interval_like(), interval_like()) :: boolean()
true when a starts strictly after b ends, with a gap
(Allen's :preceded_by).
@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
@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
@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.
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
@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
@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
@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
@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 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
relation1is the Allen relation fromAtoB— one of the 13 atomsrelation/2returns.relation2is the Allen relation fromBtoC.
Returns
A list of the relations possible from
AtoC, 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}}
@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— whentrue, adds one second to the returned duration for each IERS leap-second insertion that falls inside[from, to). Defaults tofalseso behaviour matchesDateTime,Time, and:calendarfrom Elixir/OTP (none of which count leap seconds). SeeTempo.Interval.spans_leap_second?/1andleap_seconds_spanned/1for 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"
@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.
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
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
intervalis at/0.
Returns
{from, to}where each endpoint is aTempo.t/0or:undefinedfor 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}
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
aandbaret/0values.
Returns
trueif both intervals occupy the same temporal extent under UTC projection.falseotherwise.
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
@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
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
intervalis at/0.
Returns
- The
fromendpoint as aTempo.t/0or:undefinedfor 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
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
Return the list of IERS leap-second dates that fall inside
[from, to).
Arguments
intervalis at/0with both endpoints present.
Returns
- A list of
{year, month, day}tuples, each entry drawn fromTempo.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}]
@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
@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.
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
intervalis at/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)
%{}
@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
optionsis a keyword list of construction options (see below).
Options
:fromis aTempo.t/0or the atom:undefined(open start).:tois aTempo.t/0or the atom:undefined(open end).:durationis aTempo.Duration.t/0. When combined with:from, the:toendpoint is derived lazily byTempo.to_interval/1.:recurrenceis apos_integer()or:infinity.:repeat_ruleis aTempo.RRule.Rule.t/0orTempo.t/0.:metadatais a free-form map carried through set operations.
Returns
{:ok, t()}on success.{:error, reason}when endpoints are invalid,:fromis not strictly earlier than:to(a zero-extent interval is not a valid interval under the half-open convention; seeTempo.Interval.empty?/1for 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
@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
fromis the startTempo.t/0(inclusive).tois the endTempo.t/0(exclusive), and must be strictly later thanfrom.
Returns
{:ok, interval}on success.{:error, reason}when the endpoints are of incompatible calendars, orfromis not strictly earlier thanto.
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]
Bang variant of new/1. Raises on invalid input.
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]
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.
@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
aandbare each a boundedTempo.t/0,Tempo.Interval.t/0, or single-memberTempo.IntervalSet.t/0.
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
@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
@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
@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
@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
@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:
| Relation | Shape (X relative to Y) | Condition |
|---|---|---|
:precedes | X ends strictly before Y starts | x₂ < y₁ |
:meets | X ends exactly at Y's start | x₂ = y₁ |
:overlaps | X starts before Y, ends inside | x₁ < y₁ < x₂ < y₂ |
:finished_by | X contains Y, shared end | x₁ < y₁ ∧ x₂ = y₂ |
:contains | X strictly contains Y | x₁ < y₁ ∧ x₂ > y₂ |
:starts | Shared start, X ends earlier | x₁ = y₁ ∧ x₂ < y₂ |
:equals | Identical endpoints | x₁ = y₁ ∧ x₂ = y₂ |
:started_by | Shared start, X ends later | x₁ = y₁ ∧ x₂ > y₂ |
:during | X strictly inside Y | x₁ > y₁ ∧ x₂ < y₂ |
:finishes | X starts after Y, shared end | x₁ > y₁ ∧ x₂ = y₂ |
:overlapped_by | Y starts before X, ends inside X | y₁ < x₁ < y₂ < x₂ |
:met_by | X starts exactly at Y's end | x₁ = y₂ |
:preceded_by | X starts strictly after Y's end | x₁ > y₂ |
Every pair of non-empty bounded intervals stands in exactly one of these relations.
Arguments
aandbare each one of:a
Tempo.t/0point (materialised via its implicit span).a
Tempo.IntervalSet.t/0with exactly one member.
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 useTempo.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
@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
aandbare bounded interval-like values.targetis 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 aTempo.RequiresAnchorError.t/0when 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
@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
intervalis at/0. Must be bounded (both endpoints present) —:undefinedendpoints return:undefined.
Returns
- A unit atom (
:year,:month,:day,:hour,:minute,:second, …), or:undefinedfor 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
@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
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
intervalis at/0with both endpoints present. Unbounded intervals always returnfalse(open-ended to:undefined) and pre-Unix-era intervals pre-1972 returnfalse(IERS leap seconds started in 1972).
Returns
truewhen at least one entry fromTempo.LeapSeconds.dates/0falls inside[from, to).falseotherwise.
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
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
intervalis at/0.
Returns
- The
toendpoint as aTempo.t/0or:undefinedfor 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
@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
@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
aandbare each a boundedTempo.t/0,Tempo.Interval.t/0, or single-memberTempo.IntervalSet.t/0.
Returns
:certain,:possible, or:impossible.{:error, reason}for open-ended or multi-member operands, or aTempo.RequiresAnchorError.t/0when 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