defmodule Cldr.Calendar.Era do
@moduledoc """
Encapsulates the era information for
known CLDR calendars.
The [era data in CLDR](https://github.com/unicode-org/cldr/blob/master/common/supplemental/supplementalData.xml)
is presented in different calendars at different times. The current
best understanding is:
### Japanese Calendar
* From Taisho (1912), the date is Gregorian year,
month and day.
* For Tenshō (Momoyama period) to Meji era (1868)
inclusive its Gregorian year but lunar month and day.
* For earlier eras it is Julian year with lunar month and
day (but the Julian and Gregorian years coincide; there are
no era dates where the Gregorian year would be different
to the Julian year).
### Other Calendars
* For Chinese and Korean (dangi) the era dates are
Gregorian year with lunar month and lunar day
* For Coptic, Ethiopic and Islamic calendars the eras are
Julian dates (Julian day, month and year).
* Persian era is Julian year with persian month and day.
* Gregorian is, well, Gregorian date.
"""
# Just after a calendar is defined this function
# is called to create a module that provides a
# lookup function returning the appropriate era
# number for a given date in a given calendar.
#
# If the calendar does not have a known CLDR
# calendar name associated with it then no
# module is produced and no error is returned.
@doc false
def define_era_module(calendar_module) do
if function_exported?(calendar_module, :cldr_calendar_type, 0) &&
!Code.ensure_loaded?(era_module(calendar_module.cldr_calendar_type())) do
cldr_calendar = calendar_module.cldr_calendar_type()
era_module = era_module(cldr_calendar)
cldr_calendar
|> eras_for_calendar()
|> eras_to_iso_days(cldr_calendar, calendar_module)
|> define_era_module(calendar_module, era_module)
else
:no_op
end
end
# Returns a list of eras with the most
# recent era first (we want this sort
# order so that when we generate function
# clauses, most recent dates match first and
# those are the dates most likely to be used).
defp eras_for_calendar(calendar) do
Cldr.Config.calendars()
|> Map.fetch!(calendar)
|> Map.fetch!(:eras)
|> Enum.reverse()
end
defp eras_to_iso_days(eras, :japanese, calendar) do
Enum.map eras, fn
[era, %{start: [year, month, day]}] when year >= 1912 ->
[era, start: Cldr.Calendar.Gregorian.date_to_iso_days(year, month, day), year: year]
[era, %{start: [year, month, day]}] ->
[era, start: calendar.date_to_iso_days(year, month, day), year: year]
[era, %{end: [year, month, day]}] when year >= 1912 ->
[era, end: Cldr.Calendar.Gregorian.date_to_iso_days(year, month, day), year: year]
[era, %{end: [year, month, day]}] ->
[era, end: calendar.date_to_iso_days(year, month, day), year: year]
end
end
@eras_in_gregorian_year [
:chinese,
:dangi,
:persian,
:gregorian
]
defp eras_to_iso_days(eras, cldr_calendar, calendar)
when cldr_calendar in @eras_in_gregorian_year do
Enum.map eras, fn
[era, %{start: [year, month, day]}] ->
[era, start: calendar.date_to_iso_days(year, month, day), year: year]
[era, %{end: [year, month, day]}] ->
[era, end: calendar.date_to_iso_days(year, month, day), year: year]
end
end
@eras_in_julian_calendar [
:coptic,
:ethiopic,
:islamic,
:islamic_civil,
:islamic_rgsa,
:islamic_tbla,
:islamic_umalqura
]
defp eras_to_iso_days(eras, cldr_calendar, _calendar)
when cldr_calendar in @eras_in_julian_calendar do
Enum.map eras, fn
[era, %{start: [year, month, day]}] ->
[era, start: Cldr.Calendar.Julian.date_to_iso_days(year, month, day), year: year]
[era, %{end: [year, month, day]}] ->
[era, end: Cldr.Calendar.Julian.date_to_iso_days(year, month, day), year: year]
end
end
defp define_era_module(eras, calendar, module) do
module_body = [moduledoc(module), default(calendar) | function_body(eras)]
Module.create(module, module_body, Macro.Env.location(__ENV__))
end
defp moduledoc(module) do
quote do
@moduledoc """
Implements a `year_of_era/{1, 2}` function to return
the year of era and the era number for the
`#{inspect(unquote(module))}` calendar.
This module is generated at compile time from
CLDR era data.
"""
@doc false
@spec year_of_era(integer(), Calendar.year()) :: {Calendar.year(), Calendar.era()}
end
end
defp default(calendar) do
quote do
@doc """
Returns the year of era and the era number
for a given date in `iso_days`.
"""
@spec year_of_era(integer()) :: {Calendar.year(), Calendar.era()}
def year_of_era(iso_days) do
{year, _month, _day} = unquote(calendar).date_from_iso_days(iso_days)
year_of_era(iso_days, year)
end
@doc """
Returns the day of era and the era number
for a given date in `iso_days`.
"""
@spec day_of_era(integer()) :: {Calendar.day(), Calendar.era()}
def day_of_era(iso_days)
@doc false
def era(iso_days)
end
end
defp function_body(eras) do
for [era, {position, date}, {:year, era_year}] <- eras do
case position do
:start ->
quote do
def year_of_era(iso_days, year) when iso_days >= unquote(date) and year < unquote(era_year) do
{1, unquote(era)}
end
def year_of_era(iso_days, year) when iso_days >= unquote(date) do
{year - unquote(era_year) + 1, unquote(era)}
end
def day_of_era(iso_days) when iso_days >= unquote(date) do
{iso_days - unquote(date) + 1, unquote(era)}
end
def era(iso_days) when iso_days >= unquote(date) do
{unquote(date), nil, unquote(era)}
end
end
:end ->
quote do
def year_of_era(iso_days, year) when iso_days <= unquote(date) do
{year - unquote(era_year) + 1, unquote(era)}
end
def day_of_era(iso_days) when iso_days <= unquote(date) do
{iso_days + 1, unquote(era)}
end
def era(iso_days) when iso_days >= unquote(date) do
{nil, unquote(date), unquote(era)}
end
end
end
end
end
@doc """
Return the era module for a given
cldr calendar type.
"""
@era_module_base Cldr.Calendar.Era
def era_module(cldr_calendar) do
module = to_string(cldr_calendar) |> String.capitalize()
Module.concat(@era_module_base, module)
end
end