# `Tempo.Compare`
[🔗](https://github.com/kipcole9/tempo/blob/v0.20.0/lib/compare.ex#L1)

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.

# `compare_endpoints`

```elixir
@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`

```elixir
@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`

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`

```elixir
@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`

```elixir
@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`

```elixir
@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

* `tempo` is a `t:Tempo.t/0`.

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
