Calendars
is a collection of calendars that is based on the calendars contained
in the 4th edition of the book (from here on referenced as DR4)
Calendrical Calculations - The Ultimate Edition
by Edward M. Reingold and Nachum Dershowitz
Cambridge University Press, 2018
The calendars are just thin wrappers around the Calixir
package. Calixir
is a port of the Lisp calendar software calendrica-4.0.cl
that comes
with DR4.
The package is available in Hex and can be installed
by adding calendars
to your list of dependencies in mix.exs
:
def deps do
[
{:calendars, "~> 0.1.0"}
]
end
Documentation has been generated with ExDoc and published on HexDocs. The docs can be found at https://hexdocs.pm/calendars.
The Calixir library and its sample data are made public under the following conditions:
- The code and data can be used for personal use.
- The code can data be used for demonstrations purposes.
- Non-profit reuse with attribution is fine.
- Commercial use of the algorithms should be licensed and are not allowed from this library.
The permissions above are granted as long as attribution is given to the authors of the original algorithms, Nachum Dershowitz and Edward M. Reingold.
In this software, I distinguish monotonic and cyclical calendars:
A monotonic calendar has a distinct origin or epoch and
stretches from there (potentially) to negative and positive infinity.
All its values or dates are distinct. The most common example is
probably the Gregorian
calendar.
A cyclical calendar does not have an origin. It consists of periods or cycles of the same length that return indefinitely. All the values or dates within one cycle are unique, but they return from cycle to cycle, so that it is not possible, to assign such a value to a distinct point in time. The most common example is probably the week with its weekdays.
One could argue that cycles shouldn't be called 'calendars'. But that's the way they are treated in DR4. So I'm using the attributes monotonic and cyclical to distinguish calendars if and when it matters.
To facilitate the conversion of calendar dates between different calendars
is one of the main functions of Calendars
. To minimize the number of conversion
functions to be written the common star pattern is used. Each conversion from
one calendar to another is split in two steps:
- Conversion of the date of the source calendar into a common 'canonical' date.
- Conversion of the 'canonical' date into a date of the target calendar.
The 'canonical' date is the central hub for all conversions. Thus, every calendar must only know how to convert its dates into and from the corresponding date of some 'canonical' calendar. Prime candidates for a 'canonical' calendar are calendars that don't have an internal structure of their own (i.e. a hierarchy of units like 'year', 'month', and 'day'), but work with one unit only (usually the 'day').
One candidate could have been the Julian Day number (JD
in this package) or
variants thereof (i.e. the Modified Julian Day number, MJD
). But the
authors of DR4 have chosen to create their own 'canonical' calendar,
the RataDie
calendar, that is closely aligned with the Gregorian
calendar.
The base unit of the RataDie
is the day. Throughout the book (DR4) a
variable containing a day of this calendar is referred by the term date
while functions working with such a variable use the term fixed, i.e.:
gregorian-from-fixed(date) = [year, month, day] # DR4 62 (2.23)
I consider this unfortunate, because date is a broader term that can be
(and commonly is) used for all calendars. So throughout Calixir
and Calendars
,
I use the term fixed instead of date for dates of the RataDie
calendar, i.e.:
gregorian_from_fixed(fixed) = {year, month, day} # DR4 62 (2.23)
The fixed
dates of the RataDie
calendar form the basis for all calendar
conversions in Calixir
and Calendars
.
-
Both, monotonic and cyclical calendars must have a
from_fixed
function that converts afixed
date into their own corresponding dates. -
In addition, monotonic calendars must have a
to_fixed
function that converts their own dates into the correspondingfixed
dates.
Additionally, the calendars have two pairs of conversion functions that are just syntactic sugar:
from_jd
andto_jd
for Julian Day numbersfrom_date
andto_date
for direct conversions from and into another calendar.
In this section, I detail some of my decisions why this software is how it is. It might help you to understand its structure.
Before I published the Calixir
and Calendars
packages I tried
various ways to refactor the monolithic calendrica-4.0
Lisp package, but always
ended in some form of 'dependency hell'. So I finally decided to keep Calixir
as a single monolithic block and implement the calendars as thin wrappers around
Calixir
.
This approach offers an additional advantage: The Calendars
collection can be
extended just by adding calendar modules to lib/calendars
. Additional calendars
can but don't need to reference Calixir
. To allow for conversions they only
have to implement their own from_fixed
, to_fixed
, from_date
, and to_date
functions. from_jd
and to_jd
are not required.
If you need additional functionality for a calendar create a new module and use
existing functionality by creating defdelegate
s to the base calendar or Calixir
.
Here is an example for the interactive use of Calendars
:
D:\Projects\calendars>iex -S mix
Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> g_date = Gregorian.date(2020, 7, 24)
{2020, 7, 24}
iex(2)> fixed = Gregorian.to_fixed(g_date)
737630
iex(3)> j_date = Julian.from_fixed(fixed)
{2020, 7, 11}
iex(4)> Julian.to_date(j_date, Gregorian)
{2020, 7, 24}
The same example in a module:
defmodule ExampleConverter do
def gregorian_to_julian_conventional do
g_date = Gregorian.date(2020, 7, 24) # create Gregorian date
fixed = Gregorian.to_fixed(g_date) # convert Gregorian date to fixed
j_date = Julian.from_fixed(fixed) # convert fixed into Julian date
Julian.to_date(j_date, Gregorian) # should return {2020, 7, 24}
end
end
Using a pipeline:
defmodule PipelineConverter do
def check_pipeline do
gregorian_date = Gregorian.date(2020, 7, 24)
if (gregorian_date
|> Gregorian.to_fixed
|> Julian.from_fixed
|> Julian.to_fixed
|> Gregorian.from_fixed) == gregorian_date,
do: {:ok, "Pipeline works."},
else: {:error, "Pipeline is broken."}
end
def check_pipeline_using_from_date do
gregorian_date = Gregorian.date(2020, 7, 28)
if (gregorian_date
|> Julian.from_date(Gregorian)
|> Hebrew.from_date(Julian)
|> Gregorian.from_Hebrew()) == gregorian_date,
do: {:ok, "Pipeline works."},
else: {:error, "Pipeline is broken."}
end
def check_pipeline_using_to_date do
gregorian_date = Gregorian.date(2020, 7, 28)
if (gregorian_date
|> Gregorian.to_date(Julian)
|> Julian.to_date(Hebrew)
|> Hebrew.to_date(Gregorian)) == gregorian_date,
do: {:ok, "Pipeline works."},
else: {:error, "Pipeline is broken."}
end
end
Of course, this works for any combination of monotonic calendars.
Be careful when running the tests. The brute_force_conversion_test
might take about half an hour. All the other tests combined take only
a few minutes.
- Some corrections in README.md
- Added Astronomical calendar
- Cleaned up tests
- Simplified
atomize
function
- Completely rewritten logic and documentation
- Added functions
date_text
to Gregorian, Julian, and Hebrew.
- Fixed and renamed
_as_date
functions.
- Added function
today
to Gregorian. - Added function
start_of_year
to Gregorian. - Added function
end_of_year
to Gregorian. - Added function
start_of_month
to Gregorian. - Added function
end_of_month
to Gregorian. - Added function
start_of_week
to Gregorian. - Added function
end_of_week
to Gregorian. - Added function
month_names_as_list
to Gregorian. - Added function
day_names_as_list
to Gregorian. - Added function
month_names_as_list
to Hebrew. - Added holiday functions to Gregorian.
- Added holiday functions to Hebrew.
- Added the corresponding tests.
- Fixed docs in RataDie calendar.
- Fixed
from_jd
andto_jd
functions in RataDie calendar. - Added
is_leap?
anddays_in_month
functions to Gregorian calendar. - Added
is_leap?
anddays_in_month
functions to Julian calendar.