Tempo.Compare (Tempo v0.20.0)

Copy Markdown View Source

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

compare_endpoints(a, b)

@spec compare_endpoints(Tempo.t(), Tempo.t()) :: :earlier | :later | :same

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

  • a and b are %Tempo{} structs, typically interval endpoints.

Returns

  • :earlier, :later, or :same.

compare_time(arg1, arg2)

@spec compare_time(keyword(), keyword()) :: :lt | :eq | :gt

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

  • a and b are keyword lists like [year: 2022, month: 6].

Returns

  • :lt when a is earlier than b.

  • :gt when a is later than b.

  • :eq when 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_margin_of_error(time)

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.

to_utc_seconds(tempo)

@spec to_utc_seconds(Tempo.t()) :: integer() | float()

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

  • tempo is 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

  • ArgumentError when the Tempo has no :year component (non-anchored values can't be projected to a universal instant).

unit_minimum(arg1)

@spec unit_minimum(atom()) :: integer()

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

validate_zone_offset(tempo)

@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

Returns

  • :ok when 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.