The Gregorian Calendar

Introduction

Handling dates isn't trivial. Here, we implement an API for the Gregorian Calendar. This isn't helpful for calculating when Chinese New Year, Passover, or Ramadan falls, but for most purposes it will suffice. The sources of complexity in the Gregorian calendar are in determining whether a given year is leap, and the different month lengths.

To simplify matters, we begin the calendar at a date (the EPOCH) set by the user, for example Sunday, 1st January 1950. Dates before this aren't handled. The API contains three top-level functions: indexInEpoch, which converts a given date into the number of days from the epoch; yearMonthDay, which converts back to a date, and dayOfWeek, which displays the day of the week.

In the C library time.h, months start at 0 and days of the month start at 1. That is brain-damaged. Here, we will follow the ISO date convention. Months and days of the month both start at 1. Days of the week also start at 1 (Monday). Names of months and days of the week can be returned simply by array look-up.

Data Types

First, we define Year, Month, and Day types. These are implemented as subtypes of Int. This is safer than using Int, but the extra type safety it gives us is limited. Full Metal Jacket doesn't yet support subranges of Int, but these will be added later.

Year Month Day

Functions

A leap year is divisible by 4, unless it's divisible by 100 but not 400. To make the code clearer, we first define a divisibleByP function, which outputs T if its first input, an Int, is exactly divisible by its second input, another Int, and otherwise outputs NIL.

divisibleByP

We can now define leapP, which accepts a Year as its input, and outputs a Bool. TRUE and FALSE are names for T and NIL respectively, but with Boolean type.

leapP

The number of days depends on whether the year is leap.

daysInYear

The number of days before any month before March is the the same in any year. From March onwards, it's one more if the year is leap. The vector supplied to elt contains the number of days before any month if the year isn't leap.

daysBeforeMonth

indexInYear returns the index of a day in a given year, i.e. the number of days before it.

indexInYear

The inputs of indexInEpoch are of type Year, Month, and Day. It outputs the number of days from the start of the epoch.

The first value in the loop is a year, initially set to EPOCH and incremented by one in each iteration. The second value is the date index, initially set to 0 (its value at EPOCH), and repeatedly incremented by the number of days in each year starting from EPOCH. When the year which was input has been reached, the value output by indexInYear is added to it.

indexInEpoch

indexAndYearFromIndexInEpoch takes the number of days since EPOCH as its input, and outputs the number of days since new year's day, and the year.

indexAndYearFromIndexInEpoch

monthFromIndexInYeartakes the number of days since new year's day and the year as its inputs, and outputs the month. Each time around the loop, daysBeforeMonth is called, and the value it outputs is compared against the number of days since New Year.

monthFromIndexInYear

dayFromIndexInYear takes the number of days since new year's day, the year, and the month, as its inputs, and outputs the day of the month.

dayFromIndexInYear

yearMonthDay takes number of days since EPOCH as its input.

yearMonthDay

The day of the week is just the remainder of the number of days since the start of EPOCH, except that it is 7 if the remainder is zero.

dayOfWeek

In 2016, Easter falls on March 27th. It is, of course, a Sunday. The code in the sandbox outputs the number of days since EPOCH, then the year, month, and day of the month, followed by day of the week.

dayOfWeek dayOfWeek

Bell, Book, and Candle

Determining the date on which Easter falls in a given year has always been problematic. At one point, an entire branch of the early Christian Church was threatened with excommunication over of a disagreement about how to calculate it. Computus, the algorithm currently used, is quite hairy and practically incomprehensible. Even Gauss got it wrong. There have been recent attempts to introduce a simpler mechanism.

In principle, the rule for calculating Easter is quite simple: Easter falls on the Sunday after the full moon after the vernal equinox, which close to EPOCH generally falls on March 20th. At the risk of being excommunicated, I will now present a simplified algorithm, which I have to say is wrong in 2008 and 2025. (For this, I make no apology: if ever there was a case of accidental difficulty, the currently accepted algorithm for Easter is it.)

First, we write a simple helper function to calculate the difference between the value and the closest integer not higher than it.

fraction

The synodic month (i.e. the time between a given phase and the next occurrence of it) varies only slightly, and averages 29.530588 days. So if we know the lunar phase at the EPOCH, calculating it on any later date is straightforward. The value returned can be any Real between 0.0 up to but not including 1.0, and is defined so that

PhaseValue
New moon0.0
First quarter0.25
Full moon0.5
Third quarter0.75
lunarPhase

Given a day of the week (the first argument) and an index (the second argument), the next index after the day of the week is obtained by iteration, starting one day after the second argument, until the day of the week is equal to the first argument.

nextIndexAfterDayOfWeek

The day after a given lunar phase is tricky because phases wrap around to 0.0 when a new moon is reached. This means we should iterate starting from the difference between the lunar phase on the day after the second argument, until 1.0 is reached or passed, and return the index when this happens. DAILY_LUNAR_PHASE_CHANGE is the inverse of SYNODIC_MONTH.

nextIndexAfterLunarPhase

easter returns the index of the Sunday after the full moon which follows the vernal equinox, as explained above. The constant FULL_MOON has value 0.5.

easter

Previous Up Next

© Copyright Donald Fisk 2016