# `Localize.Interval`
[🔗](https://github.com/elixir-localize/localize/blob/v1.4.0/lib/localize/interval.ex#L1)

Formats date and time intervals as localized strings.

Interval formats produce strings like "Jan 10 – 12, 2008" from
two dates, rather than repeating "Jan 10, 2008 – Jan 12, 2008".
The format is selected based on the greatest calendar field
difference between the start and end values.

An interval is formatted with the formats of its endpoints' calendar: its interval patterns, its date and time formats, and the date-time pattern joining a date to a time range. Both endpoints must therefore be in the same calendar.

An interval writes the fields its format names and no others, as TR35's interval algorithm has it. Two values alike in every field the format writes are written once; two that differ in a field it writes take CLDR's interval pattern for their greatest difference; and where there is no such pattern, as where they differ in a year, a month or an era the format does not write, each is written with the format around the locale's fallback pattern. An era is a field like any other: `format: :GyMMMd` writes "Dec 31, 1 BC – Jan 1, 1 AD", and a format without an era writes none.

The two values are written in the order the locale's interval fallback pattern states, which TR35 makes the order of every interval pattern: the earlier first in every locale but one of CLDR 49. `kek`'s Gregorian pattern is "{1} – {0}", so it writes the later value first.

# `greatest_difference`

```elixir
@spec greatest_difference(map(), map()) ::
  {:ok, :y | :M | :d | :H | :m} | {:error, Exception.t()}
```

Returns the greatest calendar field difference between
two dates or datetimes.

### Arguments

* `from` is a date or datetime map.

* `to` is a date or datetime map.

### Returns

* `{:ok, field}` where field is `:y`, `:M`, `:d`, `:H`, or `:m`.

* `{:error, %Localize.NoPracticalDifferenceError{}}` if the values
  are equal at every field considered.

### Examples

    iex> Localize.Interval.greatest_difference(~D[2022-04-22], ~D[2022-04-27])
    {:ok, :d}

    iex> Localize.Interval.greatest_difference(~D[2021-12-31], ~D[2022-01-01])
    {:ok, :y}

# `known_fields`

```elixir
@spec known_fields() :: %{
  month: %{short: :M, medium: :MMM, long: :MMM, full: :MMM},
  month_and_day: %{short: :Md, medium: :MMMd, long: :MMMEd, full: :MMMEd},
  year_and_month: %{short: :yM, medium: :yMMM, long: :yMMMM, full: :yMMMM}
}
```

Returns the locale-independent skeletons for the `:fields` option of `to_string/3`.

Only the non-default `:fields` selections (`:month`,
`:month_and_day`, `:year_and_month`) appear here, because only
those are locale-independent. The default `:date` selection is
resolved per-locale, mirroring `Localize.Date.to_string/2`'s
`:format` → skeleton mapping for that locale, so it has no fixed
entry to list.

### Returns

* A map keyed by field selection (`:month`, `:month_and_day`,
  `:year_and_month`), each value a map of `:format`
  (`:short`, `:medium`, `:long`, `:full`) to the CLDR
  skeleton atom used for that combination.

### Examples

    iex> Localize.Interval.known_fields()
    %{
      month: %{short: :M, full: :MMM, long: :MMM, medium: :MMM},
      month_and_day: %{short: :Md, full: :MMMEd, long: :MMMEd, medium: :MMMd},
      year_and_month: %{short: :yM, full: :yMMMM, long: :yMMMM, medium: :yMMM}
    }

# `parse`

```elixir
@spec parse(String.t() | {String.t(), String.t()}, Keyword.t()) ::
  {:ok, Date.Range.t() | {map(), map()}} | {:error, Exception.t()}
```

Parses a localized date interval.

The inverse of `to_string/3`. Accepts either a single string, such as `"May 5 – 10, 2026"`, or a 2-tuple `{from_string, to_string}` for two-input UIs that already have the endpoints split.

A single string is read with the locale's interval formats, in which the two dates share the fields written once, and otherwise cut where the locale's fallback pattern or a separator people write, a dash or "to", has a date on each side, each read as `Localize.Date.parse/2` reads it. The spaces about an interval format's dash may be left out or put in. Two dates written without a year are of the reference date's year, as a date alone is, and the later is of the year after where it would otherwise come before the earlier: "Dec 28 – Jan 3" ends in the January that follows. Two weeks written without a year are read the same way by their week-based years: "Sun (week: 53) – Mon (week: 1)" ends the day after it begins.

The result is a `t:Date.Range.t/0` whose endpoints share the calendar named by the `:calendar` option, which defaults to `Calendar.ISO`.

### Arguments

* `input` is either a binary or a `{from_binary, to_binary}` tuple.

* `options` is a keyword list of options.

### Options

Same as `Localize.Date.parse/2` — `:locale`, `:calendar`, `:format`,
`:reference_date`, `:as`. As there, `:calendar` is a calendar module and
the endpoints are returned in it. `:format` is the interval's own format, as `to_string/3`
takes it, and the text is read as two dates are written at it: with the
patterns of the format's interval format, in which the two share the
fields written once ("Jun 16 – 20, 2026" at `:medium`); as two dates each
written whole with the format, either side of the locale's separator; and
as one date, which two dates alike in every field the format writes are
written as, read as the range of that date alone. No pattern of another
format is tried. Plus:

* `:allow_inverted` is a boolean. When `true`, an end-before-start
  interval is returned as-is, since `Date.range/3` builds a descending
  range. When `false`, the default, an inverted interval is rejected
  with a `t:Localize.DateRangeParseError.t/0`. Applies only when
  `as: :struct`, the default; `as: :map` skips the comparison because
  partial maps may not carry enough fields to compare.

### Returns

* `{:ok, range}`, a `t:Date.Range.t/0`, when `as: :struct`, or

* `{:ok, {from_map, to_map}}` when `as: :map`. Each endpoint is a field
  map; missing fields are inherited from the other endpoint per the
  CLDR interval convention, so `"May 5 – May 10, 2026"` yields two maps
  both carrying `:year`. A field given a value no date has is an error,
  never inherited, or

* `{:error, exception}`, a `t:Localize.DateParseError.t/0` or
  `t:Localize.DateRangeParseError.t/0`, on failure, or a
  `t:Localize.InvalidValueError.t/0` if an endpoint is not a string or
  an option is malformed.

### Examples

    iex> {:ok, range} = Localize.Interval.parse({"2026-05-05", "2026-05-10"})
    iex> {range.first, range.last}
    {~D[2026-05-05], ~D[2026-05-10]}

    iex> Localize.Interval.parse("May 5 – May 10, 2026", locale: :en, as: :map)
    {:ok,
     {%{calendar: Calendar.ISO, year: 2026, month: 5, day: 5},
      %{calendar: Calendar.ISO, year: 2026, month: 5, day: 10}}}

    iex> {:ok, range} = Localize.Interval.parse("May 5, 2026 – May 10, 2026", locale: :en)
    iex> {range.first, range.last}
    {~D[2026-05-05], ~D[2026-05-10]}

    iex> Localize.Interval.parse("May 5–10, 2026", locale: :en)
    {:ok, Date.range(~D[2026-05-05], ~D[2026-05-10])}

    iex> Localize.Interval.parse("May 5 – 10", locale: :en, reference_date: ~D[2026-01-01])
    {:ok, Date.range(~D[2026-05-05], ~D[2026-05-10])}

# `split_interval`

```elixir
@spec split_interval(term()) :: {:ok, [String.t()]} | {:error, Exception.t()}
```

Splits an interval format string into `[left, right]` halves
at the point where a format character repeats.

### Arguments

* `interval` is an interval format pattern string.

### Returns

* `{:ok, [left, right]}` where `left` and `right` are the two
  halves of the interval pattern.

* `{:error, exception}` if the pattern is malformed, has no
  repeating field, or is not a string.

### Examples

    iex> Localize.Interval.split_interval("MMM d – d")
    {:ok, ["MMM d – ", "d"]}

# `to_parts`

```elixir
@spec to_parts(map(), map(), Keyword.t()) ::
  {:ok, [%{type: atom(), value: String.t(), source: atom()}]}
  | {:error, Exception.t()}
```

Formats a date, time, or datetime interval into typed parts, mirroring ECMA-402's `formatRangeToParts`.

The parts concatenate to exactly the string `to_string/3` produces with the same options. Every part carries a `:source` key: `:start_range` for parts of the interval start, `:end_range` for parts of the interval end, and `:shared` for the separators between them. When the endpoints have no practical difference the single formatted value carries source `:shared` throughout.

Unlike `to_string/3`, open intervals (a `nil` endpoint) are not supported — both endpoints are required, matching the JS API.

### Arguments

* `from` is a `Date`, `Time`, `DateTime`, `NaiveDateTime`, or compatible map for the interval start.

* `to` is a value of the same kind for the interval end.

* `options` is a keyword list of options. See `to_string/3`.

### Returns

* `{:ok, parts}` where `parts` is a list of `%{type: atom(), value: String.t(), source: atom()}` maps.

* `{:error, exception}` on failure or when an endpoint is `nil`.

### Examples

    iex> Localize.Interval.to_parts(~D[2022-04-22], ~D[2022-04-25], locale: :en)
    {:ok,
     [
       %{type: :month, value: "Apr", source: :start_range},
       %{type: :literal, value: " ", source: :start_range},
       %{type: :day, value: "22", source: :start_range},
       %{type: :literal, value: " – ", source: :shared},
       %{type: :day, value: "25", source: :end_range},
       %{type: :literal, value: ", ", source: :end_range},
       %{type: :year, value: "2022", source: :end_range}
     ]}

# `to_parts!`

```elixir
@spec to_parts!(map(), map(), Keyword.t()) :: [
  %{type: atom(), value: String.t(), source: atom()}
]
```

Same as `to_parts/3` but raises on error.

### Arguments

* `from` is the interval start.

* `to` is the interval end.

* `options` is a keyword list of options. See `to_parts/3`.

### Returns

* A list of `%{type: atom(), value: String.t(), source: atom()}` maps.

### Raises

* Raises an exception if the interval cannot be decomposed into parts.

### Examples

    iex> Localize.Interval.to_parts!(~D[2022-04-22], ~D[2022-04-25], locale: :en) |> length()
    7

# `to_string`

```elixir
@spec to_string(map() | nil, map() | nil, Keyword.t()) ::
  {:ok, String.t()} | {:error, Exception.t()}
```

Formats an interval between two dates, times or datetimes as a localized string.

### Arguments

* `from` is the start of the interval: a `t:Date.t/0`, `t:Time.t/0`, `t:NaiveDateTime.t/0` or `t:DateTime.t/0`, or a map with their fields, or `nil` for an interval open at its start. A date's map may hold only some of its `:year`, `:month` and `:day`.

* `to` is the end of the interval, of the same kind as `from`, or `nil` for an interval open at its end.

* `options` is a keyword list of options.

### Options

* `:locale` is a locale identifier. The default is `:en`.

* `:format` is a standard format, `:short`, `:medium`, `:long` or `:full`, a skeleton such as `:yMMMEd` or `:yMMMdHm`, or a pattern such as `"d MMM y"`. The default is `:medium`. A skeleton selects CLDR's interval format for its fields, as CLDR keys them, so it can name a format no standard format reaches. A pattern names no interval format, so both endpoints are formatted with it around the locale's interval fallback pattern. A datetime interval's skeleton is split into its date and time fields, as TR35's interval algorithm separates them: on one day the date is written once and the times as a range where CLDR has an interval format for the time fields, a skeleton of time fields alone writes only the times, and a skeleton of date fields alone formats the dates as a date interval does. CLDR has no interval format with seconds, so at a format that writes them, `:medium`, `:long` and `:full` among them, two times of one day are each written whole around the fallback pattern, as the algorithm's last step has it: "Jun 15, 2026, 10:00:00 – Jun 15, 2026, 14:30:00". A date in a calendar of weeks, such as `Calendrical.ISOWeek`, is written at a standard format in the calendar's own notation, so an interval of its dates is both dates around the fallback pattern: "2026-W25-2 – 2026-W27-1". A skeleton's month is such a date's week and its day the weekday, which no interval format is keyed by, so its interval is both dates in full too: "Tue, week 25 of 2026 – Mon, week 26 of 2026".

* `:date_format` and `:time_format` choose the date and the time half of a datetime interval separately, each a standard format, a skeleton or a pattern. `:date_format` is a date interval's format too, and `:time_format` a time interval's.

* `:fields` selects *which* date fields a date interval shows with a standard `:format`: `:date` (the whole date, the default), `:month`, `:month_and_day`, or `:year_and_month`. See `known_fields/0`.

* `:style` is the pattern joining a datetime interval's date and time: `:default`, the locale's standard date-time pattern, which TR35 says an interval takes ("June 15, 2026, 10:00 – 14:30"), or `:at`, the "at time" pattern a single date and time takes by default in `Localize.DateTime.to_string/2`. The default is `:default`.

* `:numeric_date_separator` and `:numeric_time_separator` are
  strings replacing the locale's own separators in the rendered
  pattern, as they do on `Localize.Date.to_string/2`. A separator
  the pattern merges with neighbouring literal text is left alone
  — fi's day-differing interval pattern is `"d.–d.M.y"`, whose
  first separator carries the range dash — so an interval may
  substitute one endpoint and not the other.

With a standard format the two are independent axes: `:fields` chooses which fields appear, `:format` chooses how wide they are rendered. So `fields: :year_and_month` renders the two months against a single year either way — numerically for `format: :short` ("1/2022" … "3/2022") and spelled out for `format: :long` ("January" … "March 2022").

At a standard format an interval's dates and times have the fields of the pattern the single value is written with, at the pattern's widths, so they read as `Localize.Date.to_string/2` and `Localize.Time.to_string/2` write one alone: `vi`'s short date is "1/4/23" and its interval "1/4/23 – 10/4/23", and a locale whose short time is 24-hour writes a 24-hour range. The order and the text between the fields are those of CLDR's interval format for those fields.

Endpoints that differ in no field the interval shows are formatted once: whole dates in the requested standard format, exactly as `Localize.Date.to_string/2` renders them. A date interval's fields are compared as its format writes them, so a week (`:yw`) or a quarter (`:yQQQ`) is one field whatever months and days it spans: 15 June to 20 July 2026 is "week 25 of 2026 – week 30 of 2026", and 15 to 17 June "week 25 of 2026". Nothing is added to a skeleton: two dates that differ in a month or a year it does not write are each written with it around the fallback pattern, `format: :MMMd` from 28 December to 3 January being "Dec 28 – Jan 3", and the same day of two years is that day once, "Jun 15". A date and a time are compared the same way: two days of June at 10:00 are "Jun 2026, 10:00" at `:yMMMHm`, and at two times of day both are written in full.

Dates that hold only some of their fields, two months or a month and a day each, take at a standard format CLDR's interval format for the fields they hold, as `Localize.Date.to_string/2` writes each alone: `%{month: 6}` to `%{month: 8}` is "Jun – Aug". One format writes both endpoints, so they must hold the same fields. A skeleton, a `:fields` selection or a pattern is used as it is given.

### Returns

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

* `{:error, exception}` on failure, including endpoints in different calendars and dates that hold different fields.

### Examples

    iex> Localize.Interval.to_string(~D[2022-04-22], ~D[2022-04-25], locale: :en)
    {:ok, "Apr 22 – 25, 2022"}

    iex> Localize.Interval.to_string(~D[2026-06-15], ~D[2026-06-18], format: :yMMMEd, locale: :en)
    {:ok, "Mon, Jun 15 – Thu, Jun 18, 2026"}

    iex> Localize.Interval.to_string(%{month: 6, day: 15}, %{month: 6, day: 20}, locale: :en)
    {:ok, "Jun 15 – 20"}

    iex> Localize.Interval.to_string(~N[2026-06-15 10:00:00], ~N[2026-06-15 14:30:00],
    ...>   format: :yMMMdHm,
    ...>   locale: :en
    ...> )
    {:ok, "Jun 15, 2026, 10:00 – 14:30"}

    iex> Localize.Interval.to_string(~N[2026-06-15 10:00:00], ~N[2026-06-15 14:30:00],
    ...>   format: :Hm,
    ...>   locale: :en
    ...> )
    {:ok, "10:00 – 14:30"}

    iex> Localize.Interval.to_string(~N[2026-06-15 10:00:00], ~N[2026-06-15 14:30:00],
    ...>   format: :yMMMdHms,
    ...>   locale: :en
    ...> )
    {:ok, "Jun 15, 2026, 10:00:00 – Jun 15, 2026, 14:30:00"}

# `to_string!`

```elixir
@spec to_string!(map(), map(), Keyword.t()) :: String.t()
```

Same as `to_string/3` but raises on error.

### Arguments

* `from` is a `t:Date.t/0`.

* `to` is a `t:Date.t/0`.

* `options` is a keyword list of options.

### Options

See `to_string/3` for the supported options.

### Returns

* The formatted interval as a string.

* Raises an exception if the interval cannot be formatted.

### Examples

    iex> Localize.Interval.to_string!(~D[2022-04-22], ~D[2022-04-25], locale: :en)
    "Apr 22 – 25, 2022"

    iex> Localize.Interval.to_string!(~D[2022-01-15], ~D[2022-03-20], locale: :en)
    "Jan 15 – Mar 20, 2022"

---

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