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

Documentation for `Tempo`.

### Terminology

The following terms, defined by ISO 8601, are used throughout
Tempo. For further information consult:

* [ISO Online browsing platform](https://www.iso.org/obp)
* [IEC Electropedia](http://www.electropedia.org/)

#### Date

A [time](#time) on the the calendar time scale. Common forms of date include calendar date,
ordinal date or week date.

#### Time

A mark attributed to an [instant](#instant) or a [time interval](#time_interval) on a specified
[time scale](#time_scale).

The term “time” is often used in common language. However, it should only be used if the
meaning is clearly visible from the context.

On a time scale consisting of successive time intervals, such as a clock or calendar,
distinct instants may be expressed by the same time.

This definition corresponds with the definition of the term “date” in
IEC 60050-113:2011, 113-01-12.

#### Instant

A point on the [time axis](#time_axis). An instantaneous event occurs at a specific instant.

#### Time axis

A mathematical representation of the succession in time according to the space-time model
of instantaneous events along a unique axis/

According to the theory of special relativity, the time axis depends on the choice of a
spatial reference frame.

In IEC 60050-113:2011, 113-01-03, time according to the space-time model is defined to be
the one-dimensional subspace of space-time, locally orthogonal to space.

#### Time scale

A system of ordered marks which can be attributed to [instants](#instant) on the
[time axis](#time_axis), one instant being chosen as the origin.

A time scale may amongst others be chosen as:

* continuous, e.g. international atomic time (TAI) (see IEC 60050-713:1998, 713-05-18);

* continuous with discontinuities, e.g. UTC due to leap seconds, standard time due
  to summer time and winter time;

* successive steps, e.g. [calendars](#calendar), where the [time axis](#time_axis) is split
  up into a succession of consecutive time intervals and the same mark is attributed to all
  instants of each time interval;

* discrete, e.g. in digital techniques.

#### Time interval

A part of the [time axis](#time_axis) limited by two [instants](#instant) *including, unless
otherwise stated, the limiting instants themselves*.

#### Time scale unit

A unit of measurement of a [duration](#duration)

For example:

* Calendar year, calendar month and calendar day are time scale units
  of the Gregorian calendar.

* Clock hour, clock minutes and clock seconds are time scale units of the 24-hour clock.

In Tempo, time scale units are referred to by the shortened term "unit".  When a "unit" is
combined with a value, the combination is referred to as a "component".

#### Duration

A non-negative quantity of time equal to the difference between the final and initial
[instants](#instant) of a [time interval](#interval)

The duration is one of the base quantities in the International System of Quantities (ISQ)
on which the International System of Units (SI) is based. The term “time” instead of
“duration” is often used in this context and also for an infinitesimal duration.

For the term “duration”, expressions such as “time” or “time interval” are often used,
but the term “time” is not recommended in this sense and the term “time interval” is
deprecated in this sense to avoid confusion with the concept of “time interval”.

The exact duration of a [time scale unit](#time_scale_unit) depends on the
[time scale](#time_scale) used. For example, the durations of a year, month, week,
day, hour or minute, may depend on when they occur (in a Gregorian calendar, a
calendar month can have a duration of 28, 29, 30, or 31 days; in a 24-hour clock, a
clock minute can have a duration of 59, 60, or 61 seconds, etc.). Therefore,
the exact duration can only be evaluated if the exact duration of each is known.

# `error_reason`

```elixir
@type error_reason() :: Exception.t()
```

The error payload returned inside `{:error, reason}` tuples.

As of v0.21, every originating error site in Tempo returns an
`Exception`-conforming struct (one of the types under
`lib/tempo/exception/`), mirroring the convention in Localize
and Calendrical. The `atom() | binary()` members are retained
transiently for backward compatibility during the migration
and will be removed once all callers are updated.

# `extended_info`

```elixir
@type extended_info() :: %{
  calendar: atom() | nil,
  zone_id: String.t() | nil,
  zone_offset: integer() | nil,
  tags: %{optional(String.t()) =&gt; [String.t()]}
}
```

Extended information parsed from an IXDTF suffix.

* `:calendar` — calendar identifier atom derived from `u-ca=`.

* `:zone_id` — IANA time zone name such as `"Europe/Paris"`.

* `:zone_offset` — numeric offset in minutes from `[+HH:MM]`.

* `:tags` — map of non-`u-ca` elective tagged suffixes.

# `qualification`

```elixir
@type qualification() :: :uncertain | :approximate | :uncertain_and_approximate | nil
```

ISO 8601-2 / EDTF date qualification.

* `:uncertain` — the value is uncertain (`?`).

* `:approximate` — the value is approximate (`~`), e.g. "circa".

* `:uncertain_and_approximate` — both (`%`).

* `nil` when no qualification was supplied.

# `qualifications`

```elixir
@type qualifications() :: %{optional(atom()) =&gt; qualification()} | nil
```

Per-component qualifications parsed from an EDTF Level 2 date.

A map from the component unit (`:year`, `:month`, `:day`) to its
qualification atom. `nil` when no component-level qualification
was present in the parsed string.

# `t`

```elixir
@type t() :: %Tempo{
  calendar: Calendar.calendar() | nil,
  extended: extended_info() | nil,
  qualification: qualification(),
  qualifications: qualifications(),
  shift: time_shift(),
  time: token_list()
}
```

# `time_shift`

```elixir
@type time_shift() :: [hour: integer(), minute: integer()] | nil
```

# `time_unit`

```elixir
@type time_unit() :: :year | :month | :week | :day | :hour | :minute | :second
```

# `token`

```elixir
@type token() :: integer() | list() | tuple()
```

# `token_list`

```elixir
@type token_list() :: [
  year: token(),
  month: token(),
  week: token(),
  day: token(),
  day_of_year: token(),
  day_of_week: token() | [integer()],
  hour: token(),
  minute: token(),
  second: token()
]
```

# `adjacent?`

`true` when the two intervals touch at a single boundary
(Allen's `:meets | :met_by`). See `Tempo.Interval.adjacent?/2`.

# `after?`

`true` when `a` starts strictly after `b` ends (Allen's
`:preceded_by`). See `Tempo.Interval.after?/2`.

# `anchor`

```elixir
@spec anchor(t(), t()) :: t() | {:error, error_reason()}
```

Combine a date-like value with a time-of-day value into a
datetime.

This is **axis composition**, not a set operation. Set
operations require both operands to share an anchor class;
`anchor/2` is how the user explicitly composes cross-axis
values before set operations run. No set-algebra laws apply
to `anchor/2` — it's a constructor, not an operator.

### Arguments

* `anchored` is an anchored `t:Elixir.Tempo.t/0` (has a year
  component) — typically a date like `~o"2026-01-04"`.

* `non_anchored` is a non-anchored `t:Elixir.Tempo.t/0` (pure
  time-of-day) — typically a time like `~o"T10:30"`.

### Returns

* A new `t:t/0` combining the two. The date components come
  from `anchored`; the time components come from `non_anchored`.

### Raises

* `ArgumentError` when either argument has the wrong anchor
  class — if `anchored` is non-anchored or `non_anchored` is
  already anchored.

### Examples

    iex> Tempo.anchor(~o"2026-01-04", ~o"T10:30")
    ~o"2026Y1M4DT10H30M"

# `anchored?`

```elixir
@spec anchored?(tempo :: t()) :: boolean()
```

Returns a boolean indicating if a `t:Tempo.t/0` struct
is anchored to the timeline.

Anchored means that the time representation contains
enough information for it to be located in a single
location on the timeline.  In practise this means the
if the tempo struct has a `:year` value then
it is anchored.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

### Returns

* `true` or `false`

### Examples

    iex> Tempo.anchored? ~o"2022"
    true

    iex> Tempo.anchored? ~o"2M"
    false

# `at_least?`

`true` when the interval is at least as long as the given
duration. See `Tempo.Interval.at_least?/2`.

# `at_most?`

`true` when the interval is at most as long as the given
duration. See `Tempo.Interval.at_most?/2`.

# `at_resolution`

```elixir
@spec at_resolution(tempo :: t(), target_unit :: time_unit()) ::
  t() | {:error, error_reason()}
```

Return a Tempo at the specified resolution, dispatching to
`trunc/2` or `extend_resolution/2` based on whether `target_unit`
is coarser or finer than the current resolution.

This is the unified entry point for normalising resolution. It
is idempotent when `target_unit` matches the current resolution.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

* `target_unit` is any time unit atom (`:year`, `:month`,
  `:day`, `:hour`, `:minute`, `:second`, …).

### Returns

* The Tempo at the requested resolution, or

* `{:error, reason}`.

### Examples

    iex> Tempo.at_resolution(~o"2020Y", :day)
    ~o"2020Y1M1D"

    iex> Tempo.at_resolution(~o"2020Y6M15DT10H", :day)
    ~o"2020Y6M15D"

    iex> Tempo.at_resolution(~o"2020Y6M15D", :day)
    ~o"2020Y6M15D"

# `before?`

`true` when `a` ends strictly before `b` starts (Allen's
`:precedes`). See `Tempo.Interval.before?/2`.

# `beginning_of_day`

```elixir
@spec beginning_of_day(t()) :: t() | {:error, error_reason()}
```

Return a second-resolution `t:t/0` at the start (`00:00:00`) of
the day that contains `tempo`.

Preserves the input's calendar, shift, and zone metadata so that
beginning-of-day in `[Europe/Paris]` still names midnight Paris
time, not midnight UTC.

### Arguments

* `tempo` is a `t:t/0` with at least year/month/day components.

### Returns

* A second-resolution `t:t/0`.

### Examples

    iex> Tempo.beginning_of_day(~o"2026-06-15T14:30:00")
    ~o"2026Y6M15DT0H0M0S"

    iex> Tempo.beginning_of_day(~o"2026-06-15")
    ~o"2026Y6M15DT0H0M0S"

# `beginning_of_month`

```elixir
@spec beginning_of_month(t()) :: t() | {:error, error_reason()}
```

Return a second-resolution `t:t/0` at the start of the month
(`YYYY-MM-01T00:00:00`) that contains `tempo`.

### Arguments

* `tempo` is a `t:t/0` with at least year/month components.

### Returns

* A second-resolution `t:t/0`.

### Examples

    iex> Tempo.beginning_of_month(~o"2026-06-15T14:30:00")
    ~o"2026Y6M1DT0H0M0S"

    iex> Tempo.beginning_of_month(~o"2026-06")
    ~o"2026Y6M1DT0H0M0S"

# `bounded?`

`true` when both endpoints of the interval are concrete
(neither `:undefined` nor `nil`). See `Tempo.Interval.bounded?/1`.

# `complement`

Complement of a Tempo value within a bounding universe. The
`:bound` option is required. See `Tempo.Operations.complement/2`.

# `contains?`

`true` when every instant of `b` is also in `a`. Alias for
`subset?(b, a, opts)`. See `Tempo.Operations.contains?/3`.

# `day`

```elixir
@spec day(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `day` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.day(~o"2026-06-15T10:30:45")
    15

    iex> Tempo.day(~o"2026")
    nil

# `day_of_week`

```elixir
@spec day_of_week(t(), atom()) :: 1..7
```

Return the day of the week as an integer (`1..7`) using the
Tempo value's calendar.

For the Gregorian calendar with `:default` ordering, `1` is
Monday and `7` is Sunday — matching `Date.day_of_week/1`.

### Arguments

* `tempo` is a `t:t/0` anchored with at least year/month/day
  components.

* `starting_on` is a day-of-week atom controlling which day is
  numbered `1`. Accepts `:default` (calendar's default),
  `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`,
  `:saturday`, or `:sunday`. Defaults to `:default`.

### Returns

* Integer `1..7`.

### Raises

* `ArgumentError` when `tempo` has no date components.

### Examples

    iex> Tempo.day_of_week(~o"2026-06-15")
    1

    iex> Tempo.day_of_week(~o"2026-06-15", :sunday)
    2

# `day_of_year`

```elixir
@spec day_of_year(t()) :: pos_integer()
```

Return the 1-based ordinal day of the year (`1..365` or `1..366`
in a leap year) using the Tempo value's calendar.

### Arguments

* `tempo` is a `t:t/0` anchored with at least year/month/day
  components.

### Returns

* A positive integer.

### Raises

* `ArgumentError` when `tempo` has no date components.

### Examples

    iex> Tempo.day_of_year(~o"2026-01-01")
    1

    iex> Tempo.day_of_year(~o"2024-12-31")
    366

# `days_in_month`

```elixir
@spec days_in_month(t()) :: pos_integer()
```

Return the number of days in the Tempo's month under its
calendar.

### Arguments

* `tempo` is a `t:t/0` with year and month components.

### Returns

* A positive integer.

### Raises

* `ArgumentError` when `tempo` has no year or no month
  component.

### Examples

    iex> Tempo.days_in_month(~o"2024-02-15")
    29

    iex> Tempo.days_in_month(~o"2025-02")
    28

    iex> Tempo.days_in_month(~o"2026-04")
    30

# `difference`

Difference `a \ b` — every instant in `a` that is not in
`b`. Each result interval is the trimmed remainder; `a`
members can split into multiple fragments. See
`Tempo.Operations.difference/3`.

# `disjoint?`

`true` when `a` and `b` share no instants.
See `Tempo.Operations.disjoint?/3`.

# `duration`

Return the interval's length as a `%Tempo.Duration{}`, or
`:infinity` for unbounded intervals. See `Tempo.Interval.duration/1`.

# `during?`

`true` when `a` is strictly inside `b` (Allen's `:during`).
See `Tempo.Interval.during?/2`.

# `empty?`

`true` when the interval has zero length. See
`Tempo.Interval.empty?/1`.

# `end_of_day`

```elixir
@spec end_of_day(t()) :: t() | {:error, error_reason()}
```

Return a second-resolution `t:t/0` at the **exclusive** end of
the day that contains `tempo` — i.e. `00:00:00` of the following
day.

Tempo follows the half-open `[from, to)` convention everywhere,
so `end_of_day/1` returns the upper bound at which the day ends
and the next day begins. This is the right argument for
interval construction — pairing `beginning_of_day/1` and
`end_of_day/1` gives you the 24-hour (or DST-adjusted) window.

### Arguments

* `tempo` is a `t:t/0` with at least year/month/day components.

### Returns

* A second-resolution `t:t/0`.

### Examples

    iex> Tempo.end_of_day(~o"2026-06-15T14:30:00")
    ~o"2026Y6M16DT0H0M0S"

    iex> Tempo.end_of_day(~o"2026-12-31")
    ~o"2027Y1M1DT0H0M0S"

# `end_of_month`

```elixir
@spec end_of_month(t()) :: t() | {:error, error_reason()}
```

Return a second-resolution `t:t/0` at the **exclusive** end of
the month that contains `tempo` — i.e. the first day of the
following month at `00:00:00`.

Half-open by design; see `end_of_day/1` for the rationale.

### Arguments

* `tempo` is a `t:t/0` with at least year/month components.

### Returns

* A second-resolution `t:t/0`.

### Examples

    iex> Tempo.end_of_month(~o"2026-06-15")
    ~o"2026Y7M1DT0H0M0S"

    iex> Tempo.end_of_month(~o"2026-12")
    ~o"2027Y1M1DT0H0M0S"

# `equal?`

`true` when `a` and `b` span the same instants (at their
aligned resolution). See `Tempo.Operations.equal?/3`.

# `exactly?`

`true` when the interval's length equals the given duration.
See `Tempo.Interval.exactly?/2`.

# `explain`

```elixir
@spec explain(term()) :: String.t()
```

Return a multi-line prose explanation of any Tempo value —
what it is, what it spans, and how to work with it.

Returns a plain string suitable for iex. For structured output
that renderers can style (ANSI, HTML, visualizer components),
use `Tempo.Explain.explain/1` directly and pick a formatter.

# `extend`

Adds an extended enumeration to a Tempo.

This has the effect of increasing the
resolution of the the Tempo struct but
still covering the same interval.

### Example

    iex> Tempo.extend(~o"2020")
    {:ok, ~o"2020Y{1..12}M"}

# `extend!`

# `extend_resolution`

```elixir
@spec extend_resolution(tempo :: t(), target_unit :: time_unit()) ::
  t() | {:error, error_reason()}
```

Extend a Tempo's resolution by padding finer units with their
start-of-unit minimum values.

`extend_resolution/2` is the scalar counterpart to `extend/2`:
where `extend/2` adds an implicit enumeration (turning `~o"2020Y"`
into `~o"2020Y{1..12}M"` — a range), `extend_resolution/2` fills
in concrete minimums (turning `~o"2020Y"` into `~o"2020Y1M1D"`
when extended to `:day`). This is the operation needed to align
resolutions before interval comparison.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

* `target_unit` is the finer resolution to pad to. Must be
  finer than or equal to `tempo`'s current resolution.

### Returns

* The padded `t:t/0`, or

* `{:error, reason}` when `target_unit` is coarser than the
  current resolution (use `trunc/2` for that direction) or when
  no path exists from the current unit to `target_unit` under
  the tempo's calendar.

### Examples

    iex> Tempo.extend_resolution(~o"2020Y", :day)
    ~o"2020Y1M1D"

    iex> Tempo.extend_resolution(~o"2020Y6M", :hour)
    ~o"2020Y6M1DT0H"

# `from_date`

```elixir
@spec from_date(date :: Date.t()) :: t()
```

Creates a `t:Tempo.t/0` struct from a `t:Date.t/0`.

### Arguments

* `date` is any `t:Date.t/0`.

### Returns

* `t:t/0` or

* `{:error, reason}`

### Examples

    iex> Tempo.from_date ~D[2022-11-20]
    ~o"2022Y11M20D"

# `from_date_time`

```elixir
@spec from_date_time(DateTime.t()) :: t()
```

Creates a `t:Tempo.t/0` struct from a `t:DateTime.t/0`.

The DateTime's time zone information is preserved on the Tempo:
the total offset (`utc_offset + std_offset`) populates the
`:shift` field, and the IANA zone identifier is stored on the
`:extended` map under `:zone_id`. Iteration on the returned
Tempo carries both pieces of metadata through.

### Arguments

* `date_time` is any `t:DateTime.t/0`.

### Returns

* `t:t/0`.

### Examples

    iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).time
    [year: 2022, month: 11, day: 20, hour: 10, minute: 37, second: 0]

    iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).shift
    [hour: 0]

    iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).extended.zone_id
    "Etc/UTC"

# `from_elixir`

```elixir
@spec from_elixir(
  value :: Date.t() | Time.t() | NaiveDateTime.t() | DateTime.t(),
  options :: Keyword.t()
) :: t() | {:error, error_reason()}
```

Create a `t:Tempo.t/0` from any Elixir date/time type.

Unifies `Date.t`, `Time.t`, `NaiveDateTime.t`, and `DateTime.t`
into the single `Tempo.t` representation under the principle
that every date/time value is a bounded interval on the time
line at some resolution.

The intended resolution is either given explicitly via the
`:resolution` option or inferred from the input:

* `Date.t` → `:day` (Date has no time components).

* `Time.t`, `NaiveDateTime.t`, `DateTime.t` → the finest
  Tempo-supported component that is non-zero. If all time
  components are zero (e.g. midnight on a date), the resolution
  falls back to `:day` for datetime types or `:hour` for a bare
  `Time.t`. Microsecond is discarded (Tempo does not yet model
  sub-second resolution).

When an explicit `:resolution` is given, the resulting Tempo is
passed through `at_resolution/2` to either truncate or pad to
that resolution.

### Arguments

* `value` is any `t:Date.t/0`, `t:Time.t/0`,
  `t:NaiveDateTime.t/0`, or `t:DateTime.t/0`.

### Options

* `:resolution` is a time unit atom (`:year`, `:month`, `:day`,
  `:hour`, `:minute`, `:second`) overriding the inferred
  resolution.

### Returns

* The `t:t/0` at the chosen resolution, or

* `{:error, reason}` if `:resolution` is incompatible with the
  input.

### Examples

    iex> Tempo.from_elixir(~D[2022-06-15])
    ~o"2022Y6M15D"

    iex> Tempo.from_elixir(~T[10:30:00])
    ~o"T10H30M"

    iex> Tempo.from_elixir(~N[2022-06-15 10:30:00])
    ~o"2022Y6M15DT10H30M"

    iex> Tempo.from_elixir(~N[2022-06-15 00:00:00])
    ~o"2022Y6M15D"

    iex> Tempo.from_elixir(~D[2022-06-15], resolution: :hour)
    ~o"2022Y6M15DT0H"

    iex> Tempo.from_elixir(~N[2022-06-15 10:30:00], resolution: :day)
    ~o"2022Y6M15D"

# `from_iso8601`

```elixir
@spec from_iso8601(string :: String.t()) ::
  {:ok,
   t()
   | Tempo.Interval.t()
   | Tempo.Duration.t()
   | Tempo.Set.t()
   | Tempo.Range.t()}
  | {:error, error_reason()}
```

Creates a `t:Tempo.t/0` struct from an ISO 8601 or IXDTF
string.

The parser supports the vast majority of [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)
parts 1 and 2 as well as the Internet Extended Date/Time Format
(IXDTF) defined in
[draft-ietf-sedate-datetime-extended-09](https://www.ietf.org/archive/id/draft-ietf-sedate-datetime-extended-09.html).

An IXDTF suffix follows the ISO 8601 production and consists of
an optional time zone (`[Europe/Paris]` or `[+08:45]`) followed
by zero or more tagged suffixes (`[u-ca=hebrew]`, `[_key=value]`).
Any bracket may be prefixed with `!` to mark it critical —
unrecognised critical suffixes cause the parse to fail; elective
suffixes are retained verbatim under `extended.tags`.

### Arguments

* `string` is any ISO 8601 formatted string, optionally
  followed by an IXDTF suffix.

* `calendar` (optional) is any `t:Calendar.calendar/0`. When
  passed, the explicit calendar always wins over any
  `[u-ca=NAME]` tag in the IXDTF suffix. When omitted, the
  `[u-ca=NAME]` tag is resolved to a `Calendrical.*` module via
  `Calendrical.calendar_from_cldr_calendar_type/1`; if no tag is present,
  `Calendrical.Gregorian` is used.

### Returns

* `{:ok, t}` where the returned struct's `:extended` field
  is populated when an IXDTF suffix was parsed, or `nil`
  otherwise.

* `{:error, reason}` when the string cannot be parsed or a
  critical IXDTF suffix is unrecognised.

## Examples

    iex> Tempo.from_iso8601("2022-11-20")
    {:ok, ~o"2022Y11M20D"}

    iex> Tempo.from_iso8601("2022Y")
    {:ok, ~o"2022Y"}

    iex> {:error, %Tempo.ParseError{}} = Tempo.from_iso8601("invalid")

    iex> {:ok, tempo} = Tempo.from_iso8601("5786-10-30[u-ca=hebrew]")
    iex> tempo.calendar
    Calendrical.Hebrew

    iex> {:ok, tempo} = Tempo.from_iso8601("2022-11-20T10:30:00Z[Europe/Paris][u-ca=hebrew]")
    iex> {tempo.extended.zone_id, tempo.extended.calendar, tempo.calendar}
    {"Europe/Paris", :hebrew, Calendrical.Hebrew}

    iex> {:error, %Tempo.UnknownZoneError{zone_id: "Continent/Imaginary"}} =
    ...>   Tempo.from_iso8601("2022-11-20T10:30:00Z[!Continent/Imaginary]")

# `from_iso8601`

```elixir
@spec from_iso8601(string :: String.t(), calendar :: Calendar.calendar()) ::
  {:ok,
   t()
   | Tempo.Interval.t()
   | Tempo.Duration.t()
   | Tempo.Set.t()
   | Tempo.Range.t()}
  | {:error, error_reason()}
```

# `from_iso8601!`

```elixir
@spec from_iso8601!(string :: String.t()) :: t() | no_return()
```

Creates a `t:Tempo.t/0` struct from an ISO8601
string.

The parser supports the vast majority of [ISO8601](https://www.iso.org/iso-8601-date-and-time-format.html)
parts 1 and 2.

### Arguments

* `string` is any ISO8601 formatted string

* `calendar` is any `t:Calendar.calendar/0`. The default is
  `Calendrical.Gregorian`.

### Returns

* `t:t/0` or

* raises an exception

## Examples

    iex> Tempo.from_iso8601!("2022-11-20")
    ~o"2022Y11M20D"

    iex> Tempo.from_iso8601!("2022Y")
    ~o"2022Y"

# `from_iso8601!`

```elixir
@spec from_iso8601!(string :: String.t(), calendar :: Calendar.calendar()) ::
  t() | no_return()
```

# `from_naive_date_time`

```elixir
@spec from_naive_date_time(naive_date_time :: NaiveDateTime.t()) :: t()
```

Creates a `t:Tempo.t/0` struct from a `t:NaiveDateTime.t/0`.

### Arguments

* `naive_date_time` is any `t:NaiveDateTime.t/0`.

### Returns

* `t:t/0` or

* `{:error, reason}`

### Examples

    iex> Tempo.from_naive_date_time ~N[2022-11-20 10:37:00]
    ~o"2022Y11M20DT10H37M0S"

# `from_time`

```elixir
@spec from_time(time :: Time.t()) :: t()
```

Creates a `t:Tempo.t/0` struct from a `t:Time.t/0`.

### Arguments

* `time` is any `t:Time.t/0`.

### Returns

* `t:t/0` or

* `{:error, reason}`

### Examples

    iex> Tempo.from_time ~T[10:09:00]
    ~o"T10H9M0S"

# `hour`

```elixir
@spec hour(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `hour` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.hour(~o"2026-06-15T10:30:45")
    10

    iex> Tempo.hour(~o"2026")
    nil

# `intersection`

Intersection of two Tempo values — every instant in both
operands. Each result interval is the trimmed overlap; `a`
members can split into multiple fragments. See
`Tempo.Operations.intersection/3`.

# `leap_year?`

```elixir
@spec leap_year?(t()) :: boolean()
```

Return `true` when the Tempo's year is a leap year under its
calendar.

### Arguments

* `tempo` is a `t:t/0` with at least a year component.

### Returns

* `true` or `false`.

### Raises

* `ArgumentError` when `tempo` has no year component.

### Examples

    iex> Tempo.leap_year?(~o"2024")
    true

    iex> Tempo.leap_year?(~o"2025")
    false

# `longer_than?`

`true` when the interval is strictly longer than the given
duration. See `Tempo.Interval.longer_than?/2`.

# `meets?`

`true` when `a`'s end coincides exactly with `b`'s start
(Allen's `:meets`). See `Tempo.Interval.meets?/2`.

# `members_in_exactly_one`

Member-preserving symmetric-difference filter — members of
either operand that don't overlap any member of the other,
kept whole. See `Tempo.Operations.members_in_exactly_one/3`.

# `members_outside`

Member-preserving anti-overlap filter — returns the whole
members of `a` that do NOT overlap any member of `b`, kept
whole with their original metadata. Use this when the
question is about *which events* survive the filter (e.g.
"which workdays aren't holidays?"). See
`Tempo.Operations.members_outside/3`.

# `members_overlapping`

Member-preserving overlap filter — returns the whole members of
`a` that overlap any member of `b`, with their original
metadata. Use this when the question is about *which events*
hit the query window. See `Tempo.Operations.members_overlapping/3`.

# `merge`

# `minute`

```elixir
@spec minute(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `minute` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.minute(~o"2026-06-15T10:30:45")
    30

    iex> Tempo.minute(~o"2026")
    nil

# `month`

```elixir
@spec month(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `month` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.month(~o"2026-06-15T10:30:45")
    6

    iex> Tempo.month(~o"2026")
    nil

# `new`

```elixir
@spec new(keyword()) :: {:ok, t()} | {:error, error_reason()}
```

Construct a `t:Tempo.t/0` from a keyword list of time-scale
components and options.

The companion to `~o` sigils and `Tempo.from_iso8601/1`: where the
sigil is ideal for literal values and `from_iso8601/1` for already-
formatted strings, `new/1` is the right constructor when you have
structured data at runtime — form inputs, database rows, API
payloads, test fixtures.

Components can be passed in any order; `new/1` reorders them
coarse-to-fine (year → month → day → hour → minute → second)
before building the struct.

Axis coherence is enforced: the Gregorian axis (`:month`, `:day`),
the ISO-week axis (`:week`, `:day_of_week`), and the ordinal axis
(`:day_of_year`) are mutually exclusive.

### Arguments

* `components` is a keyword list mixing time-scale components
  and options. At least one time-scale component must be present.

### Time-scale components

Every component value must be an integer.

* `:year` is the calendar year.

* `:month` is the calendar month (Gregorian axis).

* `:week` is the ISO week number (ISO-week axis).

* `:day` is the day of month (Gregorian axis).

* `:day_of_year` is the ordinal day within the year (ordinal axis).

* `:day_of_week` is the ISO day-of-week number (ISO-week axis).

* `:hour` is the clock hour `0..23`.

* `:minute` is the clock minute `0..59`.

* `:second` is the clock second `0..59` (or `60` on a leap-second date).

### Options

* `:calendar` is the `Calendrical` calendar module used to
  interpret and validate the components. Defaults to
  `Calendrical.Gregorian`.

* `:zone` is an IANA time-zone name as a binary (e.g.
  `"Australia/Sydney"`). Sets `extended.zone_id`. Requires at
  least one of `:hour`, `:minute`, `:second` to be present —
  a zoned value without a time of day has no UTC projection.

* `:shift` is a manual UTC offset expressed as `[hour: n]` or
  `[hour: n, minute: m]`.

* `:qualification` marks the value's EDTF qualification.
  One of `:uncertain`, `:approximate`, or
  `:uncertain_and_approximate`.

* `:metadata` is a free-form map attached to `extended.tags`.

### Returns

* `{:ok, t()}` on success.

* `{:error, reason}` when components are missing, have
  non-integer values, mix axes, or name a non-existent zone.

### Examples

    iex> {:ok, tempo} = Tempo.new(year: 2026, month: 6, day: 15)
    iex> tempo.time
    [year: 2026, month: 6, day: 15]

    iex> {:ok, tempo} = Tempo.new(day: 15, month: 6, year: 2026)
    iex> tempo.time
    [year: 2026, month: 6, day: 15]

    iex> {:ok, meeting} = Tempo.new(year: 2026, month: 6, day: 15, hour: 14, minute: 30)
    iex> meeting.time
    [year: 2026, month: 6, day: 15, hour: 14, minute: 30]

    iex> {:ok, ww} = Tempo.new(year: 2026, week: 24, day_of_week: 3)
    iex> ww.time
    [year: 2026, week: 24, day_of_week: 3]

    iex> {:error, _} = Tempo.new(year: 2026, month: 13)

    iex> {:error, _} = Tempo.new(year: 2026, month: 6, week: 24)

# `new!`

```elixir
@spec new!(keyword()) :: t()
```

Bang variant of `new/1` — raises on invalid input.

### Examples

    iex> Tempo.new!(year: 2026, month: 6, day: 15).time
    [year: 2026, month: 6, day: 15]

# `now`

```elixir
@spec now(String.t()) :: t()
```

Return the current time in the given IANA time zone as a
second-resolution `t:t/0`.

Reads from the configured clock (as `utc_now/0` does) and shifts
the result into `zone`. The returned Tempo's wall-clock time is
the zone-local reading of the current UTC instant.

### Arguments

* `zone` is an IANA time zone name (e.g. `"Europe/Paris"`,
  `"America/New_York"`). Defaults to `"Etc/UTC"`, in which case
  `now/1` is equivalent to `utc_now/0`.

### Returns

* A `t:t/0` at second resolution with `extended.zone_id: zone`.

### Examples

    iex> tempo = Tempo.now("Europe/London")
    iex> tempo.extended.zone_id
    "Europe/London"

    iex> Tempo.now("Etc/UTC").extended.zone_id
    "Etc/UTC"

# `overlaps?`

`true` when `a` and `b` share at least one instant.
See `Tempo.Operations.overlaps?/3`.

# `quarter_of_year`

```elixir
@spec quarter_of_year(t()) :: 1..4
```

Return the 1-based quarter of the year (`1..4`) for Gregorian-like
calendars.

### Arguments

* `tempo` is a `t:t/0` anchored with at least year/month
  components.

### Returns

* An integer `1..4`.

### Raises

* `ArgumentError` when `tempo` has no year/month components.

### Examples

    iex> Tempo.quarter_of_year(~o"2026-01-15")
    1

    iex> Tempo.quarter_of_year(~o"2026-11-30")
    4

# `relation`

Classify the Allen interval-algebra relation between two
interval-like values.

Thin delegate to `Tempo.Interval.relation/2` — see that
function's docs for the full table of 13 relations.

Named `relation` (not `compare`) because it returns one of 13
Allen relations rather than stdlib's ternary `:lt | :eq | :gt`
— using `compare` would invite the wrong mental model at the
call site.

Use `Tempo.IntervalSet.relation_matrix/2` when both operands
are multi-member sets and you want the per-pair breakdown.

### Examples

    iex> Tempo.relation(~o"2026-06-15", ~o"2026-06-16")
    :meets

    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.relation(a, b)
    :overlaps

# `resolution`

```elixir
@spec resolution(tempo :: t()) :: {time_unit(), time_unit() | non_neg_integer()}
```

Returns the resolution of a `t:Tempo.t/0` struct.

The resolution is the smallest time unit of the
struct and an appropriate scale.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

### Returns

* `{time_unit, scale}`

### Examples

    iex> Tempo.resolution ~o"2022"
    {:year, 1}

    iex> Tempo.resolution ~o"2022-11"
    {:month, 1}

    iex> Tempo.resolution ~o"2022-11-20"
    {:day, 1}

    iex> Tempo.resolution ~o"2022Y1M2G3DU"
    {:day, 3}

# `round`

```elixir
@spec round(tempo :: t(), round_to :: time_unit()) :: t() | {:error, error_reason()}
```

Truncates a tempo struct to the specified resolution.

Rounding rounds to the specified time unit resolution.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

* `round_to` is any time unit. The default
  is `:day`.

### Returns

* `rounded` is a tempo struct that is rounded or

* `{:error, reason}`

### Examples

    iex> Tempo.round ~o"2022-11-21", :day
    ~o"2022Y11M21D"

    iex> Tempo.round ~o"2022-11-21", :month
    ~o"2022Y12M"

    iex> Tempo.round ~o"2022-11-21", :year
    ~o"2023Y"

# `second`

```elixir
@spec second(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `second` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.second(~o"2026-06-15T10:30:45")
    45

    iex> Tempo.second(~o"2026")
    nil

# `select`

Narrow a Tempo span by a selector — the composition primitive
for "workdays of June", "the 15th of every month", and similar
queries.

`Tempo.select/2` is a **pure function**: the selector is a value,
not an ambient configuration. Locale-dependent constraints are
constructed by `Tempo.workdays/1` and `Tempo.weekend/1` and
composed in at the call site.

See `Tempo.Select` for the full vocabulary.

### Examples

    iex> {:ok, set} = Tempo.select(~o"2026-02", [1, 15])
    iex> set |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
    [1, 15]

    iex> {:ok, set} = Tempo.select(~o"2026", ~o"12-25")
    iex> [xmas] = Tempo.IntervalSet.to_list(set)
    iex> {xmas.from.time[:year], xmas.from.time[:month], xmas.from.time[:day]}
    {2026, 12, 25}

# `shift`

```elixir
@spec shift(
  t(),
  keyword()
) :: t()
```

Shift a `t:t/0` by a keyword list of signed unit amounts,
returning a new `t:t/0`.

This is the ergonomic companion to `Tempo.Math.add/2` — the
Duration-based API remains the principled path (durations carry
their own calendar and leap-second semantics), but for ad-hoc
shifts a keyword list reads more naturally.

Units are applied largest-to-smallest with the standard
month-end clamping rule (e.g. `~o"2024-01-31" + 1 month` is
`2024-02-29`, not `2024-03-02`).

### Arguments

* `tempo` is any `t:t/0`.

* `units` is a keyword list of `{unit, amount}` pairs such as
  `[month: 1, day: -5]` or `[year: 2]`. Valid units: `:year`,
  `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`.
  Amounts may be negative.

### Returns

* The shifted `t:t/0`.

### Examples

    iex> Tempo.shift(~o"2026-06-15", month: 1, day: -5)
    ~o"2026Y7M10D"

    iex> Tempo.shift(~o"2026-01-31", month: 1)
    ~o"2026Y2M28D"

    iex> Tempo.shift(~o"2026-06-15T10:00:00", hour: -3)
    ~o"2026Y6M15DT7H0M0S"

# `shift_zone`

```elixir
@spec shift_zone(t(), String.t()) :: {:ok, t()} | {:error, error_reason()}
```

Project a zoned or UTC-anchored Tempo into another IANA time
zone, preserving the UTC instant.

The returned Tempo names the same point on the time line, but the
wall-clock reading is the one an observer in `target_zone` would
see. This is the stdlib analogue of `DateTime.shift_zone/2`: in
Tempo it routes through `Tempo.Compare.to_utc_seconds/1` so zone
rules are re-evaluated from Tzdata at call time.

### Arguments

* `tempo` is a `t:t/0` that carries zone information — either an
  IANA zone on `extended.zone_id`, a numeric `zone_offset`, or a
  `shift` keyword list. A floating Tempo (no zone info) cannot be
  projected because its UTC instant is undefined.

* `target_zone` is an IANA zone name (`"Europe/Paris"`,
  `"America/New_York"`, `"Etc/UTC"`, …).

### Returns

* `{:ok, tempo}` at second resolution in `target_zone`, or

* `{:error, reason}` when `tempo` is not zoned or `target_zone`
  is unknown to Tzdata.

### Examples

    iex> paris = Tempo.from_iso8601!("2026-06-15T14:00:00[Europe/Paris]")
    iex> {:ok, new_york} = Tempo.shift_zone(paris, "America/New_York")
    iex> new_york.extended.zone_id
    "America/New_York"
    iex> Keyword.take(new_york.time, [:hour, :minute])
    [hour: 8, minute: 0]

# `shorter_than?`

`true` when the interval is strictly shorter than the given
duration. See `Tempo.Interval.shorter_than?/2`.

# `split`

Split a tempo struct into a date
and time.

# `subset?`

`true` when every instant of `a` is also in `b`.
See `Tempo.Operations.subset?/3`.

# `symmetric_difference`

Symmetric difference `a △ b` — instants in exactly one of
the two operands. Trimmed/instant-level. See
`Tempo.Operations.symmetric_difference/3`.

# `to_calendar`

# `to_date`

Convert a Tempo struct into a Date.

Accepts the three single-day Tempo shapes:

* Calendar date — `[year: Y, month: M, day: D]`.

* Ordinal date — `[year: Y, day: DDD]`. When the Tempo carries
  only year and day (no month), the day is interpreted as
  day-of-year per ISO 8601-2 §4.3.4. `D` and `O` both parse to
  the same `:day` key, so `~o"2020-166"`, `~o"2020Y166O"`, and
  `~o"2020Y166D"` all convert correctly.

* ISO week date — `[year: Y, week: W, day_of_week: K]`.

### Returns

* `{:ok, %Date{}}` on success.

* `{:error, reason}` when the Tempo covers a span rather than a
  single day, or the components don't form a valid date.

### Examples

    iex> {:ok, date} = Tempo.to_date(~o"2020-06-15")
    iex> date
    ~D[2020-06-15]

    iex> {:ok, date} = Tempo.to_date(~o"2020-166")
    iex> date
    ~D[2020-06-14]

    iex> {:ok, date} = Tempo.to_date(~o"2020-W24-3")
    iex> date
    ~D[2020-06-10]

# `to_interval`

```elixir
@spec to_interval(
  t()
  | Tempo.Interval.t()
  | Tempo.IntervalSet.t()
  | Tempo.Set.t()
  | Tempo.Duration.t(),
  keyword()
) ::
  {:ok, Tempo.Interval.t() | Tempo.IntervalSet.t()} | {:error, error_reason()}
```

Convert an implicit-span `t:Elixir.Tempo.t/0` into the
equivalent explicit `t:Tempo.Interval.t/0` or
`t:Tempo.IntervalSet.t/0`.

Every Tempo value represents a bounded interval on the time
line. `~o"2026-01"` *is* the interval `[2026-01-01, 2026-02-01)`
— `to_interval/1` materialises that implicit span as a pair of
concrete endpoints under the half-open `[from, to)` convention
(`from` inclusive, `to` exclusive). This is the canonical
representation used by the upcoming set-operations API
(`union/2`, `intersection/2`, `coalesce/1`).

When the input expands to multiple disjoint spans — a set of
explicit values, a range over a time unit, a stepped range —
the result is a `%Tempo.IntervalSet{}` with intervals sorted
and coalesced. The conversion is idempotent on values that are
already explicit.

### Arguments

* `value` is a `t:Elixir.Tempo.t/0`, `t:Tempo.Interval.t/0`,
  `t:Tempo.IntervalSet.t/0`, or `t:Tempo.Set.t/0`.

### Options

* `:bound` is a Tempo value whose upper endpoint limits
  expansion of an unbounded recurrence (`recurrence: :infinity`
  with no `UNTIL`). Required to materialise such rules;
  ignored otherwise.

* `:coalesce` controls whether the resulting IntervalSet
  merges adjacent or overlapping intervals (`true`, the
  default) or preserves each expanded occurrence as a
  distinct interval (`false`). Expansion consumers that care
  about event identity — `Tempo.ICal`, the RRULE expander —
  pass `false`; ordinary implicit-span materialisation uses
  the default.

### Returns

* `{:ok, interval}` when the value materialises to a single
  contiguous span.

* `{:ok, interval_set}` when the value expands to multiple
  disjoint spans.

* `{:error, reason}` when the input cannot be materialised — a
  bare `Tempo.Duration` (no anchor), a `Tempo` already at its
  finest resolution (no finer unit to bound the span), a
  one-of `Tempo.Set` (epistemic disjunction is not an interval
  list; the user must pick one or handle the disjunction
  themselves), or an unbounded recurrence with no `:bound`.

### Examples

    iex> {:ok, tempo} = Tempo.from_iso8601("2026-01")
    iex> {:ok, interval} = Tempo.to_interval(tempo)
    iex> interval.from.time
    [year: 2026, month: 1, day: 1]
    iex> interval.to.time
    [year: 2026, month: 2, day: 1]

    iex> {:ok, tempo} = Tempo.from_iso8601("156X")
    iex> {:ok, interval} = Tempo.to_interval(tempo)
    iex> {interval.from.time, interval.to.time}
    {[year: 1560], [year: 1570]}

    iex> {:ok, duration} = Tempo.from_iso8601("P3M")
    iex> {:error, %Tempo.MaterialisationError{reason: :bare_duration}} = Tempo.to_interval(duration)

# `to_interval!`

```elixir
@spec to_interval!(
  t()
  | Tempo.Interval.t()
  | Tempo.IntervalSet.t()
  | Tempo.Set.t()
  | Tempo.Duration.t()
) :: Tempo.Interval.t() | Tempo.IntervalSet.t()
```

Raising version of `to_interval/1`.

### Arguments

* `value` is a `t:Elixir.Tempo.t/0`, `t:Tempo.Interval.t/0`,
  `t:Tempo.IntervalSet.t/0`, or `t:Tempo.Set.t/0`.

### Returns

* The materialised `t:Tempo.Interval.t/0` or
  `t:Tempo.IntervalSet.t/0`.

### Raises

* `ArgumentError` when the input cannot be materialised. See
  `to_interval/1` for the error cases.

### Examples

    iex> {:ok, tempo} = Tempo.from_iso8601("2026")
    iex> interval = Tempo.to_interval!(tempo)
    iex> {interval.from.time, interval.to.time}
    {[year: 2026, month: 1], [year: 2027, month: 1]}

# `to_interval_set`

```elixir
@spec to_interval_set(
  t()
  | Tempo.Interval.t()
  | Tempo.IntervalSet.t()
  | Tempo.Set.t()
  | Tempo.Duration.t()
) :: {:ok, Tempo.IntervalSet.t()} | {:error, error_reason()}
```

Convert any Tempo value to a `t:Tempo.IntervalSet.t/0`.

Unlike `to_interval/1` (which may return either a single
interval or an IntervalSet), `to_interval_set/1` always returns
an IntervalSet — wrapping a single interval in a one-element
set if needed. This is the convenient form when the caller
wants a uniform shape (e.g. to pipe into set operations).

### Arguments

* `value` is a `t:Elixir.Tempo.t/0`, `t:Tempo.Interval.t/0`,
  `t:Tempo.IntervalSet.t/0`, or `t:Tempo.Set.t/0`.

### Returns

* `{:ok, interval_set}` on success, or `{:error, reason}` for
  the same cases that `to_interval/1` errors on.

### Examples

    iex> {:ok, tempo} = Tempo.from_iso8601("2026-01")
    iex> {:ok, set} = Tempo.to_interval_set(tempo)
    iex> length(set.intervals)
    1

# `to_iso8601`

```elixir
@spec to_iso8601(t() | Tempo.Interval.t() | Tempo.Duration.t() | Tempo.Set.t()) ::
  String.t()
```

Encode a Tempo value back into an ISO 8601-2 string.

The output uses the explicit-suffix form (`2022Y11M20D`), which
is a valid ISO 8601-2 / EDTF representation that round-trips
cleanly through `from_iso8601/1`. Constructs that exist only
in ISO 8601-2 Part 2 (seasons, groups, selections,
uncertainty qualifiers, unspecified digits) are preserved in
their explicit form.

IXDTF suffixes (`[Europe/Paris]`, `[u-ca=hebrew]`) are **not**
emitted by this function — the `:extended` field is currently
ignored. Round-trip of IXDTF-enriched values is a future
extension.

### Arguments

* `value` is a `t:Tempo.t/0`, `t:Tempo.Interval.t/0`,
  `t:Tempo.Duration.t/0`, or `t:Tempo.Set.t/0`.

### Returns

* An ISO 8601-2 binary that parses back to the same AST.

### Examples

    iex> Tempo.from_iso8601!("2022-11-20") |> Tempo.to_iso8601()
    "2022Y11M20D"

    iex> Tempo.from_iso8601!("R5/2022-01-01/P1M") |> Tempo.to_iso8601()
    "R5/2022Y1M1D/P1M"

    iex> {:ok, i} = Tempo.from_iso8601("1984?/2004~")
    iex> Tempo.to_iso8601(i)
    "1984Y?/2004Y~"

# `to_naive_date_time`

Convert a Tempo struct into a NaiveDateTime.

# `to_relative_string`

```elixir
@spec to_relative_string(
  t() | Tempo.Interval.t(),
  keyword()
) :: String.t()
```

Format a Tempo as a locale-aware relative time string like
`"3 hours ago"` or `"in 2 days"`.

Routes through Localize's CLDR `relativeTime` patterns. The
reference point ("now") comes from `Tempo.utc_now/0` unless
overridden with the `:from` option — which makes this safe to
use in tests via `Tempo.Clock.Test`.

For intervals, the `:from` endpoint of the interval is used as
the target — "the meeting starts in 2 hours" rather than
"lasts 2 hours" (for duration phrasing, use `Tempo.to_string/2`
on a `Tempo.Duration`).

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`. The value
  must be anchored (have a year component); non-anchored values
  raise `Tempo.NonAnchoredError`.

### Options

* `:from` is a `t:t/0` — the reference point the output is
  relative to. Defaults to `Tempo.utc_now/0`.

* `:unit` forces the output unit (`:second`, `:minute`,
  `:hour`, `:day`, `:week`, `:month`, `:year`). Omit to let
  Localize auto-derive.

* `:format` is `:standard`, `:narrow`, or `:short`. Defaults to
  `:standard`.

* `:locale` is a CLDR locale. Defaults to Localize's configured
  default.

### Returns

* A `t:String.t/0`.

### Examples

    iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
    iex> Tempo.to_relative_string(~o"2026-06-14T12:00:00Z", from: now)
    "yesterday"

    iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
    iex> Tempo.to_relative_string(~o"2026-06-15T15:00:00Z", from: now)
    "in 3 hours"

    iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
    iex> Tempo.to_relative_string(~o"2026-06-10T12:00:00Z", from: now)
    "5 days ago"

# `to_rrule`

```elixir
@spec to_rrule(Tempo.Interval.t() | term()) ::
  {:ok, String.t()} | {:error, Tempo.ConversionError.t()}
```

Encode a `t:Tempo.Interval.t/0` into an RFC 5545 RRULE string.

The output does **not** include the leading `RRULE:` prefix,
nor a `DTSTART` property — RRULE is a recurrence pattern, not
a full iCalendar record. Callers wanting the full record
prepend `DTSTART` themselves using the interval's `:from`
field.

### Supported inputs

* A `%Tempo.Interval{}` with a single-unit `%Tempo.Duration{}`
  cadence. Supported units: `:second`, `:minute`, `:hour`,
  `:day`, `:week`, `:month`, `:year`.

* `:recurrence` of `:infinity` (no COUNT), a positive integer
  (COUNT), or `1` combined with `:to` (UNTIL).

* `:repeat_rule` of `nil`, or a `%Tempo{}` whose `:time` holds a
  single `{:selection, [...]}` entry. Selection entries for
  `:month`, `:day` (→ BYMONTHDAY), `:day_of_year`, `:week`,
  `:hour`, `:minute`, `:second`, and the paired
  `:day_of_week`/`:instance` (→ BYDAY with optional ordinals)
  are encoded directly.

### Returns

* `{:ok, rrule_string}` on success.

* `{:error, reason}` when the interval cannot be expressed as
  an RRULE (e.g. multi-unit duration, unsupported selection
  entry).

### Examples

    iex> {:ok, i} = Tempo.RRule.parse("FREQ=DAILY;COUNT=10")
    iex> Tempo.to_rrule(i)
    {:ok, "COUNT=10;FREQ=DAILY"}

    iex> {:ok, i} = Tempo.RRule.parse("FREQ=YEARLY;BYMONTH=11;BYDAY=4TH")
    iex> Tempo.to_rrule(i)
    {:ok, "FREQ=YEARLY;BYMONTH=11;BYDAY=4TH"}

    iex> {:error, %Tempo.ConversionError{}} =
    ...>   Tempo.to_rrule(Tempo.from_iso8601!("2022-06-15"))

# `to_rrule!`

```elixir
@spec to_rrule!(Tempo.Interval.t()) :: String.t() | no_return()
```

Bang variant of `to_rrule/1`.

### Returns

* The RRULE string on success.

* Raises `Tempo.ConversionError` otherwise.

# `to_string`

```elixir
@spec to_string(
  t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Duration.t(),
  keyword()
) :: String.t()
```

Format a Tempo value as a locale-aware string.

Routes through Localize so format patterns, month and weekday
names, day periods, and punctuation all follow CLDR data for
the chosen locale. The default format is keyed off the Tempo's
resolution — a year-only value renders as `"2026"`, a month
value as `"Jun 2026"`, a day value as `"Jun 15, 2026"`, and so
on.

`Tempo.to_string/1,2` is the end-user display function.
`inspect/1` remains the programmer-facing form and returns the
`~o"…"` sigil representation unchanged.

### Arguments

* `value` is a `t:t/0`, `t:Tempo.Interval.t/0`, or
  `t:Tempo.IntervalSet.t/0`.

### Options

* `:format` is a CLDR format atom (`:short | :medium | :long |
  :full`), a skeleton atom (`:yMMM`, `:yMMMd`, `:hm`, …), or a
  pattern string. Defaults to a resolution-appropriate choice
  (see the module doc of `Tempo.Format` for the table).

* `:locale` is a CLDR locale identifier such as `"en"`,
  `"en-GB"`, or `"de"`. Defaults to Localize's configured
  default locale.

* Any other option accepted by `Localize.Date.to_string/2`,
  `Localize.Time.to_string/2`, `Localize.DateTime.to_string/2`,
  or `Localize.Interval.to_string/3` is forwarded verbatim.

### Returns

* A `t:String.t/0`.

### Raises

* Any exception Localize raises for invalid locales, missing
  CLDR data, or unresolvable format skeletons.

### Examples

    iex> Tempo.to_string(~o"2026")
    "Jan – Dec 2026"

    iex> Tempo.to_string(~o"2026-06")
    "Jun 1 – 30, 2026"

    iex> Tempo.to_string(~o"2026-06-15")
    "Jun 15, 2026"

    iex> Tempo.to_string(~o"2026-06-15", format: :long)
    "June 15, 2026"

    iex> Tempo.to_string(~o"2026", format: :long)
    "January – December 2026"

    iex> Tempo.to_string(~o"P1Y6M")
    "1 year and 6 months"

    iex> Tempo.to_string(~o"P3DT2H", style: :short)
    "3 days and 2 hr"

# `to_time`

Convert a Tempo struct into a Time.

# `today`

```elixir
@spec today(String.t()) :: t() | {:error, error_reason()}
```

Return today's date in the given IANA time zone as a
day-resolution `t:t/0`.

"Today" is zone-relative: at 11pm New York on the 14th it is
already the 15th in Paris. This function answers the zone-local
question.

### Arguments

* `zone` is an IANA time zone name. Defaults to `"Etc/UTC"`.

### Returns

* A `t:t/0` at day resolution whose wall date is the date in
  `zone` at the current UTC instant.

### Examples

    iex> Tempo.today("Etc/UTC") |> Tempo.resolution()
    {:day, 1}

# `trunc`

```elixir
@spec trunc(tempo :: t(), truncate_to :: time_unit()) ::
  t() | {:error, error_reason()}
```

Truncates a tempo struct to the specified resolution.

Truncation removes the time units that have a
higher resolution than the specified `truncate_to`
option.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

* `truncate_to` is any time unit. The default
  is `:day`.

### Returns

* `truncated` is a tempo struct that is truncated or

* `{:error, reason}`

### Examples

    iex> Tempo.trunc ~o"2022-11-21T09:30:00"
    ~o"2022Y11M21D"

    iex> Tempo.trunc ~o"2022-11-21T09:30:00", :minute
    ~o"2022Y11M21DT9H30M"

    iex> Tempo.trunc ~o"2022-11-21T09:30:00", :year
    ~o"2022Y"

# `union`

Union of two Tempo values — every instant in either operand.
See `Tempo.Operations.union/3` for full details.

# `unit_min_max`

```elixir
@spec unit_min_max(tempo :: t() | token_list()) :: {time_unit(), time_unit()}
```

Returns the maximum and minimum time units as a
2-tuple.

### Arguments

* `tempo` is any `t:Elixir.Tempo.t/0`.

### Returns

* `{max_unit, min_unit}`

### Examples

      iex> Tempo.unit_min_max ~o"2022Y1M2G3DU"
      {:day, :year}

      iex> Tempo.unit_min_max ~o"2022"
      {:year, :year}

# `utc_now`

```elixir
@spec utc_now() :: t()
```

Return the current UTC time as a second-resolution `t:t/0`
anchored in `Etc/UTC`.

Reads from the clock configured under `:ex_tempo, :clock`,
defaulting to `Tempo.Clock.System`. Tests that need determinism
should configure `Tempo.Clock.Test` — see its module doc.

### Returns

* A `t:t/0` at second resolution with `shift: [hour: 0]` and
  `extended.zone_id: "Etc/UTC"`.

### Examples

    iex> tempo = Tempo.utc_now()
    iex> tempo.extended.zone_id
    "Etc/UTC"

    iex> Tempo.utc_now() |> Tempo.resolution()
    {:second, 1}

# `utc_today`

```elixir
@spec utc_today() :: t() | {:error, error_reason()}
```

Return today's date in UTC as a day-resolution `t:t/0`.

### Returns

* A `t:t/0` at day resolution anchored in `Etc/UTC`.

### Examples

    iex> Tempo.utc_today() |> Tempo.resolution()
    {:day, 1}

# `weekend`

```elixir
@spec weekend(Tempo.Territory.input()) :: t()
```

Return a selector that matches the weekend days of a territory.

Different territories weekend on different days: the United
States is `[Saturday, Sunday]`, Saudi Arabia is `[Friday,
Saturday]`, India is `[Sunday]`. `Tempo.weekend/1` reads that
definition from CLDR via Localize and returns it as a
composable selector.

### Arguments

* `territory` is an atom, string, locale, or `%Localize.LanguageTag{}`
  resolved through `Tempo.Territory.resolve/1`. Defaults to `nil`,
  which walks the territory-resolution chain.

### Returns

* A `t:Tempo.t/0` value carrying a `day_of_week` list. Composable
  directly with `Tempo.select/2`.

### Examples

    iex> {:ok, us} = Tempo.select(~o"2026-02", Tempo.weekend(:US))
    iex> us |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
    [1, 7, 8, 14, 15, 21, 22, 28]

    iex> {:ok, sa} = Tempo.select(~o"2026-02", Tempo.weekend(:SA))
    iex> sa |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
    [6, 7, 13, 14, 20, 21, 27, 28]

# `within?`

`true` when `a` fits inside `b` inclusive of shared
endpoints. The canonical "does this fit inside that window?"
predicate. See `Tempo.Interval.within?/2`.

# `workdays`

```elixir
@spec workdays(Tempo.Territory.input()) :: t()
```

Return a selector that matches the workdays of a territory —
the days of week that are *not* in that territory's weekend.

Together, `workdays/1` and `weekend/1` partition the seven days
of the week: `workdays(:US) ++ weekend(:US)` spans Monday..Sunday.

### Arguments

* `territory` is an atom, string, locale, or `%Localize.LanguageTag{}`
  resolved through `Tempo.Territory.resolve/1`. Defaults to `nil`,
  which walks the territory-resolution chain (app config, then
  ambient locale).

### Returns

* A `t:Tempo.t/0` value carrying a `day_of_week` list. Composable
  directly with `Tempo.select/2`.

### Examples

    iex> {:ok, set} = Tempo.select(~o"2026-02", Tempo.workdays(:US))
    iex> Tempo.IntervalSet.count(set)
    20

    iex> Tempo.workdays(:US).time
    [day_of_week: [1, 2, 3, 4, 5]]

# `year`

```elixir
@spec year(t() | Tempo.Interval.t()) :: integer() | nil
```

Return the `year` component of a Tempo value, or `nil` if
the value doesn't specify one.

The accessors (`year/1`, `month/1`, `day/1`, `hour/1`,
`minute/1`, `second/1`) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.

### Arguments

* `value` is a `t:t/0` or `t:Tempo.Interval.t/0`.

### Returns

* The component value as an integer when unambiguous.

* `nil` when the value doesn't specify that unit (e.g.
  `Tempo.day(~o"2026")` returns `nil` — the year value has no
  day).

* Raises `ArgumentError` when called on an interval whose span
  covers multiple values of that unit (e.g. `Tempo.day/1` on a
  month-spanning interval is ambiguous).

### Examples

    iex> Tempo.year(~o"2026-06-15T10:30:45")
    2026

    iex> Tempo.year(~o"2026")
    2026

---

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