feat(normalizer): feast + season resolution
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,42 @@
|
|||||||
"""Tolerant historical date parsing for the family archive."""
|
"""Tolerant historical date parsing for the family archive."""
|
||||||
import datetime
|
import datetime
|
||||||
|
from enum import StrEnum
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
class Precision(StrEnum):
|
||||||
|
DAY = "DAY"
|
||||||
|
MONTH = "MONTH"
|
||||||
|
SEASON = "SEASON"
|
||||||
|
YEAR = "YEAR"
|
||||||
|
RANGE = "RANGE"
|
||||||
|
APPROX = "APPROX"
|
||||||
|
UNKNOWN = "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
def _advent_sunday(year: int, n: int) -> datetime.date:
|
||||||
|
"""n-th Advent (1..4). 4th Advent = last Sunday on/before Dec 24."""
|
||||||
|
dec24 = datetime.date(year, 12, 24)
|
||||||
|
back_to_sunday = (dec24.weekday() - 6) % 7 # Mon=0..Sun=6
|
||||||
|
fourth = dec24 - datetime.timedelta(days=back_to_sunday)
|
||||||
|
return fourth - datetime.timedelta(days=(4 - n) * 7)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_feast_or_season(token: str, year: int):
|
||||||
|
"""Return (iso, Precision) for a known feast/season token, else None."""
|
||||||
|
key = " ".join(token.lower().split()).strip(" .")
|
||||||
|
if key in config.MOVABLE_FEASTS:
|
||||||
|
d = easter(year) + datetime.timedelta(days=config.MOVABLE_FEASTS[key])
|
||||||
|
return d.isoformat(), Precision.DAY
|
||||||
|
if key in config.FIXED_FEASTS:
|
||||||
|
month, day = config.FIXED_FEASTS[key]
|
||||||
|
return datetime.date(year, month, day).isoformat(), Precision.DAY
|
||||||
|
advent = {"1. advent": 1, "2. advent": 2, "3. advent": 3, "4. advent": 4, "advent": 1}
|
||||||
|
if key in advent:
|
||||||
|
return _advent_sunday(year, advent[key]).isoformat(), Precision.DAY
|
||||||
|
if key in config.SEASON_MONTHS:
|
||||||
|
return datetime.date(year, config.SEASON_MONTHS[key], 1).isoformat(), Precision.SEASON
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def easter(year: int) -> datetime.date:
|
def easter(year: int) -> datetime.date:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import dates
|
import dates
|
||||||
|
from dates import Precision
|
||||||
|
|
||||||
def test_easter_known_years():
|
def test_easter_known_years():
|
||||||
# Anonymous Gregorian algorithm — verified against published tables
|
# Anonymous Gregorian algorithm — verified against published tables
|
||||||
@@ -7,3 +8,19 @@ def test_easter_known_years():
|
|||||||
assert dates.easter(2000) == datetime.date(2000, 4, 23)
|
assert dates.easter(2000) == datetime.date(2000, 4, 23)
|
||||||
assert dates.easter(1922) == datetime.date(1922, 4, 16)
|
assert dates.easter(1922) == datetime.date(1922, 4, 16)
|
||||||
assert dates.easter(1888) == datetime.date(1888, 4, 1)
|
assert dates.easter(1888) == datetime.date(1888, 4, 1)
|
||||||
|
|
||||||
|
def test_resolve_feast_movable():
|
||||||
|
assert dates.resolve_feast_or_season("Pfingsten", 1922) == ("1922-06-04", Precision.DAY)
|
||||||
|
assert dates.resolve_feast_or_season("Ostern", 2024) == ("2024-03-31", Precision.DAY)
|
||||||
|
assert dates.resolve_feast_or_season("Pfingstmontag", 1922) == ("1922-06-05", Precision.DAY)
|
||||||
|
|
||||||
|
def test_resolve_feast_fixed():
|
||||||
|
assert dates.resolve_feast_or_season("Weihnachten", 1900) == ("1900-12-25", Precision.DAY)
|
||||||
|
assert dates.resolve_feast_or_season("Neujahr", 1910) == ("1910-01-01", Precision.DAY)
|
||||||
|
|
||||||
|
def test_resolve_season():
|
||||||
|
assert dates.resolve_feast_or_season("Herbst", 1913) == ("1913-10-01", Precision.SEASON)
|
||||||
|
assert dates.resolve_feast_or_season("Sommer", 1910) == ("1910-07-01", Precision.SEASON)
|
||||||
|
|
||||||
|
def test_resolve_unknown_token_returns_none():
|
||||||
|
assert dates.resolve_feast_or_season("Freitag", 1919) is None
|
||||||
|
|||||||
Reference in New Issue
Block a user