Gregorian calendar computation in Python
Converts between Gregorian calendar dates and day numbers using the rules for months and leap years.
- File
gregorian_calendar_computation.py, 62 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gregorian-calendar-computation/. The authors are listed in prov/3NM0VD.jsonl.
"""Conversions between proleptic Gregorian dates and fixed day numbers (R.D.).
R.D. 1 is 1 January of year 1, a Monday, so R.D. numbers agree with
datetime.date.toordinal for the years 1 to 9999. Years use astronomical
numbering (1 BC is year 0) and may be any Python integer; // and % are the
floor division and the matching non-negative remainder, so the code is valid
for negative years and day numbers. Each function makes a fixed number of
integer operations.
"""
def gregorian_leap_year(year: int) -> bool:
"""True if year is a leap year of the Gregorian calendar."""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) # @line L1
def fixed_from_gregorian(year: int, month: int, day: int) -> int:
"""The fixed day number (R.D.) of a valid proleptic Gregorian date."""
if month <= 2: # @line L2
correction = 0
elif gregorian_leap_year(year):
correction = -1
else:
correction = -2
y = year - 1 # @line L3
return 365 * y + y // 4 - y // 100 + y // 400 + (367 * month - 362) // 12 + correction + day
def gregorian_year_from_fixed(rd: int) -> int:
"""The Gregorian year that contains the day with fixed number rd."""
d0 = rd - 1 # @line L4
n400, d1 = divmod(d0, 146097) # @line L5
n100, d2 = divmod(d1, 36524) # @line L6
n4, d3 = divmod(d2, 1461) # @line L7
n1 = d3 // 365 # @line L8
year = 400 * n400 + 100 * n100 + 4 * n4 + n1 # @line L9
if n100 != 4 and n1 != 4: # @line L10
year += 1
return year
def gregorian_from_fixed(rd: int) -> tuple[int, int, int]:
"""The proleptic Gregorian date (year, month, day) of the fixed day number rd."""
year = gregorian_year_from_fixed(rd)
prior_days = rd - fixed_from_gregorian(year, 1, 1) # @line L11
if rd < fixed_from_gregorian(year, 3, 1): # @line L12
correction = 0
elif gregorian_leap_year(year):
correction = 1
else:
correction = 2
month = (12 * (prior_days + correction) + 373) // 367 # @line L13
day = rd - fixed_from_gregorian(year, month, 1) + 1 # @line L14
return year, month, day
def day_of_week(rd: int) -> int:
"""Day of the week of a fixed day number: 0 is Sunday, 1 is Monday, ..., 6 is Saturday."""
return rd % 7 # @line L15
test_gregorian_calendar_computation.py 136 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gregorian-calendar-computation/. The authors are listed in prov/3NM0VD.jsonl.
import datetime
import random
from gregorian_calendar_computation import (
day_of_week,
fixed_from_gregorian,
gregorian_from_fixed,
gregorian_leap_year,
gregorian_year_from_fixed,
)
MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def naive_leap(y: int) -> bool:
if y % 400 == 0:
return True
if y % 100 == 0:
return False
return y % 4 == 0
def month_length(y: int, m: int) -> int:
return 29 if m == 2 and naive_leap(y) else MONTH_LENGTHS[m - 1]
def naive_fixed(y: int, m: int, d: int) -> int:
"""Count days one year and one month at a time from R.D. 1 = 1 January of year 1."""
days = 0
if y >= 1:
for yy in range(1, y):
days += 366 if naive_leap(yy) else 365
else:
for yy in range(y, 1):
days -= 366 if naive_leap(yy) else 365
for mm in range(1, m):
days += month_length(y, mm)
return days + d
def next_day(y: int, m: int, d: int) -> tuple[int, int, int]:
if d < month_length(y, m):
return y, m, d + 1
if m < 12:
return y, m + 1, 1
return y + 1, 1, 1
def random_date(rng: random.Random, lo: int, hi: int) -> tuple[int, int, int]:
y = rng.randint(lo, hi)
m = rng.randint(1, 12)
return y, m, rng.randint(1, month_length(y, m))
def test_examples() -> None:
assert fixed_from_gregorian(1, 1, 1) == 1
assert fixed_from_gregorian(0, 12, 31) == 0
assert fixed_from_gregorian(2000, 1, 1) == 730120
assert fixed_from_gregorian(2000, 12, 31) == 730485
assert gregorian_from_fixed(730485) == (2000, 12, 31)
assert fixed_from_gregorian(2026, 11, 6) == 739926
assert gregorian_from_fixed(739926) == (2026, 11, 6)
assert day_of_week(739926) == 5
assert day_of_week(1) == 1
assert fixed_from_gregorian(1970, 1, 1) == 719163
assert day_of_week(fixed_from_gregorian(1582, 10, 15)) == 5
def test_leap_years() -> None:
for y in range(-2000, 3000):
assert gregorian_leap_year(y) == naive_leap(y)
assert gregorian_leap_year(2000) and gregorian_leap_year(2024) and gregorian_leap_year(0)
assert not gregorian_leap_year(1900) and not gregorian_leap_year(2100) and not gregorian_leap_year(2023)
assert gregorian_leap_year(-4) and not gregorian_leap_year(-100) and gregorian_leap_year(-400)
def test_cycle_boundaries() -> None:
for y in (-801, -800, -401, -400, -101, -100, -1, 0, 1, 99, 100, 101, 399, 400, 401, 1600, 1900, 2000, 2100):
for m, d in ((1, 1), (2, 28), (3, 1), (12, 31)):
r = fixed_from_gregorian(y, m, d)
assert r == naive_fixed(y, m, d)
assert gregorian_from_fixed(r) == (y, m, d)
assert gregorian_year_from_fixed(r) == y
if naive_leap(y):
r = fixed_from_gregorian(y, 2, 29)
assert r == naive_fixed(y, 2, 29)
assert gregorian_from_fixed(r) == (y, 2, 29)
def test_random_against_naive_count() -> None:
rng = random.Random(2026)
for _ in range(1500):
y, m, d = random_date(rng, -6000, 6000)
r = naive_fixed(y, m, d)
assert fixed_from_gregorian(y, m, d) == r
assert gregorian_from_fixed(r) == (y, m, d)
def test_random_against_datetime() -> None:
rng = random.Random(5)
for _ in range(3000):
y, m, d = random_date(rng, 1, 9999)
date = datetime.date(y, m, d)
r = fixed_from_gregorian(y, m, d)
assert r == date.toordinal()
assert gregorian_from_fixed(r) == (y, m, d)
assert day_of_week(r) == (date.weekday() + 1) % 7
for r in [1, 2, 3652059] + [rng.randint(1, 3652059) for _ in range(2000)]:
date = datetime.date.fromordinal(r)
assert gregorian_from_fixed(r) == (date.year, date.month, date.day)
def test_consecutive_days() -> None:
rng = random.Random(9)
for start in [-146100, -36530, -1500, -5, 730000] + [rng.randint(-(10**8), 10**8) for _ in range(20)]:
date = gregorian_from_fixed(start)
for r in range(start, start + 1000):
assert gregorian_from_fixed(r) == date
assert fixed_from_gregorian(*date) == r
date = next_day(*date)
def test_huge_years() -> None:
rng = random.Random(1)
for _ in range(500):
k = rng.randint(-(10**300), 10**300)
y, m, d = random_date(rng, -800, 800)
r = fixed_from_gregorian(y + 400 * k, m, d)
assert r == naive_fixed(y, m, d) + 146097 * k
assert gregorian_from_fixed(r) == (y + 400 * k, m, d)
assert day_of_week(r) == day_of_week(naive_fixed(y, m, d))
big = rng.randint(-(10**450), 10**450)
assert fixed_from_gregorian(*gregorian_from_fixed(big)) == big
What the next level needs
- the tests did not run in this build