Making a calendar
Copy MarkdownThis guide explains how to define a calendar in Calendrical 2.0: the kernel every calendar implements, the facts it states, the five makers that build calendars from those, and what a calendar may override.
The kernel
A Calendrical calendar implements Elixir's Calendar behaviour and the eight callbacks of the Calendrical behaviour. Six Calendar callbacks are primitives the calendar states for itself: leap_year?/1, months_in_year/1, days_in_month/2, valid_date?/3, year_of_era/3 and day_of_era/3. The Calendrical kernel is:
info/0— the static facts about the calendar as aCalendrical.Infostruct: the CLDR calendars that name its months and its eras, its epoch, whether it has a year 0, the fewest and the most months its years have, the rule its own weeks follow, the cycles its years run in, the place an astronomical calendar is reckoned at, the lengths its months can have, the calendar its dates are parsed in and, for a composite, its members.date_to_iso_days/3anddate_from_iso_days/1— the spine every conversion runs through: a date of the calendar to the proleptic Gregorian day countCalendar.ISOuses, and back.leap_month/1— the position in a year of its leap month, ornil.written_date/3anddate_from_written_date/3— the figures a date is written with and the date those figures name. A fiscal date is written as the Gregorian date it is; a Hebrew date writes its month's place in the year, as ICU does; most calendars write their own fields.shift/6anddiff/3— the calendar's arithmetic in years, quarters, months, weeks and days, under one clamping contract: withclamp: true, the default, a day the month reached lacks becomes its last day, a traditional month the year reached lacks becomes the month it repeats, and a count across a missing year 0 goes a year further.
Every other answer is derived from the kernel and the primitives by the derived API, Calendrical.Month, Calendrical.Week, Calendrical.Period, Calendrical.Era and Calendrical.Shift, plain functions that take the calendar first. Every calendar also carries those answers as functions of its own module, so a consumer that holds only a calendar module, as Localize does, asks it any question:
iex> Calendrical.Hebrew.month_of_year(5786, 7, 1)
8
iex> Calendrical.Month.cldr_month(Calendrical.Hebrew, 5786, 7)
8use Calendrical.Calendar: a hand-written calendar
The maker for a calendar whose spine the author writes.
defmodule MyApp.Julian do
use Calendrical.Calendar,
epoch: ~D[0001-01-01 Calendrical.Julian],
cldr_calendar_type: :gregorian,
weeks: [first_day: 1, min_days_in_first_week: 1]
@impl Calendar
def leap_year?(year), do: rem(year, 4) == 0
@impl Calendar
def days_in_month(year, 2), do: if(leap_year?(year), do: 29, else: 28)
def days_in_month(_year, month) when month in [4, 6, 9, 11], do: 30
def days_in_month(_year, _month), do: 31
@impl true
def date_to_iso_days(year, month, day), do: Calendrical.Julian.date_to_iso_days(year, month, day)
@impl true
def date_from_iso_days(iso_days), do: Calendrical.Julian.date_from_iso_days(iso_days)
endThe options are the facts of info/0:
:epoch(required) — the first day of year 1, as a date in a calendar that has already been compiled.:cldr_calendar_type— the CLDR calendar whose data names the months, days and eras,:gregorianby default.:era_calendar_type— the CLDR calendar whose eras the calendar counts years in, the CLDR calendar type by default.:months_in_year— the number of months in a year, or{fewest, most}where a leap year has more, as the Hebrew calendar's{12, 13}. The default is12.:weeks— the rule the calendar's own weeks follow:[first_day: 7, min_days_in_first_week: 1], with the first day numbered 1 for Monday to 7 for Sunday, or:from_first_dayfor weeks counted from the first day of the year. The default is Monday and 1.:year_zero?— whether the calendar has a year 0,trueby default.:year_cycle— the cycle the years run in, as the sexagenary cycle of 60.:years_in_cycle— the number of years after which the calendar's days and weekdays repeat, where they do.:location— the place an astronomical calendar is reckoned at, a function of the ISO day number.:month_lengths— the lengths a month can have, by month, where the calendar states them rather than having them sampled.:parsing_calendar— the calendar a written date is parsed in, the calendar itself by default.
months_in_year/1, valid_date?/3, year_of_era/3, day_of_era/3, shift/6 and diff/3 have defaults the calendar may replace, and leap_month/1, written_date/3 and date_from_written_date/3 default to no leap month and a date written as its own fields. An option the maker does not read is refused at compile time. A definition of any other generated function is not refused but is unreachable: the generated delegation comes first, the compiler warns that the later clause can never match, and the generated answer stands, so compile with --warnings-as-errors to catch one.
use Calendrical.Calendar.Shifted: a year begun elsewhere
A fiscal year, or a year counted from Lady Day as the Old Style did, is a base calendar's year cut at another day. Its date is a counted month and a day of it, so a year begun within a base month has a short month 1 and a short last month.
defmodule MyApp.FiscalFromJuly do
use Calendrical.Calendar.Shifted, year_begins: {7, 1}, year_number: :ending
end
defmodule MyApp.LadyDay do
use Calendrical.Calendar.Shifted,
base: Calendrical.Julian,
year_begins: {3, 25},
year_number: :beginning,
written_year: :label
end:base— the calendar of months the year is cut from,Calendrical.Gregorianby default.:year_begins—{month, day}of the base calendar the year begins on.:year_number— the base year the year takes its number from,:beginning,:endingor:majority, the default.:written_year— the year a date is written with,:baseby default, as a fiscal date is written with the Gregorian year, or:label, the calendar's own.:weeks,:cldr_calendar_type,:era_calendar_type,:parsing_calendarand:years_in_cycleas above, the facts defaulting to the base calendar's.
The kernel falls out of the base: a date is the base's date relabelled, a month's length the base month's, and shift/6 the base's arithmetic on the base date. base_date/3 and date_from_base_date/3 convert either way. Calendrical.new(module, :month, options) makes one at runtime, and Calendrical.FiscalYear.calendar_for/1 makes the fiscal year of a territory.
use Calendrical.Calendar.Weeks: a year of whole weeks
A retail or fiscal calendar of 52 or 53 whole weeks, cut into twelve months of four or five weeks by a pattern. Its date is a month of the pattern and a day of it, so a month of four weeks has days 1 to 28.
defmodule MyApp.Retail do
use Calendrical.Calendar.Weeks,
pattern: [4, 5, 4],
year_ends: {:saturday, :nearest, {1, 31}}
end
defmodule MyApp.WeekYear do
use Calendrical.Calendar.Weeks, year_begins: {:monday, :on_or_before, {1, 4}}
end:pattern— the weeks in the three months of a quarter,[4, 4, 5],[4, 5, 4]or[5, 4, 4]; the default is[4, 5, 4].:year_begins—{weekday, :on_or_before, {month, day}}, the weekday on or before a day of a Gregorian month, the day being 1 to 7; the ISO week year begins{:monday, :on_or_before, {1, 4}}. The default is{:monday, :on_or_before, {1, 1}}.:year_ends— instead of:year_begins:{weekday, :on_or_after, {month, day}}, the weekday on or after a day within the last seven of a Gregorian month, or{weekday, :nearest, {month, last_day}}.:year_number— the Gregorian year the year takes its number from,:majorityby default.:cldr_calendar_type— the CLDR calendar that names the months,:gregorianby default.
Calendrical.new(module, :week, options) makes one at runtime.
use Calendrical.Calendar.Lunisolar: a calendar reckoned from the sky
The Chinese calendar and its relations: a year of twelve or thirteen lunar months beginning at the second new moon after the winter solstice, with the leap month placed by the solar terms.
defmodule MyApp.Chinese do
use Calendrical.Calendar.Lunisolar,
epoch: ~D[-2636-02-15],
location: &__MODULE__.location/1
def location(_iso_days), do: {39.9, 116.4, 44, 8 / 24}
end:epoch— the first day of year 1.:location— the place the calendar is reckoned at, a function of the ISO day number giving{latitude, longitude, altitude, offset}asAstrotakes them.:cldr_calendar_typeand:era_calendar_type— the naming facts,:chineseby default.
The maker supplies the whole kernel from Calendrical.Lunisolar, and new/3, new!/3, leap_month?/2, gregorian_date_for_lunar/3 and new_year_for_gregorian_year/1 beside it. A module keeps its own extras, as the festivals.
use Calendrical.Composite: calendars in sequence
A calendar that uses one calendar until a day and another from it, as England did at the reform of 1752. See the composite calendars guide.
What a calendar answers
Beyond Calendar's own callbacks, every calendar module answers the derived questions by delegation: info/0 and the naming facts (cldr_calendar_type/0, era_calendar_type/0, parsing_calendar/0, parsing_calendars/0), the months (month_of_year/3, cldr_month/2, written_month_from_cldr_month/2, traditional_months/1, month_numbers/1, day_numbers/2, days_in_month/1, named_month/2), the weeks (week_of_year/3, iso_week_of_year/3, week_of_month/3, weeks_in_year/1, week/2, weeks_of_month/2, date_from_week_date/3, week_date_from_date/3, weeks_in_week_year/1, day_of_week/4), the periods (year/1, quarter/2, quadrimester/2, semester/2, month/2, quarter_of_year/3, day_of_year/3, days_in_year/1, date_from_day_of_year/2, dates_in_gregorian_year/3), the years (year_of_era/1, extended_year/3, related_gregorian_year/3, cyclic_year/3, years_in_cycle/0) and the figures (numeric_month/3, cardinal_day/3, cardinal_year/3). Each is a one-line delegation to the derived API, generated once for every calendar, and none can be redefined: a definition a calendar adds is unreachable behind the generated one, which the compiler reports as a clause that can never match.