Algorithm Search⌘K

Julian day number in Python

Numbers days by a continuous count from a fixed epoch, which turns date arithmetic into simple integer arithmetic.

File
julian_day_number.py, 58 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
julian_day_number.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/julian-day-number/. The authors are listed in prov/EDDN3S.jsonl.

"""Julian day numbers for dates of the proleptic Gregorian and Julian calendars.

Years use astronomical numbering: 1 BC is year 0, 2 BC is year -1, and so on.
Months run from 1 (January) to 12 (December). Every year, month and day number
is a Python integer of any size; the operator // is floor division, so the
formulas hold for negative values as well. Each function makes a fixed number
of integer operations.
"""


def jdn_from_gregorian(year: int, month: int, day: int) -> int:
    """Julian day number of a date in the proleptic Gregorian calendar."""
    a = (14 - month) // 12  # @line L1
    y = year + 4800 - a  # @line L2
    m = month + 12 * a - 3  # @line L3
    return day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045  # @line L4


def gregorian_from_jdn(jdn: int) -> tuple[int, int, int]:
    """The proleptic Gregorian date (year, month, day) of a Julian day number."""
    a = jdn + 32044  # @line L5
    b = (4 * a + 3) // 146097  # @line L6
    c = a - (146097 * b) // 4  # @line L7
    d = (4 * c + 3) // 1461  # @line L8
    e = c - (1461 * d) // 4  # @line L9
    m = (5 * e + 2) // 153  # @line L10
    day = e - (153 * m + 2) // 5 + 1  # @line L11
    month = m + 3 - 12 * (m // 10)  # @line L12
    year = 100 * b + d - 4800 + m // 10  # @line L13
    return year, month, day  # @line L14


def jdn_from_julian(year: int, month: int, day: int) -> int:
    """Julian day number of a date in the proleptic Julian calendar."""
    a = (14 - month) // 12
    y = year + 4800 - a
    m = month + 12 * a - 3
    return day + (153 * m + 2) // 5 + 365 * y + y // 4 - 32083


def julian_from_jdn(jdn: int) -> tuple[int, int, int]:
    """The proleptic Julian calendar date (year, month, day) of a Julian day number."""
    c = jdn + 32082
    d = (4 * c + 3) // 1461
    e = c - (1461 * d) // 4
    m = (5 * e + 2) // 153
    day = e - (153 * m + 2) // 5 + 1
    month = m + 3 - 12 * (m // 10)
    year = d - 4800 + m // 10
    return year, month, day


def weekday(jdn: int) -> int:
    """Day of the week of a Julian day number: 0 is Sunday, ..., 6 is Saturday."""
    return (jdn + 1) % 7
test_julian_day_number.py 157 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/julian-day-number/. The authors are listed in prov/EDDN3S.jsonl.

import datetime
import random

from julian_day_number import (
    gregorian_from_jdn,
    jdn_from_gregorian,
    jdn_from_julian,
    julian_from_jdn,
    weekday,
)

MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]


def gregorian_leap(y: int) -> bool:
    return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)


def julian_leap(y: int) -> bool:
    return y % 4 == 0


def month_length(y: int, m: int, leap) -> int:
    if m == 2 and leap(y):
        return 29
    return MONTH_LENGTHS[m - 1]


def naive_jdn(y: int, m: int, d: int, leap, anchor_jdn: int) -> int:
    """Count days one year and one month at a time from 1 January 2000 of the calendar."""
    days = 0
    if y >= 2000:
        for yy in range(2000, y):
            days += 366 if leap(yy) else 365
    else:
        for yy in range(y, 2000):
            days -= 366 if leap(yy) else 365
    for mm in range(1, m):
        days += month_length(y, mm, leap)
    return anchor_jdn + days + d - 1


def naive_gregorian(y: int, m: int, d: int) -> int:
    return naive_jdn(y, m, d, gregorian_leap, 2451545)


def naive_julian(y: int, m: int, d: int) -> int:
    # 1 January 2000 in the Julian calendar is 14 January 2000 in the Gregorian calendar.
    return naive_jdn(y, m, d, julian_leap, 2451545 + 13)


def next_day(y: int, m: int, d: int, leap) -> tuple[int, int, int]:
    if d < month_length(y, m, leap):
        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, leap) -> tuple[int, int, int]:
    y = rng.randint(lo, hi)
    m = rng.randint(1, 12)
    d = rng.randint(1, month_length(y, m, leap))
    return y, m, d


def test_examples() -> None:
    assert jdn_from_gregorian(2000, 1, 1) == 2451545
    assert gregorian_from_jdn(2451545) == (2000, 1, 1)
    assert jdn_from_gregorian(-4713, 11, 24) == 0
    assert jdn_from_julian(-4712, 1, 1) == 0
    assert gregorian_from_jdn(0) == (-4713, 11, 24)
    assert julian_from_jdn(0) == (-4712, 1, 1)
    assert jdn_from_julian(1582, 10, 4) == 2299160
    assert jdn_from_gregorian(1582, 10, 15) == 2299161
    assert jdn_from_gregorian(1970, 1, 1) == 2440588
    assert jdn_from_gregorian(1858, 11, 17) == 2400001
    assert jdn_from_gregorian(-4800, 3, 1) == -32044
    assert weekday(2451545) == 6
    assert weekday(2299160) == 4 and weekday(2299161) == 5
    assert weekday(jdn_from_gregorian(1969, 7, 20)) == 0


def test_edges() -> None:
    assert jdn_from_gregorian(2000, 3, 1) - jdn_from_gregorian(2000, 2, 28) == 2
    assert jdn_from_gregorian(1900, 3, 1) - jdn_from_gregorian(1900, 2, 28) == 1
    assert jdn_from_gregorian(2100, 3, 1) - jdn_from_gregorian(2100, 2, 28) == 1
    assert jdn_from_gregorian(0, 3, 1) - jdn_from_gregorian(0, 2, 28) == 2
    assert jdn_from_gregorian(-100, 3, 1) - jdn_from_gregorian(-100, 2, 28) == 1
    assert jdn_from_gregorian(-400, 3, 1) - jdn_from_gregorian(-400, 2, 28) == 2
    assert jdn_from_julian(1900, 3, 1) - jdn_from_julian(1900, 2, 28) == 2
    assert jdn_from_gregorian(2001, 1, 1) - jdn_from_gregorian(2000, 12, 31) == 1
    for j in (-1, 0, 1, -32045, -32044, -32043, 10**6, -(10**6)):
        assert jdn_from_gregorian(*gregorian_from_jdn(j)) == j
        assert jdn_from_julian(*julian_from_jdn(j)) == j
    for y, m, d in ((2000, 2, 29), (1900, 2, 28), (0, 2, 29), (-1, 12, 31), (-4800, 2, 29), (-4801, 1, 1)):
        assert jdn_from_gregorian(y, m, d) == naive_gregorian(y, m, d)
        assert gregorian_from_jdn(jdn_from_gregorian(y, m, d)) == (y, m, d)


def test_random_against_naive_count() -> None:
    rng = random.Random(20261106)
    for _ in range(1500):
        y, m, d = random_date(rng, -7000, 7000, gregorian_leap)
        j = naive_gregorian(y, m, d)
        assert jdn_from_gregorian(y, m, d) == j
        assert gregorian_from_jdn(j) == (y, m, d)
        y, m, d = random_date(rng, -7000, 7000, julian_leap)
        j = naive_julian(y, m, d)
        assert jdn_from_julian(y, m, d) == j
        assert julian_from_jdn(j) == (y, m, d)


def test_random_against_datetime() -> None:
    rng = random.Random(7)
    offset = 1721425  # date(1, 1, 1).toordinal() == 1 and its Julian day number is 1721426
    for _ in range(3000):
        y, m, d = random_date(rng, 1, 9999, gregorian_leap)
        date = datetime.date(y, m, d)
        j = jdn_from_gregorian(y, m, d)
        assert j == date.toordinal() + offset
        assert weekday(j) == (date.weekday() + 1) % 7


def test_consecutive_days() -> None:
    rng = random.Random(11)
    for start in [-40000, -32050, -1, 2299150, 2451500] + [rng.randint(-(10**7), 10**7) for _ in range(20)]:
        date = gregorian_from_jdn(start)
        jdate = julian_from_jdn(start)
        for j in range(start, start + 800):
            assert gregorian_from_jdn(j) == date
            assert julian_from_jdn(j) == jdate
            assert jdn_from_gregorian(*date) == j
            date = next_day(*date, gregorian_leap)
            jdate = next_day(*jdate, julian_leap)


def test_huge_years() -> None:
    rng = random.Random(3)
    for _ in range(500):
        k = rng.randint(-(10**300), 10**300)
        y, m, d = random_date(rng, -800, 800, gregorian_leap)
        base = naive_gregorian(y, m, d)
        j = jdn_from_gregorian(y + 400 * k, m, d)
        assert j == base + 146097 * k
        assert gregorian_from_jdn(j) == (y + 400 * k, m, d)
        y, m, d = random_date(rng, -800, 800, julian_leap)
        base = naive_julian(y, m, d)
        j = jdn_from_julian(y + 4 * k, m, d)
        assert j == base + 1461 * k
        assert julian_from_jdn(j) == (y + 4 * k, m, d)
        big = rng.randint(-(10**400), 10**400)
        assert jdn_from_gregorian(*gregorian_from_jdn(big)) == big
        assert jdn_from_julian(*julian_from_jdn(big)) == big

What the next level needs

  • the tests did not run in this build

Read the full page about Julian day number