Composite calendars

Copy Markdown

A composite calendar stitches two or more calendars together at historical cut‑over dates, so a single calendar module can represent a place whose calendar system changed over time. This is exactly what happened across the world as territories moved from the Julian to the Gregorian calendar — and, in Japan's case, from a lunisolar calendar to the Gregorian one.

Calendrical.Composite builds these calendars. The resulting module is an ordinary Calendar implementation: it works with Date.new/4, Date.shift/2, Date.convert/2, Calendrical.Interval, the ~D sigil, and every other Calendrical function.

Defining a composite

A composite is declared with use Calendrical.Composite, a :base_calendar (the calendar in effect before the first transition) and a :calendars list of transition markers. Each marker is a date written in the calendar that takes effect on that day.

England is the classic example. It changed the civil New Year from 25 March to 1 January, and then dropped eleven days when it adopted the Gregorian calendar in 1752:

defmodule CompositeCalendar.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

Read the :calendars list as "on this day, switch to this calendar":

  • before 1155‑03‑25 — the base Calendrical.Julian calendar;
  • from 25 March 1155, the incoming calendar's month 1 day 1 — Julian with the year starting on 25 March (Lady Day), its months counted from that day;
  • from 1751‑03‑25 — Julian with the year starting on 1 January;
  • from 1752‑09‑14 — the proleptic Gregorian calendar.

Each marker must be a date its calendar has, and each must fall on a day of its own. The calendars are calendar modules of months, Calendar.ISO among them as Calendrical.Gregorian, and neither a calendar of weeks nor another composite calendar. No calendar may number its first year before the last year of the calendar before it, since the dates of a year carrying two calendars' numbers could name only one of its days; a year's number may stay the same, as England's 1155 does. use Calendrical.Composite raises for a configuration it cannot keep, and Calendrical.Composite.new/2 returns it as {:error, exception}: a Calendrical.InvalidCompositeError whose :reason names what makes no composite, or a Calendrical.UnknownOptionError for an option a composite does not read.

"Missing" days

When a territory adopts the Gregorian calendar it skips the days by which the Julian calendar had drifted. In September 1752 England went straight from the 2nd to the 14th, so that month has only 19 days and the days in between are not valid dates:

iex> CompositeCalendar.England.days_in_month(1752, 9)
19

iex> CompositeCalendar.England.valid_date?(1752, 9, 3)
false

iex> Date.shift(~D[1752-09-02 CompositeCalendar.England], day: 1)
~D[1752-09-14 CompositeCalendar.England]

The composite rejects the skipped days because their ISO day number belongs to the calendar on the other side of the transition — a date is valid only if it round‑trips to the same member calendar.

Changing when the year starts

Transitions can also change year numbering rather than skip days. Under the March‑25 (Lady Day) style a year counts thirteen months from 25 March, so the year 1750's last day is its month 13 day 24, the Julian 24 March 1751, and the day after it is 25 March 1751; after the switch to a January start, 1751 becomes the first year to run all the way to 31 December, followed directly by 1 January 1752:

iex> Date.shift(~D[1750-13-24 CompositeCalendar.England], day: 1)
~D[1751-03-25 CompositeCalendar.England]

iex> Date.shift(~D[1751-12-31 CompositeCalendar.England], day: 1)
~D[1752-01-01 CompositeCalendar.England]

A year that begins after January holds months that come before the month it begins in, and a date in them is read in the calendar whose days carry its year, so the first year of a calendar that takes effect part of the way through its months has all of its dates. Where two days carry the same year, month and day, as 10 March does in England's 1155 and again in 1156, the date names one of them, and a shift or conversion that reaches the other answers the first later day that has a date of its own: Calendrical.Composite says which.

Multiple transitions — and a 30 February

Sweden has the most unusual history in Europe, and Calendrical.Reform.Sweden (shipped with Calendrical) models it faithfully with four segments. Sweden dropped the leap day in 1700 to begin a gradual transition, ran one day ahead of the Julian calendar until 1712, then reverted to Julian by inserting the only known 30 February, before finally adopting the Gregorian calendar in 1753:

# There was no 29 February 1700
iex> Calendrical.Reform.Sweden.valid_date?(1700, 2, 29)
false

# 30 February 1712 really existed in Sweden
iex> Calendrical.Reform.Sweden.valid_date?(1712, 2, 30)
true

iex> Calendrical.Reform.Sweden.days_in_month(1712, 2)
30

# The final Gregorian adoption skips eleven days in 1753
iex> Date.shift(~D[1753-02-17 Calendrical.Reform.Sweden], day: 1)
~D[1753-03-01 Calendrical.Reform.Sweden]

Russia is a simpler multi‑transition example — Byzantine year‑start styles followed by the Soviet 1918 Gregorian decree, which skipped thirteen days (1–13 February 1918).

Splicing a lunisolar calendar

Composites are not limited to Julian and Gregorian segments. Japan moved from a lunisolar calendar to the Gregorian one in 1873, and Calendrical.Reform.Japan splices Calendrical.LunarJapanese with Calendrical.Japanese:

# The last lunisolar day is followed directly by 1 January 1873 —
# no physical days are skipped, only the rest of the lunisolar month.
iex> Date.shift(~D[1228-12-02 Calendrical.Reform.Japan], day: 1)
~D[1873-01-01 Calendrical.Reform.Japan]

# Because the Gregorian side is Calendrical.Japanese, dates carry era years:
# 1873 is Meiji 6.
iex> Calendrical.Reform.Japan.year_of_era(1873, 1, 1)
{6, 232}

Runtime and territory‑driven composites

Composites can be built at runtime with Calendrical.Composite.new/2:

iex> Calendrical.Composite.new(CompositeCalendar.Denmark,
...>   calendars: [~D[1700-03-01 Calendrical.Gregorian]],
...>   base_calendar: Calendrical.Julian)
{:ok, CompositeCalendar.Denmark}

For the common Julian‑to‑Gregorian case, Calendrical.Reform already knows the reform date of every territory in the ncal(1) table and builds the calendar for you — accepting a territory code or a Localize.LanguageTag:

iex> Calendrical.Reform.calendar_for(:GB)
{:ok, Calendrical.Reform.GB}

iex> Calendrical.Reform.reforms()[:RU]
%{
  country: "Russia",
  last_julian: ~D[1918-01-31 Calendrical.Julian],
  first_gregorian: ~D[1918-02-14 Calendrical.Gregorian]
}

Calendrical.Reform.calendar_for/1 returns the curated Calendrical.Reform.Sweden and Calendrical.Reform.Japan for those two territories, and an ncal‑derived composite for the rest. See Calendrical.Reform for the reform dates, their sources, and the territories where a single national date is only an approximation.