Changelog

Copy Markdown

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.5.0] — 2026-05-17

Breaking changes

Added

  • TR35 date pattern lettersQ/q (quarter, format & standalone, widths 1–5), w (week of year), W (week of month), Y (week-based year), D (day of year), e/c (local day of week, numeric & names), F (day-of-week-in-month). E weekday names are now validated against the constructed date instead of consumed and discarded.

  • TR35 flexible day periods (B)Calendrical.Time.parse/2 recognises locale-specific flex period names ("in the morning", "at night", "noon", "midnight") and uses them to disambiguate AM/PM for 12-hour cycles when no a marker is present.

  • TR35 time zone resolutionCalendrical.DateTime.parse/2 now returns a DateTime (with the correct UTC offset) when the input carries a zone token. Supported: ISO offsets (Z, ±HH:MM, ±HHMM), GMT/UTC format (GMT+10:30), IANA zones (Asia/Tokyo), short abbreviations (PST, EST, JST, …), and CLDR locale names (Pacific Time). New Calendrical.TimeZone.resolve/3. IANA-name resolution requires the host application to depend on :tzdata or :tz (detected at runtime); without one, IANA names fall back to a NaiveDateTime.

  • All CLDR availableFormats skeletons are iterated on parse, not just the four dateStyle / timeStyle references. The standards are themselves keys into availableFormats, so this both subsumes the previous narrower set AND admits inputs like "3-5-1960" (matches :yMd skeleton "M/d/y" under lenient separator equivalence) and "week 20 of 2026" (matches :yw skeleton "'week' w 'of' Y").

  • New Calendrical.Time.Parser.parse_with_zone/2 — same as parse/2 but also returns the captured zone string. Used by the DateTime parser; useful directly when a caller needs both the wall time and the original zone text.

  • Plural-variant patterns in availableFormats (the %{one: ..., other: ...} shape on week-bearing skeletons like :yw) are now iterated, not silently dropped.

Bug Fixes

  • Time-zone field regex (z/Z/v/V/O/X/x) tightened from the previous permissive [\p{L}\d:+\-/_]+ (which would happily eat "midnight") to require zone-shaped input — ISO offsets, GMT format, IANA region/city, uppercase abbreviation, or CLDR-style capital-led name.

  • Time parser no longer requires the minute capture — skeletons like :Bh ("h B") that omit minutes now parse instead of erroring.

  • Two-digit year pivot (yy) is correctly skipped for era-aware calendars (Japanese imperial, ROC) where the year is meant literally.

[0.4.0] — 2026-05-17

Bug Fixes

  • Calendrical.LunarJapanese.new/3, Calendrical.Chinese.new/3, and Calendrical.Korean.new/3 rejected valid {m, :leap} inputs in the documented traditional notation — the validator compared the user's traditional month number against the ordinal position returned by leap_month/1, which is always off by one. The check now correctly converts ordinal to traditional before comparing, the private helper has been renamed valid_traditional_date?/5 to disambiguate from the 3-arity valid_date?/3 callback used by Date.new/4, and the public Date.new/4 ordinal contract is unchanged.

  • Test support module renamed from Calendrical.Date to Calendrical.Test.DateGenerator to free the Calendrical.Date namespace for the new parser module. Affects test/property_test.exs and test/day_of_week_test.exs only — no public API impact.

Added

  • traditional_leap_month/1 on each of the three lunisolar calendars (Calendrical.LunarJapanese, Calendrical.Chinese, Calendrical.Korean), returning the traditional (1..12) number of the intercalary month — the number the leap month repeats — as a companion to leap_month/1 which returns the ordinal position (1..13).

  • Calendrical.Time.parse/2 and Calendrical.DateTime.parse/2 — locale-aware time and date-time parsers completing the parser trio alongside Calendrical.Date.parse/2, TR35-compliant for hour-cycle resolution, day-period names, fractional seconds, and CLDR glue patterns. See the moduledocs for the day-period inheritance and datetime-glue backtracking strategy.

  • Calendrical.TimeParseError and Calendrical.DateTimeParseError — structured errors carrying :input and :locale.

  • Calendrical.Date.parse/2 — locale-aware parser for user-typed date strings across every Calendar-behaviour module exposing cldr_calendar_type/0 (Gregorian, Buddhist, Japanese imperial, Islamic, Persian, Hebrew, ROC, Coptic, Ethiopic, Indian, …). Handles CLDR lenient-scope-date separator equivalences, non-Latin digit transliteration, 2-digit year pivoting, and era markers — see Calendrical.Date.Parser for the full strategy.

  • Calendrical.Date.parse_range/2 — locale-aware range parser. Accepts either a single string (split on CLDR's intervalFormatFallback separator) or a {from, to} tuple, with CLDR interval-skeleton inheritance so "May 5 – May 10, 2026" parses even though the left endpoint has no year.

  • Calendrical.DateParseError and Calendrical.DateRangeParseError — structured errors carrying :input, :locale, :calendar, plus :reason and :cause for ranges.

Documentation

  • Each lunisolar calendar's moduledoc now has a "Two month numbering conventions" section explaining the difference between ordinal months (used by Date.t, Date.new/4, Date.convert/2, and the Calendar callbacks) and traditional months (used by new/3 and the return value of lunar_month_of_year/1). The previous undocumented dichotomy could silently produce dates one full lunar month off after the intercalary in leap years.

  • The new/3 and new!/3 docstrings on each lunisolar calendar now state explicitly that the lunar_month argument is traditional (1..12 with {m, :leap} for the intercalary), with examples showing how the traditional number maps to the ordinal stored on the resulting Date.t struct.

[0.3.1] — 2026-04-25

Bug Fixes

  • Remove unnecessary require.

[0.3.0] — 2026-04-22

Bug Fixes

  • Fixes mapping CLDR calendar types to the implementation module name.

[0.2.0] — 2026-04-16

This is the first release of Calendrical, which consolidates the ex_cldr_calendars library family into a single package built on Localize. Functionality from the following libraries has been merged in: ex_cldr_calendars, ex_cldr_calendars_persian, ex_cldr_calendars_coptic, ex_cldr_calendars_ethiopic, ex_cldr_calendars_japanese, ex_cldr_calendars_lunisolar, ex_cldr_calendars_islamic, ex_cldr_calendars_format, and ex_cldr_calendars_composite.

Added

  • Calendrical.Behaviour — a defmacro __using__ template that supplies sensible default implementations of every Calendar and Calendrical callback. Calendars use the behaviour, supply an :epoch (and any non-default options), define date_to_iso_days/3 and date_from_iso_days/1, and override only the callbacks that differ from the defaults. Every generated function is defoverridable. See guides/calendar_behaviour.md.

  • All 17 CLDR-acceptable calendar types are implemented:

  • Calendrical.Composite — a defmacro __using__ template for building composite calendars that use one base calendar before a specified date and a different calendar after. Supports any number of transitions chained together. The pre-built Calendrical.England and Calendrical.Russia modules demonstrate the historical Julian-to-Gregorian transitions.

  • Calendrical.Era — an @after_compile hook that auto-generates a Calendrical.Era.<CalendarType> module from CLDR era data. Calendars use Calendrical.Behaviour get era support for free without writing any era boundary code. ETS-based locking coordinates module creation for calendars that share a cldr_calendar_type.

  • Calendrical.localize/3 — locale-aware names for :era, :quarter, :month, :day_of_week, :days_of_week, :am_pm, and :day_periods parts of any date. Falls through to all 766+ CLDR locales available from Localize.Calendar. Handles the CLDR _yeartype_leap variant for Hebrew Adar II without needing month_patterns substitution.

  • Calendrical.strftime_options!/1 — returns a keyword list compatible with Calendar.strftime/3 so the standard library's formatter can produce locale-aware output for any Calendrical calendar.

  • Calendrical.shift_date/5 and Calendrical.shift_naive_datetime/9 — calendar-aware date/datetime shifting that supports the standard Date.shift/2 and NaiveDateTime.shift/2 APIs across every Calendrical calendar.

  • Calendrical.IntervalDate.Range for years, quarters, months, weeks, and days in any supported calendar. The Calendrical.Interval.relation/2 function implements Allen's interval algebra (precedes, meets, overlaps, contains, …).

  • Calendrical.Kday — finds the n-th occurrence of a given weekday relative to a date (e.g. "the second Tuesday in November", "the last Sunday before Christmas").

  • Calendrical.FiscalYear — pre-built fiscal calendars for 50+ territories (US, AU, UK, JP, …). The Calendrical.FiscalYear.calendar_for/1 factory creates a fiscal calendar for any supported ISO 3166 territory code.

  • Calendrical.Format and Calendrical.Formatter — calendar formatting via a behaviour-based plugin system. Includes Calendrical.Formatter.HTML.Basic, Calendrical.Formatter.HTML.Week, and Calendrical.Formatter.Markdown for rendering calendars to HTML and Markdown. Custom formatters can be added by implementing the Calendrical.Formatter behaviour.

  • Calendrical.Parse — parses ISO-8601 date and datetime strings into the calling calendar via parse_date/1, parse_naive_datetime/1, and parse_utc_datetime/1.

  • Calendrical.Preferencecalendar_from_locale/1 and calendar_from_territory/1 return the preferred calendar for a CLDR locale or ISO 3166 territory.

  • Calendrical.Ecclesiastical — Reingold-style algorithms for the dates of Christian liturgical events in a given Gregorian year, organized into three traditions:

    • Western (Roman Catholic / Anglican / most Protestants, Gregorian computus, results returned as Calendrical.Gregorian dates): easter_sunday/1, good_friday/1 (two days before), pentecost/1 (49 days after), advent/1 (the Sunday closest to 30 November), christmas/1 (25 December), epiphany/1 (first Sunday after 1 January, US observance).

    • Eastern Orthodox (Julian computus, results returned as Calendrical.Julian dates so the calendar context is visible): orthodox_easter_sunday/1, orthodox_good_friday/1 (two days before), orthodox_pentecost/1 (49 days after), orthodox_advent/1 (the start of the Nativity Fast on 15 November Julian — Eastern Orthodoxy has no movable "Advent Sunday" equivalent), eastern_orthodox_christmas/1 (25 December Julian, projected onto the Gregorian calendar).

    • Astronomical (the World Council of Churches' 1997 Aleppo proposal for unifying Western and Eastern Easter; not currently used by any Church, included for comparison; year range restricted to 1000..3000): astronomical_easter_sunday/1 (first Sunday strictly after the astronomical Paschal Full Moon), astronomical_good_friday/1 (two days before), paschal_full_moon/1 (the astronomical PFM itself, computed via Astro.equinox/2 and Astro.date_time_lunar_phase_at_or_after/2).

    Plus coptic_christmas/1 (29 Koiak Coptic) which doesn't fit cleanly into any of the three traditions.

    The module's moduledoc includes a comparison table showing the three Easter computations side-by-side.

  • Eleven exception modules in lib/calendrical/exception/, one per file, modeled after the Localize convention. Each has semantic struct fields, an exception/1 constructor that takes a keyword list, and a message/1 callback that uses Gettext.dpgettext/5 for translation:

  • Calendrical.Gettext — gettext backend for the Calendrical library, using the "calendrical" domain with four contexts: "calendar", "date", "format", and "option".

  • Embedded CLDR Umm al-Qura reference data sourced from R.H. van Gent's Utrecht University dataset (1356–1500 AH), cross-referenced against the KACST published tables. The data is encoded as compile-time module attributes and consumed via O(1) and O(log n) lookup.

Changed (vs. ex_cldr_calendars)

  • All Cldr.Calendar.* module names renamed to Calendrical.*. The detailed renaming map is in guides/migration.md.

  • The :cldr_backend option and the entire backend-module architecture have been removed. Calendrical reads CLDR data directly from Localize.Calendar at runtime; no compile-time backend module is required. Functions that previously took a :backend parameter no longer accept one.

  • Error returns use the modern Elixir convention {:error, %Exception{}} instead of the legacy two-tuple form {:error, {ExceptionModule, "message"}}. Callers can pattern-match on the exception's structured data fields (e.g. %Calendrical.MissingFieldsError{function: f, fields: fs}).

  • Exception names ending in non-Error suffixes have been renamed to use the Error suffix consistently with Localize (Calendrical.MissingFieldsCalendrical.MissingFieldsError, Calendrical.InvalidCalendarModuleCalendrical.InvalidCalendarModuleError, etc.).

  • Calendrical.Hebrew now uses CLDR's Tishri = 1 month numbering instead of Reingold's Nisan = 1 numbering. The previous numbering produced wrong localized month names because CLDR Hebrew data uses Tishri = 1.

  • Calendrical.shift_date/5 and Calendrical.shift_naive_datetime/9 now apply duration units in the standard order (years → months → weeks → days), matching the Elixir stdlib Date.shift/2 convention. The old Cldr.Calendar.plus(date, %Duration{}) applied units in the opposite order.

  • Calendrical.Duration has been removed. Use Elixir's built-in %Duration{} struct (since Elixir 1.17) and Date.diff/2 instead.

  • The plus/minus callbacks have been removed from the Calendrical behaviour. Calendar arithmetic is now driven exclusively by Date.shift/2 / NaiveDateTime.shift/2, which delegate to the calendar's shift_date/4, shift_time/5, and shift_naive_datetime/8 callbacks.

  • All conditional code that supported Elixir versions older than 1.17 has been removed. Calendrical now requires Elixir 1.17+ and Erlang/OTP 26+, matching Localize. Removed 24 obsolete Code.ensure_loaded? / function_exported? / Version.match? guards across 7 files.

  • Calendrical.paschal_full_moon/1 has moved to Calendrical.Ecclesiastical.paschal_full_moon/1. The new home is alongside the rest of the Christian-calendar functions.

Removed

  • Cldr.Calendar.Duration — replaced by Elixir's built-in %Duration{}.

  • The MyApp.Cldr.Calendar.* backend modules and the cldr_backend_provider/1 callback. All locale data is now read from Localize at runtime.

  • Calendrical.plus/{4,5,6}, Calendrical.minus/{4,5,6}, the plus/6 callback in Calendrical.Behaviour, and the corresponding :months clause in Calendrical.Base.Month and Calendrical.Base.Week. Use Date.shift/2 / NaiveDateTime.shift/2 instead.

  • Calendrical.Sigils (the ~d sigil). Elixir's native ~D sigil has supported a trailing calendar suffix since Elixir 1.10 and works for any module implementing the Calendar behaviour. Use ~D[2024-09-01 Calendrical.Hebrew] instead of ~d[2024-09-01 Hebrew]. The Calendrical.Sigils sigil's other features (default of Calendrical.Gregorian, ISO week-date format yyyy-Wmm-dd, fiscal calendar shortcuts, B.C.E./C.E. era markers) are minor conveniences that did not justify maintaining a parallel sigil system. See guides/migration.md for one-line equivalents of every removed feature.

Calendars

This release introduces 17 calendar implementations covering every CLDR-acceptable calendar type. See guides/calendar_summary.md for the full list grouped by family, with month structures, leap-year rules, and reference dates.