# Migrating from ex_cldr_calendars
This guide covers the changes needed when migrating from [ex_cldr_calendars](https://hex.pm/packages/ex_cldr_calendars) (and its companion calendar libraries) to Calendrical.
## Overview
Calendrical consolidates the following ex_cldr packages into a single library:
| Old Package | New Module |
|---|---|
| `ex_cldr_calendars` (core) | `Calendrical` |
| `ex_cldr_calendars_persian` | `Calendrical.Persian` |
| `ex_cldr_calendars_coptic` | `Calendrical.Coptic` |
| `ex_cldr_calendars_ethiopic` | `Calendrical.Ethiopic` |
| `ex_cldr_calendars_japanese` | `Calendrical.Japanese` |
| `ex_cldr_calendars_lunisolar` | `Calendrical.Chinese`, `Calendrical.Korean`, `Calendrical.LunarJapanese` |
| `ex_cldr_calendars_format` | `Calendrical.Format`, `Calendrical.Formatter` |
All calendar functionality, localization data, formatting, and date arithmetic are available from a single dependency.
## Dependency changes
Remove all ex_cldr calendar dependencies and replace with `calendrical`:
```elixir
# Old
defp deps do
[
{:ex_cldr_calendars, "~> 2.0"},
{:ex_cldr_calendars_persian, "~> 1.0"},
{:ex_cldr_calendars_coptic, "~> 1.0"},
{:ex_cldr_calendars_format, "~> 1.0"},
# ... other calendar packages
]
end
# New
defp deps do
[
{:calendrical, "~> 0.1"},
]
end
```
Calendrical depends on [Localize](https://hex.pm/packages/localize) for CLDR locale data and [Astro](https://hex.pm/packages/astro) for astronomical calculations used by the Persian and lunisolar calendars.
## Localize replaces ex_cldr
Calendrical uses [Localize](https://hex.pm/packages/localize) instead of `ex_cldr` for all locale data access. Localize provides the same CLDR data but without the backend module architecture.
### No backend modules
The most significant architectural change is the removal of the backend pattern. There is no equivalent of `MyApp.Cldr` in Calendrical.
```elixir
# Old — required a backend module
defmodule MyApp.Cldr do
use Cldr,
providers: [Cldr.Calendar, Cldr.Number, Cldr.Unit, Cldr.List],
locales: ["en", "fr", "ar", "he"],
default_locale: "en"
end
MyApp.Cldr.Calendar.localize(date, :month)
MyApp.Cldr.Calendar.strftime_options!(locale: "fr")
# New — call Calendrical directly
Calendrical.localize(date, :month)
Calendrical.strftime_options!(locale: "fr")
```
### Locale management
Replace `Cldr` locale functions with their `Localize` equivalents:
| Old | New |
|---|---|
| `Cldr.get_locale()` | `Localize.get_locale()` |
| `Cldr.put_locale(locale)` | `Localize.put_locale(locale)` |
| `Cldr.validate_locale(locale, backend)` | `Localize.validate_locale(locale)` |
| `Cldr.validate_territory(territory)` | `Localize.validate_territory(territory)` |
| `Cldr.LanguageTag` | `Localize.LanguageTag` |
| `Cldr.Locale.territory_from_locale(locale)` | `Localize.Territory.territory_from_locale(locale)` |
All functions that previously accepted a `:backend` option no longer do. The `:locale` option defaults to `Localize.get_locale()`.
## Module namespace changes
All modules move from the `Cldr.Calendar` namespace to `Calendrical`:
| Old | New |
|---|---|
| `Cldr.Calendar` | `Calendrical` |
| `Cldr.Calendar.Gregorian` | `Calendrical.Gregorian` |
| `Cldr.Calendar.Julian` | `Calendrical.Julian` |
| `Cldr.Calendar.ISO` | `Calendrical.ISO` |
| `Cldr.Calendar.ISOWeek` | `Calendrical.ISOWeek` |
| `Cldr.Calendar.NRF` | `Calendrical.NRF` |
| `Cldr.Calendar.Persian` | `Calendrical.Persian` |
| `Cldr.Calendar.Coptic` | `Calendrical.Coptic` |
| `Cldr.Calendar.Ethiopic` | `Calendrical.Ethiopic` |
| `Cldr.Calendar.Japanese` | `Calendrical.Japanese` |
| `Cldr.Calendar.Chinese` | `Calendrical.Chinese` |
| `Cldr.Calendar.Korean` | `Calendrical.Korean` |
| `Cldr.Calendar.LunarJapanese` | `Calendrical.LunarJapanese` |
| `Cldr.Calendar.FiscalYear` | `Calendrical.FiscalYear` |
| `Cldr.Calendar.FiscalYear.US` | `Calendrical.FiscalYear.US` |
| `Cldr.Calendar.Config` | `Calendrical.Config` |
| `Cldr.Calendar.Interval` | `Calendrical.Interval` |
| `Cldr.Calendar.Kday` | `Calendrical.Kday` |
| `Cldr.Calendar.Sigils` | **removed** — use Elixir's native `~D` sigil. See [Sigils](#sigils) below. |
| `Cldr.Calendar.Preference` | `Calendrical.Preference` |
Territory-derived calendars also change namespace. For example, `Cldr.Calendar.US` becomes `Calendrical.US` and `Cldr.Calendar.GB` becomes `Calendrical.GB`.
### Exception modules
Calendrical's exceptions have been completely restructured:
* **One file per exception** in `lib/calendrical/exception/`, mirroring the layout used by Localize.
* **Semantic struct fields** instead of a single opaque `:message` string. Callers can now pattern-match on the exception's data fields.
* **`gettext`-based messages** so error text can be translated. The backend is `Calendrical.Gettext` and messages are in the `"calendrical"` domain with contexts `"calendar"`, `"date"`, `"format"`, and `"option"`.
* **All names end with `Error`** for consistency with the Localize convention.
| Old | New | Fields |
|---|---|---|
| `Cldr.IncompatibleCalendarError` | `Calendrical.IncompatibleCalendarError` | `:from`, `:to` |
| `Cldr.InvalidCalendarModule` | `Calendrical.InvalidCalendarModuleError` | `:module` |
| `Cldr.InvalidDateOrder` | `Calendrical.InvalidDateOrderError` | `:from`, `:to` |
| `Cldr.IncompatibleTimeZone` | `Calendrical.IncompatibleTimeZoneError` | `:from`, `:to` |
| `Cldr.MissingFields` | `Calendrical.MissingFieldsError` | `:function`, `:fields` |
New exceptions introduced by this refactor (replacing inline `{ArgumentError, "..."}` tuples):
| Module | Fields | Used by |
|---|---|---|
| `Calendrical.InvalidPartError` | `:part`, `:valid_parts` | `Calendrical.localize/3` |
| `Calendrical.InvalidTypeError` | `:type`, `:valid_types` | `Calendrical.localize/3` |
| `Calendrical.InvalidFormatError` | `:format`, `:valid_formats` | `Calendrical.localize/3` |
### Error return convention
All `{:error, _}` returns from Calendrical now use the modern Elixir convention `{:error, %Exception{}}` instead of the legacy two-tuple form `{:error, {ExceptionModule, "message"}}`. This applies to functions returning Localize exceptions (`Localize.UnknownTerritoryError`, `Localize.UnknownCalendarError`, `Localize.InvalidLocaleError`) as well as Calendrical exceptions.
```elixir
# Old
case Calendrical.calendar_from_territory(:YY) do
{:ok, calendar} -> calendar
{:error, {Localize.UnknownTerritoryError, message}} -> handle(message)
end
# New
case Calendrical.calendar_from_territory(:YY) do
{:ok, calendar} -> calendar
{:error, %Localize.UnknownTerritoryError{territory: territory}} -> handle(territory)
end
```
The new pattern lets callers extract structured data from the exception (e.g. `:territory`, `:calendar`, `:module`, `:fields`) instead of parsing message strings.
### Behaviour module
Calendars that use the behaviour macro change from `use Cldr.Calendar.Behaviour` to `use Calendrical.Behaviour`, and from `use Cldr.Calendar.Base.Month` / `use Cldr.Calendar.Base.Week` to `use Calendrical.Base.Month` / `use Calendrical.Base.Week`. The configuration options remain the same.
## Removed: Cldr.Calendar.Duration
The `Cldr.Calendar.Duration` module has been removed entirely. Use Elixir's built-in `%Duration{}` struct (available since Elixir 1.17) and `Date.diff/2` instead.
### Computing differences
```elixir
# Old
{:ok, duration} = Cldr.Calendar.Duration.new(~D[2020-01-01], ~D[2021-03-15])
# => {:ok, %Cldr.Calendar.Duration{year: 1, month: 2, day: 14, ...}}
# New — use Date.diff for day counts
Date.diff(~D[2021-03-15], ~D[2020-01-01])
# => 439
# Or construct a Duration directly
%Duration{year: 1, month: 2, day: 14}
```
### Formatting durations
The localized `Duration.to_string/2` function which used `Cldr.Unit` and `Cldr.List` for formatting has been removed. Use `Localize.Unit` and `Localize.List` directly if localized duration formatting is needed.
### Shifting dates with durations
```elixir
# Old
duration = Cldr.Calendar.Duration.new!(~D[2020-01-01], ~D[2020-03-15])
Cldr.Calendar.plus(~D[2025-01-01], duration)
# New — use Date.shift with a Duration
Date.shift(~D[2025-01-01], %Duration{month: 2, day: 14})
```
## Removed: Calendrical.plus and Calendrical.minus
The public `Calendrical.plus/2,3,4` and `Calendrical.minus/3,4` functions have been removed. Use Elixir's `Date.shift/2`, `DateTime.shift/2`, and `NaiveDateTime.shift/2` instead.
### Date arithmetic
```elixir
# Old
Cldr.Calendar.plus(date, :years, 1)
Cldr.Calendar.plus(date, :months, 3)
Cldr.Calendar.plus(date, :quarters, 1)
Cldr.Calendar.plus(date, :weeks, 2)
Cldr.Calendar.plus(date, :days, 10)
Cldr.Calendar.minus(date, :months, 1)
# New
Date.shift(date, year: 1)
Date.shift(date, month: 3)
Date.shift(date, month: 3) # quarters: multiply by 3
Date.shift(date, week: 2)
Date.shift(date, day: 10)
Date.shift(date, month: -1)
```
`Date.shift/2` works correctly with all Calendrical calendar types including week-based fiscal calendars, lunisolar calendars, and the Julian calendar. Each calendar implements the `Calendar.shift_date/4` callback with the appropriate semantics for its calendar system.
### Coercion
The old `plus/4` accepted a `:coerce` option that controlled whether invalid dates (such as February 30) would be clamped to valid dates or return `{:error, :invalid_date}`. `Date.shift/2` always coerces to valid dates, matching the `coerce: true` default behaviour.
```elixir
# Old — could disable coercion
Cldr.Calendar.plus(~D[2024-01-31], :months, 1, coerce: false)
# => {:error, :invalid_date}
# New — always coerces
Date.shift(~D[2024-01-31], month: 1)
# => ~D[2024-02-29]
```
### No quarter unit
`Date.shift/2` does not support a `:quarter` unit. Multiply by 3 months instead:
```elixir
# Old
Cldr.Calendar.plus(date, :quarters, 2)
# New
Date.shift(date, month: 6)
```
### Date.Range shifting
The old `Cldr.Calendar.plus/4` could shift a `Date.Range` and return a new range for the resulting period. This is no longer supported as a single operation. Use `Calendrical.next/2` or `Calendrical.previous/2` for period navigation, which return `Date.Range` values for range inputs.
## Retained public API
The following functions continue to work as before (with namespace changes):
### Navigation
`Calendrical.next/2,3` and `Calendrical.previous/2,3` work as before but now use `Date.shift` internally. They accept dates and date ranges and return the next or previous period:
```elixir
Calendrical.next(~D[2024-03-15], :month)
# => ~D[2024-04-15]
Calendrical.previous(~D[2024-03-15], :year)
# => ~D[2023-03-15]
# With Date.Range inputs, returns the next/previous period as a range
year_range = Calendrical.Gregorian.year(2024)
Calendrical.next(year_range, :year)
# => Date.range(~D[2025-01-01], ~D[2025-12-31])
```
### Localization
`Calendrical.localize/2,3` works the same way but is called directly on the `Calendrical` module instead of through a backend:
```elixir
Calendrical.localize(~D[2024-06-15], :month)
# => "Jun"
Calendrical.localize(~D[2024-06-15], :month, format: :wide, locale: "fr")
# => "juin"
Calendrical.localize(~D[2024-06-15], :day_of_week)
# => "Sat"
Calendrical.localize(%{hour: 14}, :am_pm)
# => "PM"
```
### Locale data access
The locale data functions that were on the backend (`MyApp.Cldr.Calendar.eras/2`, `.months/2`, etc.) are now on the `Calendrical` module:
```elixir
Calendrical.eras(:en, :gregorian)
Calendrical.months(:en, :gregorian)
Calendrical.days(:fr, :gregorian)
Calendrical.quarters(:en, :gregorian)
Calendrical.day_periods(:en, :gregorian)
Calendrical.cyclic_years(:en, :chinese)
Calendrical.month_patterns(:en, :chinese)
```
These return `{:ok, data}` tuples.
### Calendar creation
```elixir
# Creating custom calendars
{:ok, MyFiscal} = Calendrical.new(MyFiscal, :month, month_of_year: 7, year: :ending)
# Or using the behaviour directly in a module
defmodule MyApp.FiscalYear do
use Calendrical.Base.Month,
month_of_year: 7,
year: :ending
end
# Week-based calendar
defmodule MyApp.Retail do
use Calendrical.Base.Week,
begins_or_ends: :ends,
first_or_last: :last,
day_of_week: 6,
month_of_year: 1,
weeks_in_month: [4, 4, 5]
end
```
### Intervals and streams
```elixir
Calendrical.interval(~D[2024-01-01], 6, :months)
# => [~D[2024-01-01], ~D[2024-02-01], ~D[2024-03-01], ...]
Calendrical.interval_stream(~D[2024-01-01], ~D[2024-12-31], :quarters)
|> Enum.to_list()
```
### Sigils
The `Calendrical.Sigils` module — which provided the `~d` sigil — has been **removed**. Use Elixir's native `~D` sigil with a fully-qualified calendar suffix instead.
```elixir
# Old (ex_cldr_calendars / Calendrical 0.0.x)
import Calendrical.Sigils
~d[2024-06-15] # Gregorian (default)
~d[2024-06-15 Gregorian] # Explicit Gregorian
~d[2024-W24-6] # ISO Week
~d[2024-06-15 Persian] # Persian calendar
~d[1446-06-15 C.E. Julian] # Julian calendar
# New (Calendrical 0.1.0+) — use Elixir's native ~D, ~U, ~N
~D[2024-06-15] # Calendar.ISO (default)
~D[2024-06-15 Calendrical.Gregorian] # Explicit Gregorian
~D[2024-06-15 Calendrical.Persian] # Persian calendar
~D[1446-06-15 Calendrical.Julian] # Julian calendar
~D[-1446-06-15 Calendrical.Julian] # B.C.E. Julian (negative year)
~D[1446-09-01 Calendrical.Islamic.UmmAlQura] # Umm al-Qura
~D[5784-08-15 Calendrical.Hebrew] # Hebrew (Tishri = 1)
```
The native `~D` sigil has supported the trailing calendar form since Elixir 1.10. It works for any module implementing the `Calendar` behaviour, so all 17 Calendrical calendars (and any user-defined calendar built with `Calendrical.Behaviour`) are valid.
Features the old `~d` sigil supported that the native `~D` does not:
* **ISO week date format** (`~d[2024-W24-6]`) — there is no native sigil for week dates. If you need to parse a week date, do so explicitly:
```elixir
# Roughly equivalent to ~d[2024-W24-6]
{year, week, day} = {2024, 24, 6}
Date.new!(year, week, day, Calendrical.ISOWeek)
```
* **Short calendar names** (`~d[2024-06-15 Persian]` → `Calendrical.Persian`) — write the full module name with the native sigil.
* **B.C.E./C.E. era markers** (`~d[2024-01-01 B.C.E. Julian]`) — use a negative year with the native sigil: `~D[-2024-01-01 Calendrical.Julian]`.
* **Default to `Calendrical.Gregorian`** — bare `~d[2024-01-01]` defaulted to `Calendrical.Gregorian`. Bare `~D[2024-01-01]` defaults to `Calendar.ISO`. The two are arithmetically equivalent (Calendrical.Gregorian wraps Calendar.ISO) but the `:calendar` field is different. Use `~D[2024-01-01 Calendrical.Gregorian]` if you specifically want the Calendrical.Gregorian calendar struct.
## Calendar formatting (ex_cldr_calendars_format)
The `ex_cldr_calendars_format` library is now part of Calendrical. It provides a behaviour-based plugin system for rendering calendars as HTML, Markdown, or custom formats.
### Module renames
| Old | New |
|---|---|
| `Cldr.Calendar.Format` | `Calendrical.Format` |
| `Cldr.Calendar.Formatter` | `Calendrical.Formatter` |
| `Cldr.Calendar.Formatter.Options` | `Calendrical.Formatter.Options` |
| `Cldr.Calendar.Formatter.HTML.Basic` | `Calendrical.Formatter.HTML.Basic` |
| `Cldr.Calendar.Formatter.HTML.Week` | `Calendrical.Formatter.HTML.Week` |
| `Cldr.Calendar.Formatter.Markdown` | `Calendrical.Formatter.Markdown` |
| `Cldr.Calendar.Formatter.UnknownFormatterError` | `Calendrical.Formatter.UnknownFormatterError` |
| `Cldr.Calendar.Formatter.InvalidDateError` | `Calendrical.Formatter.InvalidDateError` |
| `Cldr.Calendar.Formatter.InvalidOption` | `Calendrical.Formatter.InvalidOptionError` |
### Removed `:backend` option
The `:backend` option has been removed from `Calendrical.Formatter.Options`. If passed, it is silently ignored for backward compatibility.
```elixir
# Old
Cldr.Calendar.Format.year(2024, backend: MyApp.Cldr, locale: "fr")
Cldr.Calendar.Format.month(2024, 6, backend: MyApp.Cldr, formatter: Cldr.Calendar.Formatter.Markdown)
# New
Calendrical.Format.year(2024, locale: "fr")
Calendrical.Format.month(2024, 6, formatter: Calendrical.Formatter.Markdown)
```
The `:locale` option defaults to `Localize.get_locale()` instead of `backend.get_locale()`.
### Number formatting changes
The formatter options validation for `:number_system` now uses the Localize API directly. The old three-argument `Cldr.Number.validate_number_system(locale, system, backend)` is replaced by `Localize.validate_number_system(system)`.
Number formatting inside formatters uses `Localize.Number.to_string!/2` instead of `Cldr.Number.to_string!/3` (no backend parameter):
```elixir
# Old (inside a custom formatter)
Cldr.Number.to_string!(day, backend, locale: locale, number_system: number_system)
# New
Localize.Number.to_string!(day, locale: locale, number_system: number_system)
```
### Custom formatter behaviour
The `Calendrical.Formatter` behaviour callbacks are unchanged. Custom formatters that implement the four callbacks (`format_year/3`, `format_month/4`, `format_week/5`, `format_day/4`) work the same way. The only change is the module name in the `@behaviour` declaration and the `Options` struct no longer containing a `:backend` field:
```elixir
# Old
defmodule MyApp.CustomFormatter do
@behaviour Cldr.Calendar.Formatter
@impl true
def format_day(date, year, month, options) do
# options.backend was available here
...
end
end
# New
defmodule MyApp.CustomFormatter do
@behaviour Calendrical.Formatter
@impl true
def format_day(date, year, month, options) do
# options.backend no longer exists
# use Localize directly for any locale data needs
...
end
end
```
### Options struct changes
The `Calendrical.Formatter.Options` struct fields:
| Field | Status | Notes |
|---|---|---|
| `:calendar` | Unchanged | Calendar module, defaults to `Calendrical.Gregorian` |
| `:formatter` | Unchanged | Formatter module, defaults to `Calendrical.Formatter.HTML.Basic` |
| `:locale` | Changed | Defaults to `Localize.get_locale()` instead of `backend.get_locale()` |
| `:number_system` | Changed | Validated via `Localize.validate_number_system/1` |
| `:territory` | Changed | Derived via `Localize.Territory.territory_from_locale/1` |
| `:backend` | **Removed** | No longer present in the struct |
| `:caption` | Unchanged | |
| `:class` | Unchanged | |
| `:id` | Unchanged | |
| `:today` | Unchanged | |
| `:day_names` | Unchanged | |
| `:private` | Unchanged | |
## Configuration changes
The `Calendrical.Config` struct no longer includes a `:cldr_backend` field. The `:locale` option can be used when creating calendars to derive locale-specific defaults for `:day_of_week` and `:min_days_in_first_week`.
```elixir
# Old
Cldr.Calendar.new(MyCalendar, :week, [
cldr_backend: MyApp.Cldr,
day_of_week: 1,
month_of_year: 1
])
# New
Calendrical.new(MyCalendar, :week, [
day_of_week: 1,
month_of_year: 1
])
```
## Unit application order
A subtle but important semantic difference: `Date.shift/2` applies duration units in order from largest to smallest (years → months → weeks → days), matching the Elixir stdlib convention. The old `Cldr.Calendar.plus(date, %Duration{})` applied units in the opposite order (days → months → years). In most cases the results are identical, but they can differ when date clamping occurs at intermediate steps.