diff --git a/backend/app/analysis/periods.py b/backend/app/analysis/periods.py index 63e6fd28..fd29fab1 100644 --- a/backend/app/analysis/periods.py +++ b/backend/app/analysis/periods.py @@ -126,7 +126,7 @@ def _shape(value: Any) -> str | None: text = value.strip() if (m := _MONTH.match(text)) and 1 <= int(m.group(2)) <= 12 and 1900 <= int(m.group(1)) <= 2200: return "month" - if _WEEK.match(text): + if (m := _WEEK.match(text)) and int(m.group(2)) <= 53: return "week" if _QUARTER.match(text): return "quarter" @@ -161,17 +161,18 @@ def _first_weekday(start: date, weekday: int) -> date: def week_span(scheme: str, year: int, week: int) -> tuple[date, date] | None: """按周号算法,year 年第 week 周覆盖的第一天和最后一天(含)。这种算法里没有这一周(ISO 只有 52 周的年份写了第 53 周、 - 1 月 1 日就是周一的年份写了第 0 周……)为 None。""" - if scheme == "iso": - try: - start = date.fromisocalendar(year, week, 1) - except ValueError: - return None - return start, start + timedelta(days=6) + 1 月 1 日就是周一的年份写了第 0 周……)、日子超出日期能表示的范围的为 None。""" try: - jan1, dec31 = date(year, 1, 1), date(year, 12, 31) - except ValueError: + return _week_span(scheme, year, week) + except (OverflowError, ValueError): return None + + +def _week_span(scheme: str, year: int, week: int) -> tuple[date, date] | None: + if scheme == "iso": + start = date.fromisocalendar(year, week, 1) + return start, start + timedelta(days=6) + jan1, dec31 = date(year, 1, 1), date(year, 12, 31) if scheme == "jan1": if not 1 <= week <= 53: return None diff --git a/backend/tests/test_analysis_periods.py b/backend/tests/test_analysis_periods.py index 76ff612e..07462b29 100644 --- a/backend/tests/test_analysis_periods.py +++ b/backend/tests/test_analysis_periods.py @@ -140,3 +140,10 @@ def test_a_plain_date_column_is_a_day_even_when_two_values_are_a_week_apart(): assert pd.parse_compared(["2026-10-13", "2026-10-20"], plain, "day", "sqlite")[1] == "day" assert pd.parse_compared(["2026-10-12", "2026-10-19"], starts, "week", "sqlite")[1] == "week" assert pd.parse_compared(["2026-09-01", "2026-10-01"], plain, "day", "sqlite")[1] == "month" + + + +def test_week_numbers_out_of_any_algorithm_are_unreadable(): + assert pd.parse(["2026-W41", "2026-W99"]) is None + assert pd.unreadable(["2026-W41", "2026-W99"]) == ("shape", ["2026-W99"]) + assert pd.week_span("sunday_year", 9999, 53) is None and pd.week_span("iso", 0, 1) is None