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

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.

# `interval_like`

```elixir
@type interval_like() :: Tempo.t() | t() | Tempo.IntervalSet.t()
```

Anything `relation/2` can reduce to a single bounded interval.

# `relation`

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

# `t`

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

# `adjacent?`

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

# `after?`

```elixir
@spec after?(interval_like(), interval_like()) :: boolean()
```

`true` when `a` starts strictly after `b` ends, with a gap
(Allen's `:preceded_by`).

# `at_least?`

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

# `at_most?`

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

# `before?`

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

# `bounded?`

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

`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

# `duration`

```elixir
@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` — when `true`, adds one second to the
  returned duration for each IERS leap-second insertion that
  falls inside `[from, to)`. Defaults to `false` so behaviour
  matches `DateTime`, `Time`, and `:calendar` from Elixir/OTP
  (none of which count leap seconds). See
  `Tempo.Interval.spans_leap_second?/1` and
  `leap_seconds_spanned/1` for 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"

# `during?`

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

# `empty?`

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

`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

# `endpoints`

```elixir
@spec endpoints(t()) :: {Tempo.t() | :undefined, Tempo.t() | :undefined}
```

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

* `interval` is a `t:t/0`.

### Returns

* `{from, to}` where each endpoint is a `t:Tempo.t/0` or
  `:undefined` for 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}

# `exactly?`

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

# `from`

```elixir
@spec from(t()) :: Tempo.t() | :undefined
```

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

* `interval` is a `t:t/0`.

### Returns

* The `from` endpoint as a `t:Tempo.t/0` or `:undefined` for
  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

# `inverse_relation`

```elixir
@spec inverse_relation(relation()) :: relation()
```

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

# `leap_seconds_spanned`

```elixir
@spec leap_seconds_spanned(t()) :: [{integer(), 1..12, 1..31}]
```

Return the list of IERS leap-second dates that fall inside
`[from, to)`.

### Arguments

* `interval` is a `t:t/0` with both endpoints present.

### Returns

* A list of `{year, month, day}` tuples, each entry drawn from
  `Tempo.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}]

# `longer_than?`

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

# `meets?`

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

# `metadata`

```elixir
@spec metadata(t()) :: map()
```

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

* `interval` is a `t:t/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)
    %{}

# `new`

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

Construct a `t: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

* `options` is a keyword list of construction options
  (see below).

### Options

* `:from` is a `t:Tempo.t/0` or the atom `:undefined`
  (open start).

* `:to` is a `t:Tempo.t/0` or the atom `:undefined`
  (open end).

* `:duration` is a `t:Tempo.Duration.t/0`. When combined with
  `:from`, the `:to` endpoint is derived lazily by
  `Tempo.to_interval/1`.

* `:recurrence` is a `pos_integer()` or `:infinity`.

* `:repeat_rule` is a `t:Tempo.RRule.Rule.t/0` or `t:Tempo.t/0`.

* `:metadata` is a free-form map carried through set operations.

### Returns

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

* `{:error, reason}` when endpoints are invalid, `:from` is 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

# `new!`

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

Bang variant of `new/1`. Raises on invalid input.

# `next_unit_boundary`

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.

# `relation`

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

* `a` and `b` are each one of:

  * a `t:Tempo.t/0` point (materialised via its implicit span).

  * a `t:Tempo.Interval.t/0`.

  * a `t:Tempo.IntervalSet.t/0` with 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 use `Tempo.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

# `resolution`

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

* `interval` is a `t:t/0`. Must be bounded (both endpoints
  present) — `:undefined` endpoints return `:undefined`.

### Returns

* A unit atom (`:year`, `:month`, `:day`, `:hour`, `:minute`,
  `:second`, …), or `:undefined` for 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

# `shorter_than?`

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

# `spans_leap_second?`

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

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

* `interval` is a `t:t/0` with both endpoints present. Unbounded
  intervals always return `false` (open-ended to `:undefined`)
  and pre-Unix-era intervals pre-1972 return `false` (IERS leap
  seconds started in 1972).

### Returns

* `true` when at least one entry from `Tempo.LeapSeconds.dates/0`
  falls inside `[from, to)`.

* `false` otherwise.

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

# `to`

```elixir
@spec to(t()) :: Tempo.t() | :undefined
```

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

* `interval` is a `t:t/0`.

### Returns

* The `to` endpoint as a `t:Tempo.t/0` or `:undefined` for
  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

# `within?`

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

---

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