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.
Summary
Types
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.
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 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.
Bang variant of new/1. Raises on invalid input.
Given a fully-resolved %Tempo{}, compute the two endpoints of
its implicit span under the half-open [from, to) convention.
Classify the Allen relation between two interval-like values.
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.
Types
@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 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}
@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 greater than:to, 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
Bang variant of new/1. Raises on invalid input.
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 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 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