# `Tempo.Ecto.Interval`
[🔗](https://github.com/elixir-tempo/tempo_sql/blob/v0.4.0/lib/tempo/ecto/interval.ex#L2)

Ecto.ParameterizedType for persisting an interval as a
PostgreSQL `tstzrange` value, or as a `tsrange` value when it is
floating.

It takes any expression that is one span — an interval, or a value
(`~o"2026-06"`) as the interval it names — and loads an interval.

## Usage

    schema "meetings" do
      field :window, Tempo.Ecto.Interval
    end

and in the migration:

    add :window, :tstzrange

(or use `Tempo.SQL.Migration.add_interval/2`).

A field of floating values (with no zone and no time shift, such as
a shop's opening hours wherever it is) is declared on the floating
frame, and stored in a `tsrange`:

    field :opening_hours, Tempo.Ecto.Interval, frame: :floating

    add :opening_hours, :tsrange

## Options

  * `:resolution` — on load, truncate both endpoints to the
    given component and drop all sub-components. One of
    `:year`, `:month`, `:day`, `:hour`, `:minute`, or
    `:second`. Defaults to `:second` (no truncation). See
    the storage contract guide for the semantics and
    caveats.

        field :reporting_period, Tempo.Ecto.Interval, resolution: :year

  * `:frame` — the line the column's instants lie on. `:utc`
    (the default) stores a value in a zone or at a time shift as
    the UTC instants it is, in a `tstzrange`; `:floating` stores a
    value with neither, in a `tsrange`. A value of the other frame
    is refused by `cast/2`, as a changeset error that names the
    frame to declare, and by `dump/3`.

## Storage contract

This type refuses to store values it cannot round-trip
semantically. The `dump` callback returns `:error` for:

  * A value of the other frame: a floating value for a column on
    the UTC line, or a value in a zone or at a time shift for a
    floating column.

  * An expression of several spans: a recurrence, a set, or a
    value naming several (`~o"2026Y{6,8}M"`). Store those as
    `Tempo.Ecto.IntervalSet` instead.

  * A qualification (`:uncertain`, `:approximate`) or a calendar
    other than the Gregorian, which a range of instants cannot
    keep. `Tempo.Ecto.TempoRange` keeps both.

  * An interval unbounded on both sides. One open end is stored
    as a `(,b)` or `[a,)` range in Postgres.

## Bracket normalisation on load

PostgreSQL does not canonicalise `tstzrange` values on
output — a column populated by another writer might hold
`[a, b]`, `(a, b)`, or `(a, b]`. The loader normalises any
non-half-open range to `[lower, upper)`
by shifting the offending endpoint one second (Tempo is
second-resolution so this is exact).

# `cast_type`

```elixir
@spec cast_type(keyword()) :: Ecto.Type.t()
```

Builds the parameterized-type tuple for use outside a schema.

Inside a schema, `field :window, Tempo.Ecto.Interval, resolution: :day`
does this for you. Reach for this when calling `Ecto.Type.cast/2`
and friends directly.

### Arguments

* `options` is a keyword list of options.

### Options

* `:resolution` truncates both endpoints on load to the given
  component. One of `:year`, `:month`, `:day`, `:hour`, `:minute`,
  or `:second`. Defaults to `:second`.

* `:frame` is `:utc` (the default) for a `tstzrange` column or
  `:floating` for a `tsrange` column.

### Returns

* A `t:Ecto.ParameterizedType.opts/0` tuple.

### Examples

    iex> Tempo.Ecto.Interval.cast_type()
    {:parameterized, {Tempo.Ecto.Interval, %{frame: :utc, resolution: :second}}}

    iex> Tempo.Ecto.Interval.cast_type(resolution: :year, frame: :floating)
    {:parameterized, {Tempo.Ecto.Interval, %{frame: :floating, resolution: :year}}}

---

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