Handling Holidays in Your Software

Hourglass and clocks

Keeping track of holidays is important in many industries but calendars are based on complex astronomical observations with roots in ancient cultures.

Introduction

Do you know if Easter falls in March or April next year? Or the weekday of Christmas Eve? If not, it is no surprise.  Managing the calendar is not a straightforward matter. In fact, it is a perfect example of how human knowledge is gathered during centuries, or even millennia, only to be confirmed and enhanced by modern science and technology.
Beyond the scheduling of our vacations, calendars and holidays have a crucial role in many business processes: pricing policies for hotels and airlines, stock estimations in retail establishments and interest rate calculations in financial contracts are just a few examples. So let’s look into what we are facing when including calendars in our software applications…

The programming examples in this article will use”/” for integer division and % for modulo operations, for example 5 / 2 =2 and 5 % 2 = 1.

Of Babylon and the Vatican

One of the most well-known calendar peculiarities is that the solar year is slightly longer than 365 days. In 1582, Pope Gregory XIII decided upon the correction we all know as “leap year”, an extra day if the year is a multiple of 4 but not of 100 unless it is also a multiple of 400. A typical leap year adjustment factor would look like this in my favorite programming language Scala:

def leapYear(year: Int): Int = (if (year % 4 == 0) 1 else 0) - (if (year % 100 == 0) 1 else 0) +( if (year % 400 == 0) 1 else 0)

Once we have gotten the solar year right, it will still not be equal to any multiple of a lunation, which follows a variable pattern with an average of a little more than 29.5 days. Curiously, with a great deal of accuracy we might say that 235 lunar months correspond to 19 solar years, which is called the “Metonic cycle”. Despite being named after a Greek astronomer, there are records of its usage in both Babylon and ancient China. For the Christian era, a year’s position in the cycle is called the “Golden Number” and defined as:

def goldenNumber(year: Int): Int = (year % 19) + 1

The habit of dividing time into seven days can also be traced back to the Babylonians, as well as to Jewish tradition. It is thought as originally meant to represent a quarter of a lunation, which of course give rise to further mismatches with the solar years and lunar month. Fortunately, the sequence of weekdays in calendar months and years is much less complicated. In fact, it only depends on the shift caused by the fact that 52 weeks equals 364 days. That is, one day less than a full year or two days less than a leap year. Before the invention of electronic calculators, complex tables were used.  The sequence of letters A – G was assigned to every day of the year, starting with an A for January 1. As a result, all Sundays will have the same code, which is denominated the “Dominical Letter”. If we index A – G as 1 – 7, the following formula applies:

def dominicalLetter(year: Int): Int = 7 - ((year + year / 4 - year / 100 + year / 400 - 1) % 7)

When using the Dominical Letter for determining the weekday for a particular date, we must make a correction for leap years. That is, the Dominical Letter shifts one step back from March 1 and onwards. All in all there are only 14 possible combinations, according to which weekday is January 1st and if it is a leap year or not. Some curious facts can be derived for these 14 cases, for example the number of Friday 13th during the year and in which months they fall.

When is Easter? Well, It’s Complicated…

Several, if not most, movable Christian holidays depends on Easter Sunday, which in its turn falls on the first Sunday after the first full moon that occurs on or after the vernal equinox. So far, so good, we could figure that out with a formula based on the Golden Number and Dominical Letter. But there are a couple of complications: firstly, we are talking ecclesiastical moons, not the real lunar cycle; and secondly, we should assume that the equinox falls on March 21, which is not always true in the real world.

Our quest starts with a concept called “epact”. In the Gregorian system, it is defined as the age of the (ecclesiastical) moon on January 1. That is, the days that has passed since the last new moon. The initial epact is obtained by a simple formula but has to be adjusted because of the leap years in the solar cycle and because of the divergence from the true moon cycle. A third correction is made to ensure that the ecclesiastical new moons will fall on different dates throughout the 19-years Metonic cycle. By applying these corrections, an adjusted epact is obtained with the following Scala code:

def initialEpact(year: Int): Int =(11 * goldenNumber(year) - 10) % 30
def solarEquation(year: Int): Int = year / 100 - year / 400 - 12
def lunarEquation(year: Int): Int = (year / 100 - 15 - (year / 100 - 17) / 25) / 3
def newMoonCorrection(year: Int): Int  = initialEpact(year) / 24 - initialEpact(year)  / 25 + (goldenNumber(year) / 12) * (initialEpact(year) / 25 - initialEpact(year) / 26)
def adjustedEpact(year: Int): Int = initalEpact(year) - (solarEquation(year) - lunarEquation(year)) % 30 + newMoonCorrection(year)

Here the terms “- 12” and “- 15” serves to initiate the correction in 1582 when the Gregorian system was introduced.

The adjusted epact can then be deployed to find the date of Easter Sunday. The following formulas take into account if Easter falls after the year’s second or third full moon and on which weekday it falls. Easter Sunday is then obtained as a day count with start on March 1.

def fullMoon :Int = if (adjustedEpact < 24) 45 - adjustedEpact else 75 - adjustedEpact
def weekDay: Int = 1 + (fullMoon + 2) % 7
def easterSunday: Int = initialEpact + (7 + dominicalLetter - weekDay) % 7

More information

A code example based on this article is available at GitHub.

Urban, S.E. & Seidelman, P.K. (2012). Explanatory Supplement to the Astronomical Almanac. Mill Valley, CA: University Science Books.

 

Gimlé Digital is currently looking for sponsors, collaborators and partnering companies. If you find our activities appealing and consider getting involved, please let us know via our website’s contact form.