# `Ash.Type.Range`
[🔗](https://github.com/ash-project/ash/blob/v3.36.1/lib/ash/type/range.ex#L5)

A range of values of an inner type, held as an `Ash.Range`.

    attribute :stay, Ash.Type.Range, constraints: [inner_type: :date]

### Inner types

| inner type | precision | `resolution` | without a `resolution` |
| --- | --- | --- | --- |
| `:integer` | `1` | a positive integer | cast to `[)` |
| `:date` | a day | a `Duration` of days or longer | cast to `[)` |
| `:naive_datetime` | a second | a `Duration` | kept as given |
| `:datetime` | a second, or a microsecond with `precision: :microsecond` | a `Duration` | kept as given |

The precision is the smallest difference between two values. Cast to inclusive-exclusive,
`[)`, `[1, 3]` becomes `[1, 4)`; kept as given, `(a, b]` stays `(a, b]`. With a
`resolution`, every range is cast to `[)`.

### Constraints

* `:inner_type` - Required. The type of the range's values: one of `:integer`, `:date`, `:naive_datetime` or `:datetime`, or an `Ash.Type.NewType` of one, such as `:utc_datetime_usec`. Values are ordered the same way in Elixir and in the data layer. Each has a precision: see Inner types, above.

* `:inner_constraints` (`t:keyword/0`) - Constraints of the inner type, applied to each bound, limit and anchor. The default value is `[]`.

* `:lower` (`t:keyword/0`) - Constraints on the range's lower bound, which is `nil` when the range is unbounded below. The default value is `[]`.

  * `:required?` (`t:boolean/0`) - The range must not be unbounded below. The default value is `false`.

  * `:inclusive?` (`t:boolean/0`) - `true` requires an inclusive lower bound, `false` an exclusive one. A range unbounded below passes either. An `:integer` or `:date` range, or one with a `resolution`, is cast to `[)`, so only `true` is allowed.

  * `:limit` (`t:term/0`) - The least value the lower bound may take: `limit: ~D[2025-07-01]` keeps a range from starting before 1 July 2025. The range cannot then be unbounded below. Written as the lower bound, `:start` is this limit, or unbounded without one, so `[:start, :end)` is every value between the limits. With a `resolution`, it must be on the grid.

* `:upper` (`t:keyword/0`) - Constraints on the range's upper bound, which is `nil` when the range is unbounded above. The default value is `[]`.

  * `:required?` (`t:boolean/0`) - The range must not be unbounded above. The default value is `false`.

  * `:inclusive?` (`t:boolean/0`) - `true` requires an inclusive upper bound, `false` an exclusive one. A range unbounded above passes either. An `:integer` or `:date` range, or one with a `resolution`, is cast to `[)`, so only `false` is allowed.

  * `:limit` (`t:term/0`) - The greatest value the upper bound may take: `limit: ~D[2026-07-01]` keeps a range from ending after 1 July 2026. An inclusive upper bound must be below it. The range cannot then be unbounded above. Written as the upper bound, `:end` is this limit, or unbounded without one. With a `resolution`, it must be on the grid.

* `:allow_empty?` (`t:boolean/0`) - Allow an empty range, one holding no values (see `Ash.Range.empty/0`). The default value is `false`.

* `:resolution` (`t:term/0`) - The length of a period: a positive `Duration`, or a positive integer for an `:integer` range. `Duration.new!(week: 2)` divides a `:date` range into fortnights. It is a whole number of the precision, in months and years or in weeks and shorter, not both.  
  The periods' boundaries are the grid, and the range's bounds and limits must be on it. A range with a resolution is cast to `[)`. Without one, a period is one unit of the precision.

* `:anchor` (`t:term/0`) - A value on the grid, aligning the periods, within the limits or not: `~D[2025-06-23]` starts fortnights on that Monday. Without one, the grid starts at `0` or at `0001-01-01T00:00:00Z`, so weeks start on Monday. Required for a resolution in months or years, on the 28th of a month or earlier.  
  A `DateTime` in a time zone aligns periods of a day or longer to its time of day in that zone, so a day is 23, 24 or 25 hours long. Its time of day must fall in no gap or overlap of the zone.

### PostgreSQL

`ash_postgres` stores a range as a native range: `:integer` as `int8range`, `:date` as
`daterange`, `:naive_datetime` as `tsrange` and `:datetime` as `tstzrange`.

# `base_type`

```elixir
@spec base_type(Ash.Type.t()) :: Ash.Type.t()
```

The inner type, looking through any `Ash.Type.NewType`.

# `handle_change?`

# `period`

```elixir
@spec period(term(), Keyword.t()) :: {:ok, Ash.Range.t()} | {:error, String.t()}
```

Returns the period containing `value`. Takes constraints from `Ash.Type.init/2`.

    iex> {:ok, constraints} =
    ...>   Ash.Type.init(Ash.Type.Range,
    ...>     inner_type: :date,
    ...>     resolution: Duration.new!(week: 2),
    ...>     anchor: ~D[2025-06-23]
    ...>   )
    iex> Ash.Type.Range.period(~D[2025-06-25], constraints)
    {:ok, %Ash.Range{lower: ~D[2025-06-23], upper: ~D[2025-07-07], bounds: :"[)"}}

# `prepare_change?`

# `resolution`

```elixir
@spec resolution(Keyword.t()) :: Duration.t() | pos_integer()
```

Returns the length of a period: the `resolution`, or one unit of the precision without
one. Takes constraints from `Ash.Type.init/2`.

    iex> {:ok, constraints} = Ash.Type.init(Ash.Type.Range, inner_type: :utc_datetime)
    iex> Ash.Type.Range.resolution(constraints)
    %Duration{second: 1}

---

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