Changelog
Copy MarkdownAll 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
Calendrical.Date.parse/2now returns the parsedDatein the calendar named by the:calendaroption (e.g.~D[5786-09-29 Calendrical.Hebrew]forcalendar: :hebrew), instead of always returningCalendar.ISO. Passreturn_calendar: :isoto force the previous behaviour.Calendrical.Date.parse_range/2keeps returning ISO-GregorianDate.Rangeendpoints —Date.Rangeis hard-coded toCalendar.ISOin Elixir stdlib.
Added
TR35 date pattern letters —
Q/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).Eweekday names are now validated against the constructed date instead of consumed and discarded.TR35 flexible day periods (
B) —Calendrical.Time.parse/2recognises locale-specific flex period names ("in the morning","at night","noon","midnight") and uses them to disambiguate AM/PM for 12-hour cycles when noamarker is present.TR35 time zone resolution —
Calendrical.DateTime.parse/2now returns aDateTime(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). NewCalendrical.TimeZone.resolve/3. IANA-name resolution requires the host application to depend on:tzdataor:tz(detected at runtime); without one, IANA names fall back to aNaiveDateTime.All CLDR
availableFormatsskeletons are iterated on parse, not just the fourdateStyle/timeStylereferences. The standards are themselves keys intoavailableFormats, so this both subsumes the previous narrower set AND admits inputs like"3-5-1960"(matches:yMdskeleton"M/d/y"under lenient separator equivalence) and"week 20 of 2026"(matches:ywskeleton"'week' w 'of' Y").New
Calendrical.Time.Parser.parse_with_zone/2— same asparse/2but 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
minutecapture — 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, andCalendrical.Korean.new/3rejected valid{m, :leap}inputs in the documented traditional notation — the validator compared the user's traditional month number against the ordinal position returned byleap_month/1, which is always off by one. The check now correctly converts ordinal to traditional before comparing, the private helper has been renamedvalid_traditional_date?/5to disambiguate from the 3-arityvalid_date?/3callback used byDate.new/4, and the publicDate.new/4ordinal contract is unchanged.Test support module renamed from
Calendrical.DatetoCalendrical.Test.DateGeneratorto free theCalendrical.Datenamespace for the new parser module. Affectstest/property_test.exsandtest/day_of_week_test.exsonly — no public API impact.
Added
traditional_leap_month/1on 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 toleap_month/1which returns the ordinal position (1..13).Calendrical.Time.parse/2andCalendrical.DateTime.parse/2— locale-aware time and date-time parsers completing the parser trio alongsideCalendrical.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.TimeParseErrorandCalendrical.DateTimeParseError— structured errors carrying:inputand:locale.Calendrical.Date.parse/2— locale-aware parser for user-typed date strings across every Calendar-behaviour module exposingcldr_calendar_type/0(Gregorian, Buddhist, Japanese imperial, Islamic, Persian, Hebrew, ROC, Coptic, Ethiopic, Indian, …). Handles CLDRlenient-scope-dateseparator equivalences, non-Latin digit transliteration, 2-digit year pivoting, and era markers — seeCalendrical.Date.Parserfor the full strategy.Calendrical.Date.parse_range/2— locale-aware range parser. Accepts either a single string (split on CLDR'sintervalFormatFallbackseparator) 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.DateParseErrorandCalendrical.DateRangeParseError— structured errors carrying:input,:locale,:calendar, plus:reasonand:causefor 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 theCalendarcallbacks) and traditional months (used bynew/3and the return value oflunar_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/3andnew!/3docstrings on each lunisolar calendar now state explicitly that thelunar_monthargument is traditional (1..12 with{m, :leap}for the intercalary), with examples showing how the traditional number maps to the ordinal stored on the resultingDate.tstruct.
[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— adefmacro __using__template that supplies sensible default implementations of everyCalendarandCalendricalcallback. Calendarsusethe behaviour, supply an:epoch(and any non-default options), definedate_to_iso_days/3anddate_from_iso_days/1, and override only the callbacks that differ from the defaults. Every generated function isdefoverridable. Seeguides/calendar_behaviour.md.All 17 CLDR-acceptable calendar types are implemented:
Calendrical.Gregorian,Calendrical.ISO,Calendrical.ISOWeek,Calendrical.NRF— month- and week-based Gregorian calendars.Calendrical.Julianand the year-start variantsCalendrical.Julian.Jan1,Calendrical.Julian.March1,Calendrical.Julian.March25,Calendrical.Julian.Sept1,Calendrical.Julian.Dec25.Calendrical.Buddhist— Thai Buddhist Era (Gregorian + 543).Calendrical.Roc— Republic of China / Minguo (Gregorian − 1911).Calendrical.Japanese— proleptic Gregorian with Japanese era data for localization.Calendrical.Indian— Indian National (Saka) calendar with custom 30/31-day month structure and Saka era (Gregorian − 78).Calendrical.Persian— astronomical Persian calendar based on the vernal equinox at Tehran, computed viaAstro.equinox/2.Calendrical.CopticandCalendrical.Ethiopic— 13-month tabular calendars sharing themod(year, 4) == 3leap-year rule, with overriddenquarter_of_year/3,day_of_week/4, andvalid_date?/3.Calendrical.Ethiopic.AmeteAlem— Ethiopic calendar with the Era of the World (Anno Mundi) year offset of +5500 over the standard Era of Mercy.Calendrical.Islamic.CivilandCalendrical.Islamic.Tbla— tabular Hijri calendars with the Type II Kūshyār 30-year leap cycle. They share a privateCalendrical.Islamic.Tabularhelper and differ only in epoch (Friday 16 July 622 Julian vs Thursday 15 July 622 Julian).Calendrical.Islamic.UmmAlQura— Saudi Umm al-Qura tabular calendar embedding the official KACST/van Gent first-of-month dataset (1356–1500 AH) at compile time. Conversions are O(1) forward and O(log n) reverse via binary search.Calendrical.Islamic.UmmAlQura.Astronomical— Astronomical implementation of the Umm al-Qura rule using theAstrolibrary's sunset/moonset and lunar phase functions for Mecca. Available for research and validation against the embedded table.Calendrical.Islamic.ObservationalandCalendrical.Islamic.Rgsa— observational Islamic calendars using actual crescent visibility computed byAstro.new_visible_crescent/3(Odeh 2006 criterion). The two share a privateCalendrical.Islamic.Visibilityhelper and differ only in observation location (Cairo vs Mecca al-Masjid al-Ḥarām).Calendrical.Hebrew— arithmetic Hebrew calendar with the molad of Tishri and Lo ADU Rosh postponement rules. Public API uses CLDR's Tishri = 1 month numbering with month 6 (Adar I) only valid in leap years. Overridesmonth_of_year/3to return{7, :leap}for Adar II so localization picks up the CLDR7_yeartype_leapvariant.Calendrical.Chinese,Calendrical.Korean(Dangi), andCalendrical.LunarJapanese— lunisolar calendars sharing aCalendrical.Lunisolarbase implementation. UseAstrofor lunar phase and winter solstice calculations at Beijing/Seoul/Tokyo respectively.
Calendrical.Composite— adefmacro __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-builtCalendrical.EnglandandCalendrical.Russiamodules demonstrate the historical Julian-to-Gregorian transitions.Calendrical.Era— an@after_compilehook that auto-generates aCalendrical.Era.<CalendarType>module from CLDR era data. Calendarsuse Calendrical.Behaviourget era support for free without writing any era boundary code. ETS-based locking coordinates module creation for calendars that share acldr_calendar_type.Calendrical.localize/3— locale-aware names for:era,:quarter,:month,:day_of_week,:days_of_week,:am_pm, and:day_periodsparts of any date. Falls through to all 766+ CLDR locales available fromLocalize.Calendar. Handles the CLDR_yeartype_leapvariant for Hebrew Adar II without needingmonth_patternssubstitution.Calendrical.strftime_options!/1— returns a keyword list compatible withCalendar.strftime/3so the standard library's formatter can produce locale-aware output for any Calendrical calendar.Calendrical.shift_date/5andCalendrical.shift_naive_datetime/9— calendar-aware date/datetime shifting that supports the standardDate.shift/2andNaiveDateTime.shift/2APIs across every Calendrical calendar.Calendrical.Interval—Date.Rangefor years, quarters, months, weeks, and days in any supported calendar. TheCalendrical.Interval.relation/2function 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, …). TheCalendrical.FiscalYear.calendar_for/1factory creates a fiscal calendar for any supported ISO 3166 territory code.Calendrical.FormatandCalendrical.Formatter— calendar formatting via a behaviour-based plugin system. IncludesCalendrical.Formatter.HTML.Basic,Calendrical.Formatter.HTML.Week, andCalendrical.Formatter.Markdownfor rendering calendars to HTML and Markdown. Custom formatters can be added by implementing theCalendrical.Formatterbehaviour.Calendrical.Parse— parses ISO-8601 date and datetime strings into the calling calendar viaparse_date/1,parse_naive_datetime/1, andparse_utc_datetime/1.Calendrical.Preference—calendar_from_locale/1andcalendar_from_territory/1return 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.Gregoriandates):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.Juliandates 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 viaAstro.equinox/2andAstro.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, anexception/1constructor that takes a keyword list, and amessage/1callback that usesGettext.dpgettext/5for translation:Calendrical.IncompatibleCalendarError— fields:from,:to.Calendrical.IncompatibleTimeZoneError— fields:from,:to.Calendrical.InvalidCalendarModuleError— field:module.Calendrical.InvalidDateOrderError— fields:from,:to.Calendrical.MissingFieldsError— fields:function,:fields.Calendrical.InvalidPartError— fields:part,:valid_parts.Calendrical.InvalidTypeError— fields:type,:valid_types.Calendrical.InvalidFormatError— fields:format,:valid_formats.Calendrical.IslamicYearOutOfRangeError— fields:year,:min_year,:max_year.Calendrical.Formatter.UnknownFormatterError— field:formatter.Calendrical.Formatter.InvalidDateError— field:date.Calendrical.Formatter.InvalidOptionError— fields:option,:value.
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 toCalendrical.*. The detailed renaming map is inguides/migration.md.The
:cldr_backendoption and the entire backend-module architecture have been removed. Calendrical reads CLDR data directly fromLocalize.Calendarat runtime; no compile-time backend module is required. Functions that previously took a:backendparameter 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-
Errorsuffixes have been renamed to use theErrorsuffix consistently with Localize (Calendrical.MissingFields→Calendrical.MissingFieldsError,Calendrical.InvalidCalendarModule→Calendrical.InvalidCalendarModuleError, etc.).Calendrical.Hebrewnow 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/5andCalendrical.shift_naive_datetime/9now apply duration units in the standard order (years → months → weeks → days), matching the Elixir stdlibDate.shift/2convention. The oldCldr.Calendar.plus(date, %Duration{})applied units in the opposite order.Calendrical.Durationhas been removed. Use Elixir's built-in%Duration{}struct (since Elixir 1.17) andDate.diff/2instead.The
plus/minuscallbacks have been removed from theCalendricalbehaviour. Calendar arithmetic is now driven exclusively byDate.shift/2/NaiveDateTime.shift/2, which delegate to the calendar'sshift_date/4,shift_time/5, andshift_naive_datetime/8callbacks.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/1has moved toCalendrical.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 thecldr_backend_provider/1callback. All locale data is now read fromLocalizeat runtime.Calendrical.plus/{4,5,6},Calendrical.minus/{4,5,6}, theplus/6callback inCalendrical.Behaviour, and the corresponding:monthsclause inCalendrical.Base.MonthandCalendrical.Base.Week. UseDate.shift/2/NaiveDateTime.shift/2instead.Calendrical.Sigils(the~dsigil). Elixir's native~Dsigil has supported a trailing calendar suffix since Elixir 1.10 and works for any module implementing theCalendarbehaviour. Use~D[2024-09-01 Calendrical.Hebrew]instead of~d[2024-09-01 Hebrew]. TheCalendrical.Sigilssigil's other features (default ofCalendrical.Gregorian, ISO week-date formatyyyy-Wmm-dd, fiscal calendar shortcuts, B.C.E./C.E. era markers) are minor conveniences that did not justify maintaining a parallel sigil system. Seeguides/migration.mdfor 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.