Calendrical.Composite (Calendrical v2.2.0)

Copy Markdown

A composite calendar uses one base calendar before a specified transition date and a different calendar after, optionally chaining multiple transitions to model a sequence of historical calendar reforms.

The canonical example is the European transition from the Julian to the Gregorian calendar between the 16th and 20th centuries, where the calendar in use literally changed on a known date and a window of "missing" days appeared in the historical record.

Configuration

A composite calendar is built by useing this module with a :calendars option listing the first day on which a new calendar takes effect. Each entry must be a Date literal expressed in the calendar that takes effect on that day.

defmodule MyApp.England do
  use Calendrical.Composite,
    calendars: [
      ~D[1752-09-14 Calendrical.Gregorian]
    ],
    base_calendar: Calendrical.Julian
end

The :base_calendar option indicates the calendar in use before any of the configured transitions. It defaults to Calendrical.Julian, and it has no first day: every day before the first transition is a date of it.

What a composite calendar can hold

Each transition is a date of the calendar that takes effect on it, and each takes effect on a day of its own. Every calendar is a calendar module of months, Calendar.ISO among them as Calendrical.Gregorian: a calendar of weeks cannot be one, nor can another composite calendar, since a composite counts months through a transition.

A calendar must not number its first year before the last year of the calendar before it. The Hebrew calendar followed by the Gregorian from 1900 would give the Gregorian years from 1900 the numbers of Hebrew years from 1900 to 5660, and the dates of those years could name only one of their days. A year's number may stay the same through a transition, as it does where the day a year begins on changes (see below).

use Calendrical.Composite raises a Calendrical.InvalidCompositeError for a configuration it cannot keep, and new/2 returns it:

iex> {:error, %Calendrical.InvalidCompositeError{reason: :years_must_not_go_back}} =
...>   Calendrical.Composite.new(MyApp.HebrewThenGregorian,
...>     calendars: [~D[1900-01-01 Calendrical.Gregorian]],
...>     base_calendar: Calendrical.Hebrew
...>   )

Julian to Gregorian transition

One of the principal uses of this calendar is to define a calendar that reflects the historical Julian-to-Gregorian transition for individual countries.

Applicable primarily to western European countries and their colonies, the transition occurred between the 16th and 20th centuries. A strong reference is the Perpetual Calendar site maintained by Toke Nørby. Wikipedia's Adoption of the Gregorian calendar page is also useful.

Multiple compositions

A more complex example chains more than one calendar. For example, Egypt used the Coptic calendar from 238 BCE until Rome introduced the Julian calendar in approximately 30 BCE. The Gregorian calendar was then introduced in 1875. We can approximate this with:

defmodule MyApp.Egypt do
  use Calendrical.Composite,
    calendars: [
      ~D[-0045-01-01 Calendrical.Julian],
      ~D[1875-09-01 Calendrical.Gregorian]
    ],
    base_calendar: Calendrical.Coptic
end

Missing days

When a transition skips dates (for example the Swedish transition in 1753 dropped the eleven days 18 February 1753 through 28 February 1753), the composite calendar treats those dates as invalid:

iex> Calendrical.Reform.Sweden.valid_date?(1753, 2, 20)
false

iex> Date.shift(~D[1753-02-17 Calendrical.Reform.Sweden], day: 1)
~D[1753-03-01 Calendrical.Reform.Sweden]

A month a transition cuts short counts only the days that remain, and a year runs from whichever day it begins on, so a year that changes its first day is shorter or longer than usual:

iex> Calendrical.Reform.Sweden.days_in_month(1753, 2)
17

iex> Calendrical.Reform.England.days_in_year(1751)
282

Years that begin on another day

A calendar need not begin its years on 1 January: Calendrical.Julian.March25 begins them on 25 March and Calendrical.Julian.Sept1 on 1 September. Such a calendar counts its months from its year's first day, so a date's fields are always in the order of its days: a year reckoned from 1 September that takes effect on 1 September 1492 begins its year 1493 on that day, as month 1 day 1, and the January that follows is its month 5:

iex> {:ok, muscovy} =
...>   Calendrical.Composite.new(MyApp.Muscovy,
...>     calendars: [~D[1493-01-01 Calendrical.Julian.Sept1]]
...>   )
iex> Date.new!(1493, 1, 15, muscovy) |> Date.convert!(Calendrical.Julian)
~D[1492-09-15 Calendrical.Julian]
iex> Date.new!(1493, 5, 15, muscovy) |> Date.convert!(Calendrical.Julian)
~D[1493-01-15 Calendrical.Julian]

When the day a year begins on moves, the incoming calendar's label year owns its whole counted year, and the outgoing calendar's last stretch of days — whose labels the incoming calendar claims — has no dates of its own and is in no year. England's move to Lady Day, 25 March, in 1155 makes 25 March 1155 the first day of the Lady Day year 1155, and the days 1 January to 24 March 1155, which the January reckoning had called 1155, have no dates; historians write them with both years ("10 March 1154/5"). Russia's September year gave way to Peter the Great's January year on 1 January 1700 the same way: 1 September to 31 December 1699, which had begun the September year 1700, have no dates of their own.

England's 1155 is then a whole Lady Day year of 366 days, from its month 1 day 1, the Julian 25 March 1155, to its month 13 day 24, the Julian 24 March 1156. Calendrical.first_gregorian_day_of_year/2 and Calendrical.last_gregorian_day_of_year/2 give a year's first and last days in the Gregorian calendar:

iex> Calendrical.Reform.England.days_in_year(1155)
366

iex> Calendrical.last_gregorian_day_of_year(1155, Calendrical.Reform.England)
~D[1156-03-31 Calendrical.Gregorian]

iex> Calendrical.Reform.England.year(1155)
Date.range(~D[1155-01-01 Calendrical.Reform.England], ~D[1155-13-24 Calendrical.Reform.England])

A shift or a conversion that reaches a day with no date of its own answers the first later day that has one, as a shift into the days a reform took out answers the day after them:

iex> Date.convert!(~D[1155-01-15 Calendrical.Julian], Calendrical.Reform.England)
~D[1155-01-01 Calendrical.Reform.England]

iex> Date.shift(~D[1752-08-05 Calendrical.Reform.England], month: 1)
~D[1752-09-14 Calendrical.Reform.England]

Arithmetic across a transition

Years, quarters and months are added in the calendar in effect on the date, a year being as many months as that calendar counts. When the result falls under another calendar the months are counted on through each calendar's own months, and a day the resulting month does not have becomes the month's next day that exists, or its last day:

iex> Date.shift(~D[1752-08-20 Calendrical.Reform.England], month: 1)
~D[1752-09-20 Calendrical.Reform.England]

iex> Date.shift(~D[1752-08-05 Calendrical.Reform.England], month: 1)
~D[1752-09-14 Calendrical.Reform.England]

A change of calendar can begin a month part of the way through it, as England's year 1751 began on 25 March. Months and years are walked as the months that name the dates — a year reckoned from 25 March counts thirteen, its March split in two, and names twelve — so a month before 25 March 1751 is 25 February 1751, which the Lady Day year 1750 counted as its month 12, and a year after 25 March 1750 is 25 March 1751:

iex> Date.shift(~D[1751-03-25 Calendrical.Reform.England], month: -1)
~D[1750-12-25 Calendrical.Reform.England]

iex> Date.shift(~D[1750-01-01 Calendrical.Reform.England], year: 2)
~D[1752-03-25 Calendrical.Reform.England]

Eras

The era of a date, and its year of the era, are those of the calendar in effect on the date. The days of an era are one count through every change of calendar the era runs through: from the era's first day, as the calendar in effect when it began counts them, or, for an era counted back from its last day as the years before the common era are, as the calendar in effect when it ended counts them. So the day after 2 September 1752 in England is the next day of the common era, although the Gregorian calendar begins that era two days after the Julian calendar does:

iex> Calendrical.Reform.England.day_of_era(1752, 9, 2)
{639798, 1}

iex> Calendrical.Reform.England.day_of_era(1752, 9, 14)
{639799, 1}

An era runs through a change of calendar when the calendars either side of it name their eras from the same CLDR calendar and give the same era there. Where they do not, as with the Coptic and Julian calendars of Egypt above, each calendar's days of its era are its own.

The Islamic calendars are of that kind: each names its eras from a CLDR calendar of its own, so a composite of two of them keeps each one's count of the Hijri era. The counts agree but for Calendrical.Islamic.Tbla, which begins the era a day before the others. Where another Islamic calendar follows it a day of the era is counted twice, and where it follows another one is left out.

Summary

Types

The reason new/2 cannot create a composite calendar.

Functions

Creates a new composite calendar at runtime.

Types

error_reason()

@type error_reason() :: Exception.t()

The reason new/2 cannot create a composite calendar.

Functions

new(calendar_module, options)

@spec new(module(), Keyword.t()) ::
  {:ok, Calendrical.calendar()} | {:error, error_reason()}

Creates a new composite calendar at runtime.

Arguments

  • calendar_module is the module name to be created. This will be the name of the new composite calendar if it is successfully created.

  • options is a keyword list of options. See Calendrical.Composite for the supported options.

Options

  • :calendars (required) is a list of Date literals indicating the first day on which a new calendar takes effect. Each entry must be expressed in the calendar that takes effect on that day.

  • :base_calendar is the calendar in use before any of the configured transitions. Defaults to Calendrical.Julian.

Returns

  • {:ok, module} if the calendar is successfully created. The call is idempotent: a module of the name that exists already is returned unchanged, and is not verified to be a calendar module. Of the processes that create the same calendar at the same moment, one creates it and the others are answered the module it created.

  • {:error, exception} if the calendar cannot be created: a Calendrical.InvalidCompositeError whose :reason says what makes no composite (see "What a composite calendar can hold" above), a Calendrical.InvalidCalendarModuleError for a calendar that is no calendar module, or the exception a calendar raises for a date it does not reach.

Examples

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