Chapter 95
How Long Before You Can Use Your 2015 Calendar Again?
Calendars are predictable: the main thing that changes year to year is the weekday a date falls on. Six questions. How many different calendars are needed to cover all possible years (remembering leap years)? When is the next year that reuses the 2015 calendar? What is the smallest number of years that can pass before a non-leap-year calendar repeats, and the largest? And the smallest and largest gaps before a leap-year calendar repeats?
The Riddler, FiveThirtyEight(original post)
Solution
A year’s calendar is pinned by two things: the weekday on which January 1 falls, and whether the year is a leap year. That is distinct calendars. A common year has days, so it nudges the January-1 weekday forward by one; a leap year nudges it forward by two. Two years share a calendar only when the weekday has returned to its start and the leap status matches.
Starting from 2015 (a Thursday), the weekday shifts by (2016 is a leap year), then , returning to Thursday after eleven steps, in .
For the repeat gaps, the leap-year pattern usually returns a common year’s calendar after or years and a leap year’s after , but the Gregorian rule that century years are common unless divisible by stretches some gaps. Walking the calendar across these century breaks gives the extremes: a non-leap-year calendar repeats in as few as years and as many as , while a leap-year calendar repeats in as few as years and as many as (for instance 2072’s calendar next appears in 2112, the year 2100 not being a leap year).
The computation
Read it straight off the real calendar. For each year record its January-1 weekday and leap status, count the distinct pairs, find the next year matching 2015, and for every year measure the gap to its first repeat, taking the smallest and largest over common years and over leap years across full four-century cycles.
from datetime import date
def leap(y): return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
def cal(y): return (date(y, 1, 1).weekday(), leap(y)) # (weekday, is-leap)
def repeat_gap(y):
yy = y + 1
while cal(yy) != cal(y): yy += 1
return yy - y
print("calendars:", len({cal(y) for y in range(1, 4001)}))
print("reuse 2015:", next(y for y in range(2016, 3000) if cal(y) == cal(2015)))
nonleap = [repeat_gap(y) for y in range(2000, 2800) if not leap(y)]
leapyrs = [repeat_gap(y) for y in range(2000, 2800) if leap(y)]
print("non-leap min/max:", min(nonleap), max(nonleap))
print("leap min/max:", min(leapyrs), max(leapyrs))
# calendars: 14
# reuse 2015: 2026
# non-leap min/max: 6 12
# leap min/max: 12 40