Calendrical

Localized month- and week-based calendars, fiscal-year support, calendar arithmetic, and 21 calendar systems for Elixir, built on the Unicode CLDR repository via Localize. Calendrical 2.0 is built on Localize 1.4 and CLDR 49.

Calendrical extends Elixir's standard Calendar and Date modules with comprehensive support for the calendar systems used around the world, including arithmetic and astronomical lunar calendars, year-shifted variants such as Buddhist and ROC, and the official tabular and observational Islamic calendars.

Features

Supported Elixir and OTP versions

Calendrical requires Elixir 1.17+ and Erlang/OTP 26+.

On OTP 26, also add {:json_polyfill, "~> 0.2 or ~> 1.0"} to your dependencies. It provides the OTP 27+ :json module that Localize uses. OTP 27 and later need no extra dependency.

Installation

Add calendrical to your dependencies in mix.exs:

def deps do
  [
    {:calendrical, "~> 2.0"}
  ]
end

Localize ships with the en locale. Download the data for any other locale your application uses at build time with mix localize.download_locales, for example mix localize.download_locales fr ar he. Without it, a call for another locale falls back to en. See mix localize.download_locales.

Calendrical.TimeZone resolves IANA time zone names through a Calendar.TimeZoneDatabase. Add one, such as tz, which is used automatically once it is loaded, or configure one with config :elixir, :time_zone_database, Tz.TimeZoneDatabase. Without one, IANA names are rejected.

Quick start

iex> # Convert a Gregorian date to the Hebrew calendar
iex> {:ok, gregorian} = Date.new(2024, 10, 3, Calendrical.Gregorian)
iex> {:ok, hebrew} = Date.convert(gregorian, Calendrical.Hebrew)
iex> hebrew
~D[5785-01-01 Calendrical.Hebrew]

iex> # Localize the month name
iex> Calendrical.localize(hebrew, :month, locale: "en")
"Tishri"

iex> Calendrical.localize(hebrew, :month, locale: "he", style: :wide)
"תשרי"

iex> # Convert to the Islamic Umm al-Qura calendar
iex> {:ok, hijri} = Date.convert(gregorian, Calendrical.Islamic.UmmAlQura)
iex> Calendrical.localize(hijri, :month, locale: "en")
"Rab. I"

iex> # Buddhist Era (Thailand)
iex> {:ok, buddhist} = Date.convert(gregorian, Calendrical.Buddhist)
iex> buddhist.year
2567

iex> # Get a date range for a fiscal year quarter. The US fiscal year
iex> # 2024 begins on 1 October 2023, which its calendar counts as month 1
iex> {:ok, calendar} = Calendrical.FiscalYear.calendar_for(:US)
iex> Calendrical.Interval.quarter(2024, 1, calendar)
Date.range(~D[2024-01-01 Calendrical.FiscalYear.US], ~D[2024-03-31 Calendrical.FiscalYear.US])

iex> # Find the second Tuesday in November 2024 (Tuesday = day 2)
iex> Calendrical.Kday.nth_kday(~D[2024-11-01], 2, 2)
~D[2024-11-12]

iex> # Western Easter Sunday for 2024 (Gregorian computus)
iex> Calendrical.Ecclesiastical.easter_sunday(2024)
~D[2024-03-31 Calendrical.Gregorian]

iex> # Eastern Orthodox Easter Sunday for 2024 (Julian computus)
iex> Calendrical.Ecclesiastical.orthodox_easter_sunday(2024)
~D[2024-04-22 Calendrical.Julian]

iex> # Astronomical Paschal Full Moon for 2025
iex> {:ok, pfm} = Calendrical.Ecclesiastical.paschal_full_moon(2025)
iex> pfm
~D[2025-04-13]

Available calendars

Calendrical implements every calendar system exposed by CLDR, plus lunisolar variants observed from other meridians (Lunar Japanese and Vietnamese). They are grouped below by their underlying mechanism. See guides/calendar_summary.md for the full descriptions, eras, month structures, and reference dates.

Family Calendars
Gregorian-based (Gregorian months, or whole weeks of them) Calendrical.Gregorian, Calendrical.ISO, Calendrical.ISOWeek, Calendrical.NRF, Calendrical.Buddhist, Calendrical.Roc, Calendrical.Japanese
Julian-based (proleptic Julian + variants) Calendrical.Julian, Calendrical.Julian.Jan1, Calendrical.Julian.March1, Calendrical.Julian.March25, Calendrical.Julian.Sept1, Calendrical.Julian.Dec25
Solar (non-Gregorian) Calendrical.Persian (astronomical), Calendrical.Indian, Calendrical.Coptic, Calendrical.Ethiopic, Calendrical.Ethiopic.AmeteAlem
Lunar (tabular) Calendrical.Islamic.Civil, Calendrical.Islamic.Tbla, Calendrical.Islamic.UmmAlQura
Lunar (observational/astronomical) Calendrical.Islamic.Observational (Cairo), Calendrical.Islamic.Rgsa (Mecca), Calendrical.Islamic.UmmAlQura.Astronomical
Lunisolar Calendrical.Hebrew (arithmetic), Calendrical.Chinese, Calendrical.Korean (Dangi), Calendrical.LunarJapanese, Calendrical.Vietnamese (Hanoi meridian)
Composite Calendrical.Composite (user-defined; e.g. England with Julian-to-Gregorian transition)
Fiscal-year Calendrical.FiscalYear.US, .AU, .GB, … (50+ territories, built by Calendrical.FiscalYear.calendar_for/1)

Defining your own calendar

A calendar is a kernel of eight callbacks and the facts its info/0 states; everything else is derived. use Calendrical.Calendar makes a hand-written calendar from its spine, date_to_iso_days/3 and date_from_iso_days/1, and the six primitive Calendar callbacks:

defmodule MyApp.Julian do
  use Calendrical.Calendar,
    epoch: ~D[0001-01-01 Calendrical.Julian],
    cldr_calendar_type: :gregorian,
    weeks: [first_day: 1, min_days_in_first_week: 1]

  @impl Calendar
  def leap_year?(year), do: rem(year, 4) == 0

  @impl Calendar
  def days_in_month(year, 2), do: if(leap_year?(year), do: 29, else: 28)
  def days_in_month(_year, month) when month in [4, 6, 9, 11], do: 30
  def days_in_month(_year, _month), do: 31

  @impl true
  def date_to_iso_days(year, month, day), do: Calendrical.Julian.date_to_iso_days(year, month, day)

  @impl true
  def date_from_iso_days(iso_days), do: Calendrical.Julian.date_from_iso_days(iso_days)
end

A year begun elsewhere in a base calendar's year, as a fiscal year is, needs no code at all: use Calendrical.Calendar.Shifted, year_begins: {7, 1}. A year of whole weeks is use Calendrical.Calendar.Weeks, pattern: [4, 5, 4], year_ends: {:saturday, :nearest, {1, 31}}, and Calendrical.new/3 makes either at runtime.

See guides/calendar_behaviour.md for the kernel, every option of the five makers, and what a calendar may override.

Localization

All calendars participate in CLDR localization automatically. Calling Calendrical.localize/3 with a date and a part (:era, :month, :quarter, :day_of_week, :days_of_week, :day_period, :day_periods) returns the locale-specific name through the Localize.Calendar data layer.

iex> {:ok, date} = Date.new(1446, 9, 1, Calendrical.Islamic.UmmAlQura)
iex> Calendrical.localize(date, :month, locale: "en", style: :wide)
"Ramadan"

iex> Calendrical.localize(date, :month, locale: "ar")
"رمضان"

iex> Calendrical.localize(date, :day_of_week, locale: "en", style: :wide)
"Saturday"

Calendrical.strftime/3 formats a date of any calendar with locale-aware names. Calendrical.strftime_options!/1 returns options for the standard library's Calendar.strftime/3, which names a month by its month field, so it suits the Gregorian-based calendars; use Calendrical.strftime/3 for the Hebrew, lunisolar, fiscal and week calendars.

Composite calendars

A composite calendar uses one base calendar before a specified transition date and another afterwards. The canonical example is the European transition from the Julian to the Gregorian calendar in the 16th–20th centuries.

defmodule MyApp.England do
  use Calendrical.Composite,
    calendars: [
      # 25 March 1155: the Lady Day calendar's month 1 day 1
      ~D[1155-01-01 Calendrical.Julian.March25],
      ~D[1751-03-25 Calendrical.Julian.Jan1],
      ~D[1752-09-14 Calendrical.Gregorian]
    ],
    base_calendar: Calendrical.Julian
end

# 11 days are "missing" at the September 1752 transition
iex> Date.shift(~D[1752-09-02 MyApp.England], day: 1)
~D[1752-09-14 MyApp.England]

A composite calendar can chain any number of transitions and combine any pair of calendars. See Calendrical.Composite for details.

Configuration

Calendrical inherits the locale and provider configuration from Localize. The only Calendrical-specific configuration is for the few lunisolar calendars that accept a custom epoch:

config :calendrical,
  chinese_epoch: ~D[-2636-02-15],
  korean_epoch: ~D[-2332-02-15],
  lunar_japanese_epoch: ~D[0645-02-05]
Option Default Description
:chinese_epoch ~D[-2636-02-15] The first sexagesimal cycle origin used by the Chinese calendar.
:korean_epoch ~D[-2332-02-15] The founding-of-Korea origin used by the Korean (Dangi) calendar.
:lunar_japanese_epoch ~D[0645-02-05] The lunar new year of the year the Taika era was proclaimed, the first day of year 1 of the Lunar Japanese calendar.
:vietnamese_epoch ~D[-2636-02-15] The sexagesimal cycle origin used by the Vietnamese calendar (shared with the Chinese calendar).

For Calendrical's underlying locale, default-locale, and locale-cache configuration, see the Localize configuration documentation.

Documentation

Full API documentation is available on HexDocs.

License

Apache License 2.0. See the LICENSE file for details. The Umm al-Qura month lengths for 1501 to 1600 AH are derived from ICU and are under the Unicode License V3.