mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-23 03:07:12 +00:00
the fast was reading the calendar and ignoring who was on it
An adversarial pass found the rules throwing away the feast rank that
describe() computes, in exactly the seasons where it decides the answer.
- A saint of vigil or polyeleos rank now lifts a weekday of a fast, to wine
and oil inside the Great Fast and as far as fish in the three lesser
ones. This is why the printed calendars mark so many Wednesdays in the
Nativity Fast; 74 days across 2020-2030 were being reported stricter than
any published calendar keeps them, St Nicholas and the Forty Martyrs of
Sebaste among them.
- The Annunciation was tested after Great Friday and Great Saturday, so on
the years it lands there it vanished into them entirely -- the Great Feast
of the Incarnation demoted to a bullet, reported as a total fast, and its
rank overwritten with the lesser movable one the rules then read. It is
now tested first and lifts those days to wine and oil. Five such years
this century on the Gregorian paschalion, seven on the Julian.
- Lazarus Saturday and Palm Sunday belonged to no season at all, sitting
between the Forty Days and Great Week. Any rule keying on the season fell
through, so Byzantine Catholic jurisdictions reported a meat glyph and
'no fast' on Palm Sunday.
- The eves of the Nativity and Theophany are relaxed to wine and oil when
they fall on a weekend, which ruthenian.py already implemented and this
module did not.
Also: iCalendar folded on characters where RFC 5545 counts octets, so an
emoji put lines up to 80 octets over a 75-octet limit; a malformed --date
printed a traceback; the overlaid menologion was rebuilt for every single day
asked about; the Nativity of the Forerunner was ranked below its class.
Fourteen new tests, each one able to fail. The old sweep asserted only
all(d.fast), and 'none' is a truthy string.
This commit is contained in:
+140
-2
@@ -7,10 +7,10 @@ import pytest
|
||||
|
||||
from typikon import describe, gregorian_pascha, julian_pascha, resolve
|
||||
from typikon.calendars import from_julian, julian_offset, to_julian
|
||||
from typikon.day import week
|
||||
from typikon.day import week, year_of
|
||||
from typikon.export import to_ics, to_json
|
||||
from typikon.fasting import (DAIRY, FISH, NONE, OBLIGATORY, STRICT, WINE_OIL,
|
||||
XEROPHAGY)
|
||||
XEROPHAGY, stricter)
|
||||
|
||||
|
||||
# ───────────────────────────── the paschalion ─────────────────────────────
|
||||
@@ -228,3 +228,141 @@ def test_a_calendar_name_resolves_without_naming_a_church():
|
||||
assert resolve("new").menologion == "revised_julian"
|
||||
assert resolve("western").paschalion == "gregorian"
|
||||
assert resolve(None).key == "new"
|
||||
|
||||
|
||||
# ─────────────────── regressions, each from a real defect ──────────────────
|
||||
|
||||
def test_the_annunciation_is_never_lost_to_great_friday():
|
||||
"""It can fall on Great Friday or Great Saturday, and when it does the
|
||||
feast lifts the total fast rather than disappearing into it."""
|
||||
for when, jurisdiction in ((dt.date(2016, 3, 25), "western"),
|
||||
(dt.date(2005, 3, 25), "western"),
|
||||
(dt.date(2029, 4, 7), "rocor"),
|
||||
(dt.date(2018, 4, 7), "rocor")):
|
||||
d = describe(when, jurisdiction)
|
||||
assert d.church_date == "25 March", when
|
||||
assert "Annunciation" in d.feast, when
|
||||
assert d.rank == "great", when
|
||||
assert d.fast == WINE_OIL, (when, d.fast)
|
||||
|
||||
|
||||
def test_kyriopascha_and_the_annunciation_in_bright_week_stay_fast_free():
|
||||
assert describe(dt.date(2035, 3, 25), "western").fast == NONE
|
||||
assert describe(dt.date(2075, 4, 7), "rocor").fast == NONE
|
||||
|
||||
|
||||
def test_palm_sunday_and_lazarus_saturday_are_still_the_fast():
|
||||
"""Both fall between the Forty Days and Great Week. Leaving that gap
|
||||
outside every season made the Byzantine Catholic rule report no fast at
|
||||
all on Palm Sunday."""
|
||||
for jurisdiction in ("ruthenian", "ugcc", "melkite", "goarch", "rocor"):
|
||||
for year in (2025, 2026, 2027):
|
||||
mv = year_of(year, jurisdiction)
|
||||
for when in (mv["lazarus_saturday"], mv["palm_sunday"]):
|
||||
d = describe(when, jurisdiction)
|
||||
assert d.season is not None, (jurisdiction, when)
|
||||
assert d.fast != NONE, (jurisdiction, when, d.fast)
|
||||
assert d.reason, (jurisdiction, when)
|
||||
|
||||
|
||||
def test_a_ranked_saint_relaxes_a_weekday_of_a_fasting_season():
|
||||
"""The rank is why the printed calendars mark so many Wednesdays in the
|
||||
Nativity Fast for wine and oil."""
|
||||
# The Nativity of the Forerunner, a great feast inside the Apostles' Fast.
|
||||
assert describe(dt.date(2020, 6, 24), "goarch").fast == FISH
|
||||
# The Forty Martyrs of Sebaste, the textbook relaxation of a Lenten weekday.
|
||||
assert describe(dt.date(2022, 3, 9), "goarch").fast == WINE_OIL
|
||||
# St Nicholas, a vigil saint on a Wednesday of the Nativity Fast.
|
||||
assert describe(dt.date(2023, 12, 6), "goarch").fast == WINE_OIL
|
||||
|
||||
|
||||
def test_no_saint_brings_fish_into_the_great_fast_or_lifts_great_week():
|
||||
for d in week(dt.date(2022, 3, 7), 40, "goarch"):
|
||||
if d.season == "Great Fast" and d.church_date != "25 March":
|
||||
assert not stricter(FISH, d.fast) or d.fast != FISH, d.date
|
||||
if d.season == "Great and Holy Week" and d.church_date != "25 March":
|
||||
assert d.fast in (STRICT, XEROPHAGY, WINE_OIL), (d.date, d.fast)
|
||||
|
||||
|
||||
def test_the_eves_relax_when_they_fall_on_a_weekend():
|
||||
"""The Liturgy of St Basil is served with Vespers, so the eve is kept at
|
||||
wine and oil rather than xerophagy."""
|
||||
assert describe(dt.date(2022, 12, 24), "goarch").fast == WINE_OIL
|
||||
assert describe(dt.date(2023, 12, 24), "goarch").fast == WINE_OIL
|
||||
assert describe(dt.date(2025, 1, 5), "goarch").fast == WINE_OIL
|
||||
# A weekday eve is still xerophagy.
|
||||
assert describe(dt.date(2024, 12, 24), "goarch").fast == XEROPHAGY
|
||||
|
||||
|
||||
def test_the_apostles_fast_is_empty_when_pascha_is_late():
|
||||
"""A late Pascha can leave the fast with no days at all, which is correct
|
||||
rather than a bug -- and the old calendar never has that problem."""
|
||||
goarch = [d for d in week(dt.date(2024, 5, 1), 120, "goarch")
|
||||
if d.season == "Apostles' Fast"]
|
||||
rocor = [d for d in week(dt.date(2024, 5, 1), 120, "rocor")
|
||||
if d.season == "Apostles' Fast"]
|
||||
assert goarch == []
|
||||
assert len(rocor) > 0
|
||||
|
||||
|
||||
# ──────────────────────── invariants, not samples ──────────────────────────
|
||||
|
||||
def test_the_paschalion_invariants_hold_for_two_centuries():
|
||||
for year in range(1900, 2101):
|
||||
j, g = julian_pascha(year), gregorian_pascha(year)
|
||||
assert j.weekday() == 6, year
|
||||
assert g.weekday() == 6, year
|
||||
assert dt.date(year, 4, 4) <= j <= dt.date(year, 5, 8), (year, j)
|
||||
assert dt.date(year, 3, 22) <= g <= dt.date(year, 4, 25), (year, g)
|
||||
|
||||
|
||||
def test_every_civil_date_round_trips_through_the_julian_calendar():
|
||||
day, last = dt.date(1800, 1, 1), dt.date(2200, 12, 31)
|
||||
while day <= last:
|
||||
y, m, d = to_julian(day)
|
||||
assert from_julian(y, m, d) == day, day
|
||||
day += dt.timedelta(1)
|
||||
|
||||
|
||||
def test_the_julian_offset_widens_the_day_after_a_julian_leap_day():
|
||||
for before, after in ((dt.date(1900, 3, 13), dt.date(1900, 3, 14)),
|
||||
(dt.date(2100, 3, 14), dt.date(2100, 3, 15)),
|
||||
(dt.date(2200, 3, 15), dt.date(2200, 3, 16))):
|
||||
assert julian_offset(after) == julian_offset(before) + 1, after
|
||||
|
||||
|
||||
def test_ics_content_lines_stay_within_seventy_five_octets():
|
||||
for jurisdiction in ("goarch", "rocor", "ruthenian"):
|
||||
text = to_ics(week(dt.date(2026, 1, 1), 365, jurisdiction))
|
||||
for line in text.split("\r\n"):
|
||||
assert len(line.encode()) <= 75, (jurisdiction, line)
|
||||
|
||||
|
||||
def test_every_jurisdiction_holds_together_for_twenty_years():
|
||||
"""The regression net. Replaces a loop that asserted only truthiness and
|
||||
so could not fail: "none" is a truthy string."""
|
||||
from typikon import JURISDICTIONS
|
||||
from typikon.fasting import LEVELS
|
||||
for key in JURISDICTIONS:
|
||||
for d in week(dt.date(2020, 1, 1), 366 * 21 // 4, key):
|
||||
assert d.fast in LEVELS, (key, d.date, d.fast)
|
||||
assert d.binds in LEVELS, (key, d.date, d.binds)
|
||||
assert d.commemorations, (key, d.date)
|
||||
assert not stricter(d.binds, d.fast), (key, d.date)
|
||||
assert d.title, (key, d.date)
|
||||
|
||||
|
||||
# ──────────────────────────────── the CLI ──────────────────────────────────
|
||||
|
||||
def test_the_cli_rejects_a_malformed_date_instead_of_traversing_a_traceback():
|
||||
from typikon.cli import main
|
||||
assert main(["-d", "tomorrow"]) == 2
|
||||
assert main(["-d", "2026-02-30"]) == 2
|
||||
assert main(["--year", "99999"]) == 2
|
||||
assert main(["--jurisdiction", "atlantis"]) == 2
|
||||
|
||||
|
||||
def test_the_cli_elides_on_a_word():
|
||||
from typikon.cli import _elide
|
||||
assert _elide("The Radiant and Life-giving Resurrection", 24) == "The Radiant and…"
|
||||
assert _elide("Short", 24) == "Short"
|
||||
|
||||
Reference in New Issue
Block a user