Well it was more than a day or so but I can summarise my findings now that ex_cldr_dates_times is published.
Week calendars are complicated
The interpretation of a week varies quite a lot, even in Gregorian calendars, based upon usage and expectation. For example:
- How is a week interpreted in a month-based calendar like Gregorian (Calendar.ISO)?
- How is a month interpreted in a week-based calendar (like ISO Week)? These kinds of calendars (excluding ISO Week) define a âmonthâ based upon 13 week quarters split into one of either
4-4-5, 4-5-4 or 4-4-5 weeks.
- If a calendar starts in July (like The Australian fiscal year calendar) is it referred to as the beginning year or the ending Gregorian year? That is, if the fiscal date is
2019-01-01 (meaning July 1st) what is the Gregorian date? Is it 2019-07-01 or is it 2020-07-01?
When does a week start?
There are both cultural and definitional differences.
- Different countries start the week on either
Sunday or Monday as common practise
- Companies in countries that allow defined fiscal years can start their calendar on the âfirst or last day_of_weekâ. For example, Ciscoâs fiscal year is defined as
last Saturday in July.
- Fiscal calendars for countries can and do start in different Gregorian months of the year. For example, the UK fiscal year starts in April, Australia in July and the US in September.
Calendar configuration for week based calendars
In order to provide configuration for calendars implementing week-based behaviour I found I had to define the following configuration elements:
# Each quarter has three
# 'months` each of 13 weeks
# in either of a 4,4,5; 4,5,4
# of 5,4,4 layout
weeks_in_month: [4, 4, 5],
# Indicates if the anchor
# represents the beginning
# of the year or the end
begins_or_ends: :begins,
# Calendar begins on the
# :first, :last or :nearest
first_or_last: :first,
# First week of the year begins on this day.
# of the week.
# `day: :first` means that the first week starts
# on the first day of the year. `day: :first` only
# applies to month-based calendars and it
# will also result in a short week for the last
# week of a year
day: 1,
# Year begins in this month
month: 1,
# The year of the last_day or first_day
# is either the year with the :majority
# of months or the :beginning year
# or :ending year
year: :majority,
# First week has at least
# this many days in current
# year
min_days: 7
Callbacks
In my implementation I found I required the following callbacks to implement week-based calendars:
@doc """
Returns the `month` for a given `year`, `month` or `week`, and `day`
for a a calendar. Required to provide for returning the notion of a month
for a week-based calendar,
The `month_of_year` is calculated based upon the calendar configuration.
"""
@callback month_of_year(
year :: Calendar.year(),
month :: Calendar.month() | Cldr.Calendar.week(),
day :: Calendar.day()
) ::
Calendar.month()
@doc """
Returns a tuple of `{year, week_in_year}` for a given `year`, `month` or `week`, and `day`
for a a calendar.
The `week_in_year` is calculated based upon the calendar configuration.
"""
@callback week_of_year(
year :: Calendar.year(),
month :: Calendar.month() | Cldr.Calendar.week(),
day :: Calendar.day()
) ::
{Calendar.year(), Calendar.week()} | {:error, :not_defined}
@doc """
Returns a tuple of `{year, week_in_year}` for a given `year`, `month` or `week`, and `day`
for a a calendar.
The `iso_week_of_year` is calculated based on the ISO calendar.
"""
@callback iso_week_of_year(
year :: Calendar.year(),
month :: Calendar.month(),
day :: Calendar.day()
) ::
{Calendar.year(), Calendar.week()} | {:error, :not_defined}
@doc """
Returns a tuple of `{month, week_in_month}` for a given `year`, `month` or `week`, and `day`
for a a calendar.
The `week_in_month` is calculated based upon the calendar configuration.
"""
@callback week_of_month(Calendar.year(), Cldr.Calendar.week(), Calendar.day()) ::
{Cldr.Calendar.month(), Cldr.Calendar.week()}
@doc """
Returns the calendar basis.
Returns either :week or :month
"""
@callback calendar_base() :: :week | :month
@doc """
Returns the number of periods (which are
months in a month calendar and weeks in a
week calendar) in a year
"""
@callback periods_in_year(year :: Calendar.year()) :: week() | Calendar.month()
@doc """
Returns the number of weeks in a year
"""
@callback weeks_in_year(year :: Calendar.year()) :: week()
@doc """
Returns the number of days in a year
"""
@callback days_in_year(year :: Calendar.year()) :: Calendar.day()
Calendar Interval callbacks
I use Interval.year/2, Interval.quarter/2 and so on. Would be renamed Calendar.year_range/2 based on the earlier conversation.
@doc """
Returns a date range representing the days in a
calendar year.
"""
@callback year(year :: Calendar.year()) :: Date.Range.t()
@doc """
Returns a date range representing the days in a
given quarter for a calendar year.
"""
@callback quarter(year :: Calendar.year(), quarter :: Cldr.Calendar.quarter()) :: Date.Range.t()
@doc """
Returns a date range representing the days in a
given month for a calendar year.
"""
@callback month(year :: Calendar.year(), month :: Calendar.month()) :: Date.Range.t()
@doc """
Returns a date range representing the days in a
given week for a calendar year.
Returns an error tuple if the calendar has no
notion of a week as is the case with the Julian
calendar.
"""
@callback week(year :: Calendar.year(), week :: week()) ::
Date.Range.t() | {:error, :not_defined}
Basic Calendar math
When incrementing or decrementing years, quarters, months, weeks and days - including date ranges - most of the heavy lifting can be done in a base calendar module.
However for incrementing (and decrementing) months and quarters a callback is required since the nature of a quarter and a month is calendar specific.
@doc """
Increments a `Date.t` or `Date.Range.t` by a specified positive
or negative integer number of periods (year, quarter, month,
week or day).
Calendars need only implement this callback for `:months` and `:quarters`
since all other date periods can be derived.
"""
@callback plus(
year :: Calendar.year(),
month :: Calendar.month() | week(),
day :: Calendar.day(),
months_or_quarters :: :months | :quarters,
increment :: integer,
options :: Keyword.t()
) :: {Calendar.year(), Calendar.month(), Calendar.day()}