From 2efea2b753c7056e438d237bee58de0c96b282cc Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 16:56:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=A8=E5=8F=B7=E8=B6=85=E5=87=BA=E4=BB=BB?= =?UTF-8?q?=E4=BD=95=E7=AE=97=E6=B3=95=EF=BC=88W54=20=E4=BB=A5=E4=B8=8A?= =?UTF-8?q?=EF=BC=89=E7=9A=84=E8=AE=A4=E4=B8=8D=E5=87=BA=E3=80=81=E7=AE=97?= =?UTF-8?q?=E8=B5=B7=E6=AD=A2=E8=B6=85=E5=87=BA=E6=97=A5=E6=9C=9F=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E7=9A=84=E5=BD=93=E8=BF=99=E4=B8=80=E5=91=A8=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=EF=BC=88=E5=A4=8D=E6=A0=B8=20R3=20=E8=A1=A5=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E8=AE=A4=E4=B8=8D=E5=87=BA=E7=AE=97=E6=B3=95=E6=97=B6?= =?UTF-8?q?=E5=8E=9F=E6=9D=A5=E6=8C=89=E5=91=A8=E5=8F=B7=E6=8E=92=E5=85=88?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E3=80=8C2026-W99=E3=80=8D=E4=B9=9F=E4=BC=9A?= =?UTF-8?q?=E5=BD=93=E4=B8=80=E5=91=A8=E6=94=B6=E4=B8=8B=EF=BC=8C=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E5=91=A8=E5=8F=B7=E5=A4=A7=E4=BA=8E=2053=20=E7=9A=84?= =?UTF-8?q?=E4=B8=8D=E5=BD=93=E5=91=A8=E5=8F=B7=E5=86=99=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E9=87=8C=E7=85=A7=E5=AE=9E=E4=B8=BE=E5=AE=83?= =?UTF-8?q?=EF=BC=9B=E6=8C=89=E7=AE=97=E6=B3=95=E7=AE=97=E8=B5=B7=E6=AD=A2?= =?UTF-8?q?=E6=97=B6=E6=97=A5=E5=AD=90=E8=B6=85=E5=87=BA=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E8=83=BD=E8=A1=A8=E7=A4=BA=E7=9A=84=E8=8C=83=E5=9B=B4=EF=BC=88?= =?UTF-8?q?=E5=85=AC=E5=85=83=209999=20=E5=B9=B4=E6=9C=AB=EF=BC=89?= =?UTF-8?q?=E4=BA=A4=E5=9B=9E=E6=B2=A1=E6=9C=89=E8=BF=99=E4=B8=80=E5=91=A8?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E6=8A=9B=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/analysis/periods.py | 21 +++++++++++---------- backend/tests/test_analysis_periods.py | 7 +++++++ 2 files changed, 18 insertions(+), 10 deletions(-) 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