Shared comparison primitives for Tempo values.
Set operations, enumeration, and IntervalSet construction all
need to compare two time keyword lists as start-moments on the
time line. This module is the single place that definition
lives. The comparison treats missing trailing units as their
unit minimum — so [year: 2022] (which means "start of 2022")
compares correctly against [year: 2022, month: 6] (which
means "start of June 2022") without ambiguity.
For set operations that span timezones, to_utc_seconds/1
projects a zoned %Tempo{} into gregorian-seconds-since-UTC
epoch so operands in different zones can share a total order.
The projection is computed on demand and never cached — that
policy decision was made in the implicit-to-explicit plan and
revisited in the set-operations plan.
The same projection makes comparison calendar-independent:
when two values are in different calendars, each is routed
through its calendar's date→absolute-day conversion (the
date_to_iso_days round-trip) before comparison, so a Hebrew
and a Gregorian date order by their true instants rather than
by their raw numeric components. Same-calendar comparison keeps
the fast structural path on the :time lists.
Summary
Functions
Return :earlier, :later, or :same for two %Tempo{}
endpoints comparing by their UTC-projected start-moments.
Compare two time keyword lists as start-moments on the time line.
Drop the margin_of_error annotation from every component of a time
keyword list.
Project a zoned %Tempo{} to UTC gregorian seconds since
year 0 (matching Erlang's :calendar.datetime_to_gregorian_seconds/1
epoch).
The start-of-unit minimum — 1 for :month, :day, :week,
:day_of_year, :day_of_week; 0 for everything else.
Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.
Functions
Return :earlier, :later, or :same for two %Tempo{}
endpoints comparing by their UTC-projected start-moments.
When both Tempos share a zone (or both have nil zone info),
this reduces to compare_time/2 on their :time lists with a
renamed return. When zones differ, both sides are projected to
UTC via to_utc_seconds/1 for a common reference frame.
Arguments
aandbare%Tempo{}structs, typically interval endpoints.
Returns
:earlier,:later, or:same.
Compare two time keyword lists as start-moments on the time line.
Missing trailing units are filled with their unit minimum
(:month / :day / :week / :day_of_year / :day_of_week
count from 1; everything else counts from 0). Both lists must
be sorted descending-by-unit — the invariant the tokenizer and
Unit.sort/2 maintain.
Mismatched units at the same position (e.g. :week vs
:month) fall through to :eq as a conservative bailout. A
well-formed comparison has operands using the same unit
vocabulary.
Arguments
aandbare keyword lists like[year: 2022, month: 6].
Returns
:ltwhenais earlier thanb.:gtwhenais later thanb.:eqwhen they are the same start-moment, or when mismatched unit vocabularies prevent a meaningful order.
Examples
iex> Tempo.Compare.compare_time([year: 2022], [year: 2022, month: 6])
:lt
iex> Tempo.Compare.compare_time([year: 2022, month: 6, day: 15], [year: 2022, month: 6, day: 15])
:eq
iex> Tempo.Compare.compare_time([year: 2023], [year: 2022, month: 12])
:gt
Drop the margin_of_error annotation from every component of a time
keyword list.
A margin of error (2018±2) is a crisp-inert uncertainty annotation
stored as {value, [margin_of_error: n]}. Crisp comparison and
materialisation operate on plain integers, so the annotation is dropped
before those operations (leaving any other annotation, e.g.
significant_digits, untouched). The margin is preserved on the caller's
original value — only the comparison/materialisation copy is reduced to
its crisp core. Graded, margin-aware relations are a future step.
Project a zoned %Tempo{} to UTC gregorian seconds since
year 0 (matching Erlang's :calendar.datetime_to_gregorian_seconds/1
epoch).
The projection is per-call, never cached. When Tzdata is
updated with new zone rules, the next call automatically uses
them. Stored IntervalSet endpoints carry wall-clock + zone as
authoritative — see plans/set-operations.md for the full
rationale on why no UTC cache exists.
Arguments
tempois a%Tempo{}with at minimum year/month/day/hour/ minute/second components. Missing components are padded with their unit minimum.
Returns
integer— gregorian seconds since year 0 in UTC.
Raises
ArgumentErrorwhen the Tempo has no:yearcomponent (non-anchored values can't be projected to a universal instant).
The start-of-unit minimum — 1 for :month, :day, :week,
:day_of_year, :day_of_week; 0 for everything else.
Exposed on Tempo.Compare and Tempo.Math as the same
definition (both modules re-export via delegation).
@spec validate_zone_offset(Tempo.t()) :: :ok | {:error, Tempo.ZoneOffsetMismatchError.t()}
Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.
When a value carries both a numeric offset and a zone identifier (e.g.
2022-11-20T10:37:00+05:00[Europe/Paris]), the offset is normally
consulted only to disambiguate a DST fall-back — the zone otherwise
wins. This check (RFC 9557 §4.2) flags the case where the stated
offset matches no offset the zone actually uses at that instant.
Arguments
tempois aTempo.t/0.
Returns
:okwhen the offset agrees with the zone, when there is nothing to check (no zone, or no explicit offset), or when the value is not anchored (no wall instant to evaluate against).{:error, t:Tempo.ZoneOffsetMismatchError.t/0}when the stated offset disagrees with the zone.
See Tempo.validate_zone_offset/1, which delegates here, for worked
examples.