Calendars, read from a chronologist's source

The inside of a church with arches, where a beam of sunlight from a small hole high in the wall falls on a brass line laid across the floor and a man with a scroll looks on.
A meridian line in a church, where a sunbeam shows the day the equinox falls on, the drift that led to the reform of 1582.

Thursday 4 October 1582 was followed by Friday 15 October 1582. In Spain, Portugal, Italy, the Papal States and the Polish–Lithuanian Commonwealth ten days of the calendar were simply never lived, and the weekdays went on as if nothing had happened. Britain made the same jump 170 years later, from Wednesday 2 September 1752 to Thursday 14 September, and gave up eleven days. Russia made it in 1918 and gave up thirteen. Behind each jump is a question that sounds too small to need a library: which years have a leap day, and who counts them differently?

The calendars library for DKE, the Deterministic Knowledge Engine, writes down some of the answers in DKE Python, and this essay teaches the subject by reading it. You record three kinds of fact. A year is recorded by its number, and the library's rules conclude whether it is a leap year under the Julian rule and under the Gregorian one, and which years the two disagree about. A reform is recorded as the last day of the old calendar in a place and the first day of the new, with the source that says so, and the library counts the days it skipped. A day is recorded by its date, and a rule marks it if it falls in the gap of a reform, a day that place never had. The marked day is kept: it is a fact, and the gap is a conclusion about it. The second half of the library calculates: the length of a month, whether a date exists, the weekday of a date, the date of Easter, day numbers, and the span between two instants. Every program below runs as shown once the library's source file has been compiled into a store, and every output is copied from a run.

1. Two rules for a leap year

Julius Caesar proposed his calendar in 46 BC, and it took effect on 1 January 45 BC. The astronomer Sosigenes of Alexandria aided the reform and is generally considered its principal designer. A year had 365 days and every fourth year had 366, which makes a mean year of 365.25 days. The solar year is about 365.2422 days, so the Julian calendar gains about one day every 128 years. It was not easy to start with. The year 46 BC had to be made 445 days long, which the account calls the last year of confusion, and the priests then added a leap day every third year instead of every fourth, until Augustus ordered twelve years to pass without one.

By 1582 the gain had reached ten days. The vernal equinox was reckoned to fall on 21 March in 325, the year of the Council of Nicaea, and the March equinox now fell on 10 or 11 March. A Calabrian doctor, Aloysius Lilius, proposed the core of the remedy and the German mathematician Christopher Clavius expanded and defended it, and Pope Gregory XIII issued the bull Inter gravissimas, dated 24 February 1582. The remedy is one clause added to the rule: every year divisible by four is a leap year, except for years that are divisible by 100 and not also by 400. So 1600 and 2000 were leap years, and 1700, 1800 and 1900 were not. The mean year becomes 365 and 97/400 days, 365.2425.

The library holds each year as a number and computes its remainders by 4, by 100 and by 400. The two rules, and the years they part on, are three short statements:

@rule
def gregorian_leap_ordinary():
    for y in Year:
        if y.r4 == 0 and y.r100 != 0:
            y.gregorian_leap = True

@rule
def gregorian_leap_fourth_century():
    for y in Year:
        if y.r400 == 0:
            y.gregorian_leap = True

@rule
def years_differ():
    for y in Year:
        if y.julian_leap == True and absent(y.gregorian_leap):
            y.differ = True

A year that is not a Gregorian leap year gets no gregorian_leap value at all, and a year that is a Julian leap year and has none is one the two rules disagree about. Nine years around 1900 show it, and why_not says what a missing leap year lacked:

import calendars

calendars.years(1896, 1904)
print(calendars.years_that_differ(), calendars.differ(1900), calendars.differ(1904))
print(calendars.julian_leap(1900), calendars.gregorian_leap(1900), calendars.gregorian_leap(1904))
n = why_not(calendars.Year.y1900.gregorian_leap)
for line in str(n).split("\n"):
    print(line)
[1900] True False
True False True
rule `calendars.gregorian_leap_fourth_century` needs calendars.Year[y1900].r400 == 0, and calendars.Year[y1900].r400 is 300 (int)
rule `calendars.gregorian_leap_ordinary` needs calendars.Year[y1900].r100 != 0, and calendars.Year[y1900].r100 is 0 (int)

Of the nine years only 1900 is one the rules part on. The answer to why it is not a Gregorian leap year names both ways it could have been one and what failed in each: it is a century, and its remainder by 400 is 300. The next program records seven more years, the centuries of the story and the one after the reform, and asks the calculation scripts, which answer for any year whether or not it has been recorded:

import calendars

for n in [1500, 1582, 1600, 1700, 1800, 2000, 2100]:
    calendars.year(n)
print(calendars.years_that_differ())
print(calendars.gregorian_leap(1600), calendars.gregorian_leap(2000), calendars.gregorian_leap(2100))
print(calendars.month_length(1900, 2), calendars.month_length(2000, 2), calendars.julian_month_length(1900, 2))
print(calendars.date_exists(1900, 2, 29), calendars.date_exists(2000, 2, 29), calendars.date_exists(2026, 4, 31))
[1500, 1700, 1800, 1900, 2100]
True True False
28 29 29
False True False

The centuries the rules disagree about are the ones not divisible by 400: 1500, 1700, 1800, 1900 and 2100, while 1600 and 2000 are leap years in both. February 1900 has 28 days under the Gregorian rule and 29 under the Julian, and February 2000 has 29. The last line is the script that asks first: 29 February 1900 does not exist, 29 February 2000 does, and neither does 31 April.

2. A reform and the days it skipped

A reform is the last day of the old calendar in a place and the first of the new, written as a year, a month and a day, with the source that says so. The library counts the days between the two on the Julian calendar, and the days a reform skipped are the ones between them:

skipped: int = first_count - last_count - 1

A rule marks a day that is after a reform's last day and before its first, a day that place never had:

@rule
def day_in_the_gap():
    for d in Day, r in Reform:
        if d.ymd > r.last and d.ymd < r.first:
            Gap.d.r = True

Here are Rome's reform and Britain's, a day in the gap of each, and a place that has no reform recorded:

import calendars

calendars.reform("rome", 1582, 10, 4, 1582, 10, 15, "Inter gravissimas")
calendars.reform("britain", 1752, 9, 2, 1752, 9, 14, "Calendar (New Style) Act 1750")
print(calendars.skipped("rome"), calendars.skipped("britain"), calendars.skipped("paris"))
calendars.day("d1", 1582, 10, 10)
calendars.day("d2", 1752, 9, 10)
print(calendars.missing("d1", "rome"), calendars.missing("d1", "britain"), calendars.missing("d2", "rome"), calendars.missing("d2", "britain"))
[10] [11] []
True False False True

Rome skipped ten days and Britain eleven, and a place with no reform recorded has skipped none. The tenth of October 1582 is in Rome's gap, a day Rome never had, and it is not in Britain's: Britain still kept the Julian calendar and lived through it. The tenth of September 1752 is the reverse. The count agrees with the sources, and the sources are not all of one kind. The Gregorian reform spread by decree, a state at a time. France followed Rome in the same year, but later, with Sunday 9 December 1582 followed by Monday 20 December. The Protestant states of the Holy Roman Empire, with Denmark–Norway, waited until Sunday 18 February 1700, which was followed by Monday 1 March, and they kept to a calendar of their own for the date of Easter until 1774.

Britain's act is called Chesterfield's Act, the Calendar (New Style) Act 1750, and it did two things. It moved the start of the legal year from 25 March to 1 January, so that the legal year 1751 ran only 282 days, from 25 March to 31 December. And it omitted eleven nominal days, 3 to 13 September 1752. The old Lady Day, 25 March, became 5 April in the new style, which is why the tax year in the United Kingdom still ends on 5 April. The story that crowds rioted in the streets crying "Give us our eleven days" rests on a satirical journal and on a placard in a painting by William Hogarth, and the account of the act calls it very likely a myth: it reports no contemporary record of such riots.

Other places had other dates, and some had no tidy history at all. Greece went from Wednesday 15 February 1923 to 1 March. Sweden planned to drop the leap days from 1700 to 1740 and close its gap by degrees, failed to drop those of 1704 and 1708, so it stood ten days behind instead of eight, and Charles XII abandoned the plan and went back to the Julian calendar by inserting a unique 30 February in 1712. Sweden adopted the Gregorian calendar in 1753, when 17 February was followed by 1 March. The decree of the Soviet government of 24 January 1918 stated that Wednesday 31 January was to be followed by Thursday 14 February, and dropped thirteen days. The five reforms below are written together, with two days to ask about:

import calendars

calendars.reform("france", 1582, 12, 9, 1582, 12, 20, "the French edict")
calendars.reform("russia", 1918, 1, 31, 1918, 2, 14, "Sovnarkom decree")
calendars.reform("greece", 1923, 2, 15, 1923, 3, 1, "the military government")
calendars.reform("sweden", 1753, 2, 17, 1753, 3, 1, "the Swedish act")
print(calendars.skipped("france"), calendars.skipped("russia"), calendars.skipped("greece"), calendars.skipped("sweden"))
calendars.day("d3", 1918, 2, 5)
calendars.day("d4", 1923, 2, 20)
print(calendars.missing("d3", "russia"), calendars.missing("d3", "greece"), calendars.missing("d4", "greece"), calendars.missing("d1", "france"))
[10] [13] [13] [11]
True False True False

France skipped ten days, Russia and Greece thirteen, and Sweden eleven. The count holds across the end of a month, because it is made from day numbers: from 31 January to 14 February is fourteen days, and the days skipped are one fewer, thirteen. The fifth of February 1918 is in Russia's gap and not in Greece's, the twentieth of February 1923 is in Greece's, and the tenth of October 1582 is not in France's, which still had the old calendar then.

3. When two sources disagree

Dates in history are given by sources, and sources differ. When a fact is written again under another source, the library holds both values beside each other and reasons about both, and unsettled() names what they disagree about. Suppose a second source gives 16 October as the first day of Rome's new calendar. Then 15 October is in the gap under that reading and not under the first:

import calendars

print(calendars.unsettled())
calendars.reform("rome", 1582, 10, 4, 1582, 10, 16, "a rival reading")
print(calendars.skipped("rome"), calendars.missing("d1", "rome"), calendars.unsettled())
calendars.day("d5", 1582, 10, 15)
print(calendars.missing("d5", "rome"))
[]
[10, 11] True ['reform rome first']
True

Nothing was replaced, since the second value came from another source. Both counts are given, ten and eleven. The disagreement is named as the first day of Rome's reform, and the fifteenth of October, a day Rome had by the bull, is marked as one it did not have under the rival value. No query refused. The marked day is the library's way of keeping an argument alive without deciding it.

A correction is a different thing. Writing a fact again under the same source replaces the value, and what rested on the old one is withdrawn with it. Britain's reform is first written by mistake with 15 September as its first day, and then as the act has it:

import calendars

calendars.day("d7", 1752, 9, 14)
calendars.reform("britain", 1752, 9, 2, 1752, 9, 15, "Calendar (New Style) Act 1750")
print(calendars.skipped("britain"), calendars.missing("d7", "britain"))
calendars.reform("britain", 1752, 9, 2, 1752, 9, 14, "Calendar (New Style) Act 1750")
print(calendars.skipped("britain"), calendars.missing("d7", "britain"))
[12] True
[11] False

With the mistaken day the gap is twelve days and the fourteenth falls in it. When the reform is written again under the same source the count goes back to eleven and the fourteenth is out of the gap. Nothing that rested on the mistake remains.

4. The week does not break

The weekday of a date is a small calculation in whole numbers, and it counts from Sunday as 0. Tomohiko Sakamoto posted a short form of it to the comp.lang.c newsgroup: a table of twelve small numbers for the months, and the year, with its quarter and its century terms, added to the day. The library uses it, and the result is the proleptic Gregorian weekday: the Gregorian rule applied to dates before any place adopted it. A date in the gap of a reform has a weekday too, the one it would have had if the reform had come earlier, and that is what shows the week going on through a reform. The program asks for the two days on either side of each of three jumps, written in the Gregorian count:

import calendars

print(calendars.weekday(1582, 10, 14), calendars.weekday(1582, 10, 15), calendars.weekday(1752, 9, 13), calendars.weekday(1752, 9, 14), calendars.weekday(1918, 2, 13), calendars.weekday(1918, 2, 14))
4 5 3 4 3 4

In the Gregorian count the 14th of October 1582 is a Thursday and the 15th a Friday. In Rome the Thursday was called the 4th of October and the Friday the 15th, so the Julian 4th and the Gregorian 14th were one and the same day, and the week went on whatever the days were called. In the same way the 13th of September 1752 is a Wednesday and the 14th a Thursday, and the 13th of February 1918 is a Wednesday and the 14th a Thursday, as the decree for Russia said of the 31st of January and the 14th of February.

5. Easter

The council of Nicaea in 325 did not give explicit rules for the date of Easter. The rule that was adopted is that Easter is celebrated on the first Sunday after the Paschal full moon, which approximates the first astronomical full moon on or after 21 March, and so it falls between 22 March and 25 April inclusive, in the Gregorian count. Carl Friedrich Gauss presented an algorithm for it in 1800, for the Julian or the Gregorian calendar. The method in the library is the version that a New York correspondent sent to Nature in 1876 and that bears the name of Jean Meeus, from his book of 1991, and it is a run of whole-number divisions:

a = y % 19
b = y // 100
c = y % 100
d = b // 4
e = b % 4
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i = c // 4
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
month = (h + l - 7 * m + 114) // 31
day = (h + l - 7 * m + 114) % 31 + 1

Those fourteen lines are the whole of it: a is the remainder of the year by 19, b and c are its century and the year in the century, and the rest is a run of whole-number divisions and remainders that ends in a month and a day. The script returns the two, and it is written for the Western date and for years from 1583. The program asks for four years, and then looks for the extremes over 918 years:

import calendars

print(calendars.easter(2024), calendars.easter(2025), calendars.easter(2026), calendars.easter(2038))
early_years = []
late_years = []
for y in range(1583, 2501):
    e = calendars.easter(y)
    if e[0] == 3 and e[1] == 22:
        early_years = early_years + [y]
    if e[0] == 4 and e[1] == 25:
        late_years = late_years + [y]
print(early_years, late_years)
[3, 31] [4, 20] [4, 5] [4, 25]
[1598, 1693, 1761, 1818, 2285, 2353, 2437] [1666, 1734, 1886, 1943, 2038, 2190, 2258, 2326, 2410]

Easter is 31 March in 2024, 20 April in 2025 and 5 April in 2026, which are the dates the tables give. The earliest date, 22 March, falls in 1598, 1693, 1761 and 1818, and then not again until 2285: there is none in the years 1900 to 2199, as the account of the date says. The latest, 25 April, falls in 1943 and in 2038. The Western date is not the only one. Most Eastern churches use the Julian calendar, and Orthodox Easter in 2026 is 12 April, a week after the Western one. The library calculates the Western date only. The dates of Gregorian Easter repeat in exactly the same order only after 5,700,000 years.

6. Counting days, and instants

To subtract one date from another a program wants a single number for each. day_number() is the Gregorian count, 1 for the first of January in the year 1, and julian_day_number() is the same unit on the Julian calendar, which is how the library counts the days a reform skipped. Across the 400 years of a full Gregorian cycle the two calendars part by exactly the three days the story is about:

import calendars

print(calendars.julian_day_number(1582, 10, 5) - calendars.julian_day_number(1582, 10, 4), calendars.julian_day_number(1582, 10, 15) - calendars.julian_day_number(1582, 10, 4))
g = calendars.day_number(2001, 1, 1) - calendars.day_number(1601, 1, 1)
j = calendars.julian_day_number(2001, 1, 1) - calendars.julian_day_number(1601, 1, 1)
print(g, g // 7, g % 7, j, j - g)
print(g / 400.0, j / 400.0)
print(calendars.day_number(1, 1, 1), calendars.day_number(2026, 11, 9))
1 11
146097 20871 0 146100 3
365.2425 365.25
1 739929

On the Julian count the day after the 4th of October is the 5th, one day on, and Rome's next day was the 15th, eleven on, so ten names were skipped. Four hundred Gregorian years are 146,097 days, which is exactly 20,871 weeks, so the Gregorian calendar repeats its weekdays every 400 years. The same four centuries on the Julian rule are 146,100 days, three more. The mean years are 365.2425 and 365.25, the figures of the sources. The library's first day is 1 and the ninth of November 2026 is day 739,929.

The instants of the library are values of their own. A datetime is a UTC instant, written with a trailing Z, and a duration has days, hours, minutes and seconds and no months or years, since neither has a fixed number of seconds. Both count in the proleptic Gregorian calendar, so a day inside a reform's gap is a valid datetime, and it is the rules above that say a place never had it:

import calendars

print(calendars.after(datetime("1752-09-02T00:00:00Z"), 1))
print(calendars.span(datetime("1918-02-14T00:00:00Z"), datetime("2026-11-09T12:00:00Z")))
print(calendars.date_exists(1752, 9, 5), calendars.date_exists(1712, 2, 30), calendars.date_exists(10000, 1, 1))
1752-09-03T00:00:00Z
P39715DT12H
True False False

The day after the 2nd of September 1752 is, in this count, the 3rd, a day Britain never had. The span from the first day of the new calendar in Russia to noon on 9 November 2026 is 39,715 days and 12 hours. A date exists if the proleptic calendar has it: 5 September 1752 does, 30 February 1712, Sweden's, does not, and neither does a date in the year 10000. A day is 86,400 seconds, and a leap second has no place in a datetime. UTC has had 27 leap seconds since 1972, the last at the end of 2016, because the rotation of the Earth is slowing and irregular, and a difference of more than 0.9 seconds between UTC and the Earth's rotation was not allowed. In November 2022 the General Conference on Weights and Measures decided that the largest allowed difference was to be increased in, or before, 2035. A program that needs the leap seconds needs a table, and this library has none.

7. What the library leaves out

The library counts in the proleptic Gregorian calendar, in the years 1 to 9999, and a span of more than about 292 years is refused, so a longer one is measured in days by day_number. It does not check that a date given as three integers exists unless the script says so, and date_exists is the script that asks. Easter is the Western date, from 1583. It knows no calendar but the Julian and the Gregorian. And the reforms are only those you write: the library holds no table of places, since a table of places is a claim about history, and a claim about history is a thing to be sourced.

Closing

A calendar is an agreement about counting, and its history is a series of corrections to it: Caesar's year of confusion, the leap day the priests added every third year, the ten days of Gregory, the eleven days of Chesterfield's Act, the thirteen of a decree in January 1918. The library keeps their arithmetic where a reader can run it and ask about it. It names the century a leap-day rule fails on, counts the days a reform skipped across the end of a month, marks the day a place never had without throwing it away, and holds two sources side by side when they disagree. And because each conclusion names the facts it rests on, a date corrected in one place is corrected in everything that rested on it.

Free to read

George Parker, Earl of Macclesfield, Remarks upon the Solar and the Lunar Years, the Cycle of 19 Years, Commonly Called the Golden Number, the Epact, and a Method of Finding the Time of Easter (Charles Davis, London, 1751). Written by the man who led Britain's change of calendar in 1752, it explains the golden number, the epact and the reckoning of Easter. Read it free at Project Gutenberg.

Read on