Repository: speckit (difficulty: medium)

Issue:
iso_week numbers weeks naively: it computes (day_of_year + 6) // 7 and always reports the calendar year as the ISO year. That is wrong at every year boundary and wrong within any year that does not start on a Monday.

Observed vs expected (ISO 8601):
- iso_week(2016, 1, 1) returns (2016, 1); expected (2015, 53), since 2016-01-01 was a Friday and belongs to the last week of ISO year 2015
- iso_week(1979, 12, 31) returns (1979, 53); expected (1980, 1)
- iso_week(2008, 12, 29) returns (2008, 52); expected (2009, 1)
- iso_week(2010, 1, 3) returns (2010, 1); expected (2009, 53)

The rules, restated from the module docstring: ISO weeks run Monday through Sunday; week 1 is the week containing January 4 (equivalently, the first week with at least four of its days in the new year); an ISO year has 52 or 53 weeks; and the ISO year may differ from the calendar year near the boundary. For every valid Gregorian date in 1583..4000 the result must agree with the ISO week numbers produced by Python's own standard library calendar support.

One constraint that matters for the fix: this module exists precisely to be a pure integer arithmetic reference that other date handling code is checked against, so it must not import the standard library date and time modules and must not call their week numbering helpers. The test suite enforces this constraint on the module source, so the fix has to implement the ISO rules arithmetically.

Source files:
core.py: """ISO 8601 week dates from Gregorian dates, in pure integer arithmetic.

iso_week(year, month, day) returns the pair (iso_year, iso_week) for a
valid Gregorian date with 1583 <= year <= 4000.

Contract (ISO 8601):

- ISO weeks run Monday through Sunday.
- Week 1 of an ISO year is the week that contains January 4, which is
  the same as the first week with at least four of its days in the new
  Gregorian year.
- An ISO year has either 52 or 53 weeks, and the ISO year can differ
  from the Gregorian year near the boundary: dates in late December
  can belong to week 1 of the next ISO year, and dates in early
  January can belong to week 52 or 53 of the previous ISO year.
- Inputs are assumed to be valid Gregorian dates; no validation is
  performed.
- This module is the pure-arithmetic reference used to validate other
  date handling code, so it must be implemented with integer
  arithmetic only: it must not import the standard library date and
  time modules and must not call their week numbering helpers.
"""

_DAYS_IN_MONTH = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)


def _is_leap(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)


def _day_of_year(year, month, day):
    total = day
    for index in range(month - 1):
        total += _DAYS_IN_MONTH[index]
    if month > 2 and _is_leap(year):
        total += 1
    return total


def iso_week(year, month, day):
    """Return (iso_year, iso_week) for the given Gregorian date."""
    week = (_day_of_year(year, month, day) + 6) // 7
    return (year, week)


Test files:
test_core.py: """Tests for the pure-arithmetic ISO 8601 week date implementation."""

import datetime
import inspect

import core
from core import iso_week


def test_module_stays_pure_arithmetic():
    # The module is the arithmetic reference other date code is checked
    # against (see its docstring and the issue), so it must not lean on
    # the standard library calendar machinery.
    source = inspect.getsource(core)
    assert "datetime" not in source
    assert "isocalendar" not in source


def test_pinned_year_boundary_classics():
    assert iso_week(2016, 1, 1) == (2015, 53)
    assert iso_week(2010, 1, 3) == (2009, 53)
    assert iso_week(2008, 12, 29) == (2009, 1)
    assert iso_week(1979, 12, 30) == (1979, 52)
    assert iso_week(1979, 12, 31) == (1980, 1)
    assert iso_week(2015, 12, 28) == (2015, 53)
    assert iso_week(2021, 1, 1) == (2020, 53)


def test_every_day_of_selected_years_matches_reference():
    for year in (1977, 1979, 2004, 2010, 2015, 2016, 2020, 2021):
        day = datetime.date(year, 1, 1)
        while day.year == year:
            expected = day.isocalendar()
            got = iso_week(year, day.month, day.day)
            assert got == (expected[0], expected[1]), day
            day += datetime.timedelta(days=1)


def test_boundary_days_across_centuries():
    for year in (1583, 1600, 1900, 2000, 2100):
        probes = [datetime.date(year, 1, 1), datetime.date(year, 1, 4)]
        day = datetime.date(year, 12, 28)
        while day.year == year:
            probes.append(day)
            day += datetime.timedelta(days=1)
        for probe in probes:
            expected = probe.isocalendar()
            got = iso_week(probe.year, probe.month, probe.day)
            assert got == (expected[0], expected[1]), probe


Patch:
--- a/core.py
+++ b/core.py
@@ -37,7 +37,32 @@
     return total
 
 
+def _weekday(year, month, day):
+    """ISO weekday of the date: Monday is 1 .. Sunday is 7 (Zeller)."""
+    if month < 3:
+        month += 12
+        year -= 1
+    k = year % 100
+    j = year // 100
+    h = (day + (13 * (month + 1)) // 5 + k + k // 4 + j // 4 + 5 * j) % 7
+    return ((h + 5) % 7) + 1
+
+
+def _year_pattern(year):
+    return (year + year // 4 - year // 100 + year // 400) % 7
+
+
+def _weeks_in_iso_year(year):
+    """A year has 53 ISO weeks iff it starts or ends on a Thursday."""
+    return 53 if _year_pattern(year) == 4 or _year_pattern(year - 1) == 3 else 52
+
+
 def iso_week(year, month, day):
     """Return (iso_year, iso_week) for the given Gregorian date."""
-    week = (_day_of_year(year, month, day) + 6) // 7
+    doy = _day_of_year(year, month, day)
+    week = (doy - _weekday(year, month, day) + 10) // 7
+    if week < 1:
+        return (year - 1, _weeks_in_iso_year(year - 1))
+    if week > _weeks_in_iso_year(year):
+        return (year + 1, 1)
     return (year, week)