web-fetcher/build/lib/web_fetcher/periods.py
tegwick 49de8d15b6
All checks were successful
Tests / unittest-py310 (push) Successful in 9s
Tests / unittest-py313 (push) Successful in 13s
WEBFETCH-WP-0003: Forgejo CI, org licence, inbox acks, progress
- .forgejo/workflows/test.yaml: offline tests on Python 3.10 and 3.13
- LICENSE: Target Revenue Source License V1C1 (org-wide preliminary licence)
- pyproject: license field; AGENTS/SCOPE: CI and licence facts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 153021@bnt-lap001
Assistant-Session: 46917be4-6400-4106-b8b7-61953a0caf24
2026-10-19 14:27:11 +02:00

62 lines
2.5 KiB
Python

"""Period ordering for "latest value" selection.
A source's period labels are only compared when they share one recognised
format; mixed or unrecognised formats are an error, never a silent string
comparison. Spec field `period_format` pins the format ("auto" by default);
"string" opts in to plain lexicographic order.
"""
from __future__ import annotations
import re
from typing import Callable
_FORMATS: dict[str, tuple[re.Pattern[str], Callable[[re.Match[str]], tuple[int, ...]]]] = {
"year": (re.compile(r"^(\d{4})$"), lambda m: (int(m[1]),)),
"month": (re.compile(r"^(\d{4})-(\d{2})$"), lambda m: (int(m[1]), int(m[2]))),
"date": (re.compile(r"^(\d{4})-(\d{2})-(\d{2})$"), lambda m: (int(m[1]), int(m[2]), int(m[3]))),
"quarter": (re.compile(r"^(\d{4})-?Q([1-4])$"), lambda m: (int(m[1]), int(m[2]))),
"semester": (re.compile(r"^(\d{4})-?S([12])$"), lambda m: (int(m[1]), int(m[2]))),
"week": (re.compile(r"^(\d{4})-?W(\d{2})$"), lambda m: (int(m[1]), int(m[2]))),
# Fixed-width digit labels such as YYYYMM or Census BTOS YYYYPP.
"digits": (re.compile(r"^(\d+)$"), lambda m: (len(m[1]), int(m[1]))),
}
class PeriodError(ValueError):
pass
def detect(label: str) -> str | None:
label = label.strip()
for name, (pat, _) in _FORMATS.items():
if pat.match(label):
return name
return None
def sort_key(label: str, fmt: str) -> tuple[int, ...] | str:
if fmt == "string":
return label
pat, key = _FORMATS[fmt]
m = pat.match(label.strip())
if m is None:
raise PeriodError(f"period {label!r} does not match format {fmt!r}")
return key(m)
def order(labels: list[str], fmt: str = "auto") -> tuple[str, list[str]]:
"""Return (format, labels newest first). Raises PeriodError for mixed formats."""
if fmt == "auto":
found = {detect(lbl) for lbl in labels}
if None in found:
bad = [lbl for lbl in labels if detect(lbl) is None][:3]
raise PeriodError(f"unrecognised period labels {bad}; set period_format")
if len(found) > 1:
raise PeriodError(f"mixed period formats {sorted(found)}; set period_format")
fmt = found.pop() if found else "string"
if fmt == "digits" and len({len(lbl.strip()) for lbl in labels}) > 1:
raise PeriodError("digit period labels of different widths; set period_format")
elif fmt != "string" and fmt not in _FORMATS:
raise PeriodError(f"unknown period_format {fmt!r}")
return fmt, sorted(labels, key=lambda lbl: sort_key(lbl, fmt), reverse=True)