diff --git a/wizard-work/beam/test_zeller_dow.py b/wizard-work/beam/test_zeller_dow.py new file mode 100644 index 0000000..4643013 --- /dev/null +++ b/wizard-work/beam/test_zeller_dow.py @@ -0,0 +1,31 @@ +import unittest +from zeller_dow import dow + +class TestZellerDow(unittest.TestCase): + def test_gap_dates_raise(self): + for day in range(5, 15): + with self.assertRaises(ValueError): + dow(1582, 10, day) + + def test_silent_month_length_aliases_raise(self): + cases = [(2026, 2, 31), (2026, 2, 30), (2026, 4, 31), + (1900, 2, 29), (2000, 2, 30), (2024, 13, 1), + (2024, 0, 1), (2024, 1, 0), (2024, 1, 32)] + for y, m, d in cases: + with self.assertRaises(ValueError): + dow(y, m, d) + + def test_valid_anchors(self): + # 1582-10-15 was a Friday (Gregorian cutover) + self.assertEqual(dow(1582, 10, 15), "Friday") + # 1582-10-04 Julian Thursday + self.assertEqual(dow(1582, 10, 4), "Thursday") + self.assertEqual(dow(2024, 2, 29), "Thursday") # leap day exists + self.assertEqual(dow(2026, 10, 27), "Tuesday") + + def test_leap_day_invalid_non_leap(self): + with self.assertRaises(ValueError): + dow(2023, 2, 29) + +if __name__ == "__main__": + unittest.main() diff --git a/wizard-work/beam/zeller_dow.py b/wizard-work/beam/zeller_dow.py index 98e908a..67a6a87 100644 --- a/wizard-work/beam/zeller_dow.py +++ b/wizard-work/beam/zeller_dow.py @@ -1,8 +1,26 @@ """Day-of-week via Julian Day Number, handling Julian/Gregorian transition. Dates before 1582-10-15 use the Julian calendar; on/after use Gregorian. -The gap 1582-10-05..14 did not exist. +The gap 1582-10-05..14 did not exist, so those dates are invalid. +Month lengths (including leap-year February) are validated per calendar. """ +def _julian_leap(y): + return y % 4 == 0 + +def _gregorian_leap(y): + return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0) + +def _is_julian(y, m, d): + return (y, m, d) < (1582, 10, 15) + +def _days_in_month(y, m, julian): + if m in (1, 3, 5, 7, 8, 10, 12): + return 31 + if m in (4, 6, 9, 11): + return 30 + leap = _julian_leap(y) if julian else _gregorian_leap(y) + return 29 if leap else 28 + def _jdn_julian(y, m, d): a = (14 - m) // 12 yy = y + 4800 - a @@ -18,9 +36,15 @@ def _jdn_gregorian(y, m, d): NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] def dow(y, m, d): - if not (1 <= m <= 12 and 1 <= d <= 31): - raise ValueError("invalid month/day") - if (y, m, d) < (1582, 10, 15): + if not (1 <= m <= 12): + raise ValueError("invalid month") + julian = _is_julian(y, m, d) + # gap dates did not exist + if (1582, 10, 5) <= (y, m, d) <= (1582, 10, 14): + raise ValueError("date did not exist (Julian/Gregorian gap)") + if not (1 <= d <= _days_in_month(y, m, julian)): + raise ValueError("invalid day for month") + if julian: jdn = _jdn_julian(y, m, d) else: jdn = _jdn_gregorian(y, m, d)