Repository navigation
Calendrical version 2.0.0
[2.0.0] — 2026-10-24
Calendrical 2.0 rebuilds every calendar on a kernel of eight callbacks and derives the rest. The migration guide describes each change in full.
Breaking changes
-
The
Calendricalbehaviour has eight callbacks:info/0,date_to_iso_days/3,date_from_iso_days/1,leap_month/1,written_date/3,date_from_written_date/3,shift/6anddiff/3. The other callbacks of 1.3 are derived functions every calendar still answers but cannot override. -
use Calendrical.Behaviourisuse Calendrical.Calendar, withmonths_in_year:andweeks:in place of the old month and week options. A maker refuses an option it does not read at compile time. -
Every error is
{:error, exception}::invalid_date,:invalid_month,:undefined,:not_defined,:year_out_of_rangeand:unknown_territoryare exceptions, mapped in the migration guide. Only theCalendarparse callbacks,date_from_written_date/3andwritten_month_from_cldr_month/2keep their atoms. -
days_in_month/2returns{:error, %Calendrical.InvalidMonthError{}}in every calendar for a month its year does not have, where it raisedFunctionClauseErroror answered0. -
Calendrical.ReformreturnsCalendrical.UnknownReformErrorfor a territory it has no reform for, and the lunisolarnew!/3raisesCalendrical.InvalidDateError. -
shift/6returnsCalendrical.InvalidDateErrorfor a{year, month, day}that is no date, where it shifted the fields to another non-date;Date.shift/2raises it.diff/3returns the same error, andCalendrical.MissingFieldsErrorfor a date missing a field, where it counted or raised by calendar, and both returnCalendrical.InvalidPartErrorfor a part other than:years,:quarters,:months,:weeksand:days. -
months_in_year/1returnsCalendrical.MissingFieldsErrorfor anilyear in every calendar, where some raised and others answered a count. -
use Calendrical.Base.Month,use Calendrical.Base.Weekanduse Calendrical.Julianareuse Calendrical.Calendar.Shiftedanduse Calendrical.Calendar.Weeks, andCalendrical.new/3takes their options (year_begins:,year_ends:,year_number:,pattern:). The guide maps each old option. -
plus/6isshift/6and:coerceis:clamp,trueby default in every calendar, including the Julian calendars where:coercedefaulted tofalse. -
Every calendar numbers whole weeks by TR35's rule from its own first day of the week, and
weeks_in_year/1answers an integer. The hand-written, Julian and composite calendars cut their weeks at the year's ends in 1.3. -
A Hebrew date's month is its position in the year (Adar is 6 and Nisan 7 in an ordinary year), as
Date.new/4and ICU expect, while its name stays CLDR's. The calendar has a year 0, as itsinfo/0states and the other calendars of one era do, so the years before 1 are 0, -1 and so on. -
month_of_year/3is the CLDR month a date's month is named by, whichcardinal_month/1used to add: a fiscal year begun in July answers 7 for its month 1. -
calendar_year/3,epoch_day_of_week/0,first_day_of_week/0,last_day_of_week/0,__config__/0,Calendrical.start_end_gregorian_years/2and theDateand tuple forms ofleap_year?/1,leap_month/1,leap_month?/2,cyclic_year/3,lunar_month_of_year/2anddate_to_iso_days/3are removed. -
The Julian year-start variants'
date_from_julian_date/3isdate_from_base_date/3, beside a newbase_date/3. They carry counted months, so a variant whose year begins within a month (March25,Dec25) has thirteen. -
Calendrical.Julian.Dec25andCalendrical.Julian.Sept1number a year by the Julian year it ends in, as the Nativity and Byzantine years were reckoned. The Julian year-start variants take a date's era from its label year. -
A date of a calendar of weeks (
Calendrical.ISOWeek,Calendrical.NRF) is a year, a month of its pattern and a day of that month, so the second day of week 25 is~D[2026-06-16 Calendrical.NRF].week_of_year/3anddate_from_week_date/3convert, and~Dstill reads2026-W25-2. -
A calendar of weeks names its months "M01" to "M12" from CLDR's generic calendar (
cldr_calendar_type/0is:generic), from the Gregorian month its year begins in, so NRF's first month, which begins in February, is "M02"; it keeps Gregorian era names. -
days_in_month/1answers a composite with the range its members span, and month lengths that are not consecutive are a list,[28, 35]for a week calendar's last month. -
A month or week calendar's
day_of_era/3counts era 1 from the first day of calendar year 1, so NRF's days count 34 fewer. -
Calendrical.TimeZone.resolve/3isLocalize.DateTime.Timezone.resolve/3: errors are exceptions, abbreviations resolve only in a locale whose CLDR data writes them, and a repeated hour is read in standard time. -
Calendrical.strftime/3returns{:ok, string}or{:error, exception}where it raised;Calendrical.strftime!/3returns the string. It passesCalendar.strftime/3's own options through. -
The parse functions delegate to Localize's parsers and return its errors, so
Calendrical.DateParseError,DateRangeParseError,DateTimeParseError,TimeParseErrorandParseErrorare removed.week N of YYYYcounts the calendar's own weeks and an ISO 8601 offset is kept. -
The parse functions'
:calendaroption is a calendar module and the date is returned in it;:return_calendaris removed and returns aCalendrical.RemovedOptionError. A CLDR calendar name such as:hebrewisLocalize.UnknownCalendarError. -
Calendrical.Coptichas CLDR 49's single era, so its years before 1 are 0, -1 and so on.Calendrical.Ethiopicgives them to Amete Alem: year -50 is{5450, 0}. -
A composite's member counts its own months, so England's 1155 is the Lady Day year from 25 March 1155 to 24 March 1156. The Julian days 1 January to 24 March 1155, whose dates it takes, have no dates of their own.
-
Calendrical.Composite.new/2anduse Calendrical.Compositerefuse a configuration a composite cannot keep with aCalendrical.InvalidCompositeError, wherenew/2returned atoms such as:no_calendars_configured, and an option other than:calendarsand:base_calendarwith aCalendrical.UnknownOptionError.new/2returns{:ok, module}for a module that exists already, asCalendrical.new/3does, where it returned{:module_already_exists, module}. -
Every calendar maker, and
Calendrical.new/3, refuses an option it does not read with aCalendrical.UnknownOptionErrorand a value it cannot take with aCalendrical.InvalidOptionValueError, whichCalendrical.new/3also returns for a kind other than:monthand:week.Calendrical.FiscalYear.calendar_for/1returnsCalendrical.UnknownFiscalYearErrorfor a territory with no fiscal calendar. -
date_at/2,solar_term/2andCalendrical.Lunisolar.solar_term_name/1return exceptions for errors:Calendrical.InvalidOptionValueError,Calendrical.UnsupportedDateRangeError,Calendrical.NoDuskErrorandCalendrical.InvalidSolarTermError.
Added
-
Calendrical.Info, the facts of a calendar as data, within_effect/4for composites;info().month_lengthsstates a calendar's month lengths where sampling them is slow. -
Calendrical.Month,Calendrical.Week,Calendrical.Period,Calendrical.EraandCalendrical.Shift, the derived API every calendar module delegates to. -
Calendrical.Calendar.Shifted,Calendrical.Calendar.WeeksandCalendrical.Calendar.Lunisolar, the makers of a year begun elsewhere, a year of whole weeks and a calendar reckoned from the sky. -
written_date/3,date_from_written_date/3andleap_month/1on every calendar. -
Calendrical.diff/3and adiff/3callback on every calendar count whole years, quarters, months, weeks or days asshift/6adds them. -
add/9, the callbackCalendarIntervalsteps an interval of a year or month with, on every calendar, where only month and week calendars had it. -
Quarters on every calendar, and
quadrimester/2andsemester/2withCalendrical.Interval.quadrimester/3andsemester/3. -
solar_term/2on every calendar andCalendrical.Lunisolar.solar_term_name/1give the date and name of each of the 24 solar terms. -
Calendrical.Vietnamese, the Chinese lunisolar calendar observed from Hanoi's meridian, so Tết differs from the Chinese New Year in 1968, 1969, 1985, 2007, 2030 and 2053. -
Calendrical.Reform.England, England's 1155, 1751 and 1752 calendar reforms as a composite calendar. -
date_at/2on the Islamic and Hebrew calendars maps an instant to a date under a:day_startof:midnight(the default),:evening,:sunsetor (Hebrew):nightfall. -
dates_in_gregorian_year/3on every calendar gives the dates of a calendar month and day within a Gregorian year. -
calendar_from_cldr_calendar_type/1andadditional_calendars/0give every calendar an IXDTF[u-ca=…]identifier, and Calendrical registers itself as Localize's calendar provider. -
Calendrical.named_month/3, the days of a named month in the order of time. -
A composite calendar's
parsing_calendars/0names its members, so a date written in a member's formats reads back. -
Calendrical.Hebrewconverts between a month's position and its RFC 7529 traditional month, as the lunisolar calendars do. -
mix calendrical.umm_al_qura.verifyaudits the embedded Umm al-Qura tables, and with--kacstcompares them with KACST's data.
Changed
-
Requires
localize ~> 1.4andastro ~> 2.8. -
The optional
tz_worldrequirement is~> 2.3, where it was~> 1.0. -
The package's licenses are
Apache-2.0andUnicode-3.0: the Umm al-Qura months for 1501 to 1600 AH are derived from ICU. -
Calendrical.Islamic.UmmAlQurauses KACST's official month lengths for 1 to 1500 AH and ICU's for 1501 to 1600 AH, and the civil calendar outside them. -
Calendrical.Persiancomputes its new year from the bundled ephemeris for Persian years 1279 to 1479 and uses ICU's arithmetic cycle outside them.new_year_gregorian/1andyear_end_gregorian/1cover Gregorian years 1900 to 2100. -
Japanese era boundaries before Meiji are the proleptic Gregorian dates Localize publishes, and
Calendrical.LunarJapanesecounts an era's years from the lunar year of its proclamation. -
Calendrical.Preferenceresolves-u-ca-iso8601toCalendrical.ISO, the Gregorian calendar with ISO 8601's week rules, instead ofCalendrical.ISOWeek. -
quarter_of_year/3answers for the Hebrew, Coptic and Ethiopic calendars, where it returned{:error, :not_defined}. -
next/3andprevious/3step a week range to the week holding the day after or before it, so a week cut short at a year's end is never skipped. -
The lunisolar calendars compute each new moon once per question, a quarter as many across a holiday corpus.
-
The lunisolar calendars answer for the 40,001 years counted from their epoch as Gregorian years -20,000 to 20,000; beyond them
valid_date?/3isfalseand conversions raiseCalendrical.UnsupportedDateRangeError, where a far year took minutes.
Fixed
-
Calendrical.Islamic.ObservationalandCalendrical.Islamic.Rgsabegin a month on the day after the evening the crescent is first seen, where they began a day early. -
Every calendar answers a question about a date only for a date it has: a
Calendrical.InvalidDateErrorfor a datevalid_date?/3rejects, and aCalendrical.MissingFieldsErrorfor a missing field, asquarter/2,quadrimester/2,semester/2andweek/2now do too. -
month_of_year/3andquarter_of_year/3answer a partial date from its year and month, checking the month without a year, soCalendrical.strftime/3names a partial date's month as it names a whole date's. -
Calendrical.Lunisolar.day_name/1andday_name_on_or_before/2count from the right epoch, so 1 October 1949 is 甲子; days were named five places on, andday_name_on_or_before/2did not return a day of the name. -
The observational Islamic calendars refuse a date far outside the ephemeris at once, where it took seconds or more.
-
leap_year?/1raisesFunctionClauseErrorfor anything but an integer year in every calendar, asCalendar.ISOdoes, andleap_month/1answersnilfor one in every calendar. -
year_of_era/1returns aCalendrical.MissingFieldsErrorfor a missing year in the Coptic, Ethiopic and Julian calendars, as every other calendar does. -
A Julian year-start variant's or fiscal calendar's written and base dates answer a partial date with
nilfields, where they raisedArithmeticError. -
gregorian_date_for_lunar/3raisesCalendrical.InvalidDateErrorfor a lunar month or day the year lacks, where it returned another day's date. -
valid_date?/3isfalserather than raising for a month or day below 1, for non-integer parts and for a date outside a calendar's supported range. -
days_in_month/1answers a month with no year in every calendar: its length, or{:ambiguous, range}where it varies, and aCalendrical.NotDefinedErrorfor a month the calendar lacks. -
days_in_month/2no longer gives a length for a month the year does not have: the lunisolar calendars answered 30 for a 13th month of a 12-month year, and Persian andReform.Sweden.Transitional30 or 31 for months 0 and 13. -
Calendrical.localize/3returnsCalendrical.InvalidDateErrorfor:era,:day_of_weekand:days_of_weekof a date its calendar does not have, where it answerednilor raised. -
The astronomical Ecclesiastical functions return
Calendrical.UnsupportedDateRangeErrorfor a year outside 1000 to 3000, where they raisedFunctionClauseError. -
Calendrical.Persian,Calendrical.EcclesiasticalandCalendrical.Islamic.UmmAlQura.Astronomicalreturn an exception for anAstroerror, where they passed its atom through. -
The lunisolar
new/3checks a leap month's day against that month's own length, and a lunisolar calendar reads a day's year as the year its new year begins. -
Calendrical.LunarJapaneseconverts each lunar year once, and a:lunar_japanese_epochin late July or August no longer duplicates or skips years. -
Date.shift/2adds years and months together and clamps the day once, asCalendar.ISOdoes, and lunisolar and Hebrew shifts walk each year's own months. -
A week-based calendar's
shift/6places the day of the month once, and itsday_of_year/3numbers the first day 1. -
The composite calendars add years, months, weeks and days across a transition, count
diff/3andday_of_era/3through a change of calendar, and read each date in the calendar that has its year. -
A composite calendar names a date's months, eras and cycles from the member in effect, a partial date's included where one member holds all of it, so
Calendrical.Reform.Japannames its lunisolar months and Meiji era. -
A composite calendar writes a day with no date of its own as the first later day that has one, and names a month a change began part of the way through from its year and month, so England's March 1751 is "Mar 1751".
-
A Julian year-start variant's dates agree with the Julian calendar's month lengths, weeks and leap days, it has no year 0, and its months and years shift in the Julian months that name them.
-
extended_year/3counts a Julian year BC from 0 down, as TR35 defines it. -
Calendrical.KoreanandCalendrical.LunarJapaneseplace a year in the sexagenary cycle by its related Gregorian year, as ICU does, and every lunisolarcyclic_year/3returns that place. -
A date before a calendar's first era counts back from it in
year_of_era/3andday_of_era/3, where it raised, and a before era counts back from its last day. -
Calendrical.strftime/3names the month and day of the week from the date, so Hebrew, Chinese leap and fiscal months are named correctly. -
A month calendar's
week_of_month/3follows TR35, and itsiso_week_of_year/3gives the ISO 8601 week of the day its date names. -
Calendrical.Interval.day/3answers for every calendar, every constructor acceptsCalendar.ISO, and the date forms return an error for a date the calendar lacks wheremonth/1andday/1raised. -
Calendrical.first_day_of_year/2,last_day_of_year/2and the Gregorian-day functions answer for every calendar, where most raisedUndefinedFunctionError. -
Calendrical.interval/3andinterval_stream/3order their dates by day, which the Julian year-start calendars need. -
Calendrical.calendar_from_locale/1maps-u-ca-<name>throughcalendar_from_cldr_calendar_type/1, and a value that is no locale is an error where it raised. -
Calendrical.Islamic.UmmAlQura.first_day_of_month/2returns aCalendrical.InvalidMonthErrorfor a month outside 1 to 12, where it reported an out-of-range year. -
Calendrical.Composite.new/2andCalendrical.Reform.calendar_for/1create a calendar once when several processes ask at once, where all but one raisedCompileError. -
Every calendar's
parse_date/1,parse_naive_datetime/1andparse_utc_datetime/1read a date asCalendar.ISOdoes:{:error, :invalid_format}for text that is no date, and a+before a four-digit year. -
A calendar of weeks writes its year with four digits at least, so a date before year 1000 reads back: "0004-02-35".
-
The parse functions keep a month name that is also a weekday name, accept format and stand-alone month names, parse dates with eras, resolve flexible day periods and Hebrew month names, and return errors where they raised.
-
as: :mapparsing gives a fixed offset such as "GMT+5" theDateTimezone fields, where the map kept the text with no offset. -
Calendrical.TimeZone.resolve/3keeps the offset of a name specific to standard or daylight time: "14:00 EST" in May is 14:00 at -05:00. -
Calendrical.localize/3names the month of a composite or Julian year-start date, andCalendrical.Formatlays out calendars without weeks and leaves out months a year lacks. -
The tabular Islamic calendars convert dates before the Hijra with floored division.
-
Calendrical.Reform.Sweden.Transitionalhas no year 0, as the Julian calendar has none, and round-trips every day from 1700 to 1712. -
Calendrical.Era.year_of_era/4,day_of_era/4and every calendar'syear_of_era/1answer as the calendar's ownyear_of_era/3andday_of_era/3do, where they reckoned frominfo/0and disagreed with a calendar that defines its own:Calendrical.Reform.Japan.year_of_era(1000)is{21, 198}, asyear_of_era(1000, 1, 1)is. -
Calendrical.Reform.TRchanges calendar on 1 January 1926, where its data said 1927. -
A composite calendar's
leap_year?/1isfalsefor a year a change of calendar took the leap day from, so Sweden's 1700, with no 29 February, is no leap year, where it answered the Julian calendar'strue. -
numeric_month/3,cardinal_day/3andcardinal_year/3returnCalendrical.InvalidDateErrorfor a date the calendar does not have, where they echoed its figures. -
Calendrical.PersianandCalendrical.LunarJapanesestate the first day of their year 1 asinfo().epoch, where the Persian fact was two days after it and the Lunar Japanese fact the Taika proclamation day, five months after it; the default:lunar_japanese_epochis~D[0645-02-05]. -
Calendrical.Hebrew.date_from_iso_days/1reads the days before year 1, where its estimate of the year was one too high. -
Calendrical.Era.era_data/1raisesLocalize.UnknownCalendarErrorfor a type CLDR does not define, andday_of_week/4raisesCalendrical.InvalidOptionValueErrorfor astarting_onthat is no weekday, where both raisedArgumentError.