diff options
| -rw-r--r-- | internal/calendar/calendar.go | 29 | ||||
| -rw-r--r-- | internal/calendar/precedence_ef.go | 37 | ||||
| -rw-r--r-- | internal/calendar/precedence_ef_repro_test.go | 39 |
3 files changed, 101 insertions, 4 deletions
diff --git a/internal/calendar/calendar.go b/internal/calendar/calendar.go index f364321..19c0efb 100644 --- a/internal/calendar/calendar.go +++ b/internal/calendar/calendar.go @@ -258,16 +258,39 @@ func transferIfImpededEF(cel Celebration, when time.Time, occupiedByClass1, occu if cel.Rank != RankClass1 { return when } + // Is cel actually impeded on `when` at all? Compare its OWN precedence + // (via precedenceEF, the same function pickEF uses to decide the day) + // against the temporal office's, rather than testing the temporal + // office's class in isolation -- a plain "is the temporal day I class" + // test wrongly impedes a candidate that would in fact WIN a tie against + // it: RG 91 entry 4 (the Immaculate Conception, the Assumption) sits + // ABOVE entry 6 (Sundays), so the Immaculate Conception is not impeded + // by the Advent Sunday it may fall on at all, unlike an ordinary I-class + // feast (entry 11, e.g. St Joseph), which is. occupiedByClass1 still + // covers the separate case of a competing FIXED I-class SANCTORAL + // feast, which this temporal-only comparison cannot see. startTemporal := temporalEF(when) - startImpeded := startTemporal.Cel.Rank == RankClass1 || occupiedByClass1(when) + tCand := candidate{Cel: startTemporal.Cel, Temporal: true, Season: startTemporal.Season, Sunday: startTemporal.Sunday} + sCand := candidate{Cel: cel, Temporal: false} + startImpeded := precedenceEF(tCand) < precedenceEF(sCand) || occupiedByClass1(when) if !startImpeded { return when } day := when.AddDate(0, 0, 1) for i := 0; i < 30; i++ { b := temporalEF(day) - band := precedenceEF(candidate{Cel: b.Cel, Temporal: true, Season: b.Season, Sunday: b.Sunday}) - if band <= 3 || occupiedByClass1Or2(day) { + // A direct CLASS test, not precedenceEF's tie-break band: band + // encodes which of two EQUAL-class candidates wins a tie (e.g. a + // Sunday beats an equal-class feast, but a privileged Ember/late- + // Advent FERIA of the same class yields to one, defect 2) -- an + // unrelated question from RG 96's own "is this day itself I or II + // class" test. Reusing band here undercounted: a day within the + // Octave of the Nativity (26-31 Dec) is genuinely II class (RG 67), + // but as an ordinary, non-Sunday II-class temporal candidate its OWN + // band yields (5, not <=3) -- so band<=3 alone let an impeded I-class + // feast wrongly land there. isHighClass tests the class directly. + isHighClass := b.Cel.Rank == RankClass1 || b.Cel.Rank == RankClass2 + if isHighClass || occupiedByClass1Or2(day) { day = day.AddDate(0, 0, 1) // still I- or II-class; keep walking continue } diff --git a/internal/calendar/precedence_ef.go b/internal/calendar/precedence_ef.go index a1788be..9f5016f 100644 --- a/internal/calendar/precedence_ef.go +++ b/internal/calendar/precedence_ef.go @@ -1,6 +1,9 @@ package calendar -import "sort" +import ( + "sort" + "strings" +) // efRankOrder orders EF (1960) ranks: class-1 highest, then class-2..4, // commemoration, ferial lowest. @@ -69,10 +72,42 @@ func precedenceEF(c candidate) int { // falls on (unlike a saint's feast, which is only commemorated). Give it // the edge over the Sunday's tie-break bonus. p -= 2 + } else if c.Cel.Rank == RankClass1 && beatsClass1Sunday(c.Cel.Slug) { + // RG 91 entries 4 (Immaculate Conception, Assumption BVM) and 5 + // (Vigil & Octave day of the Nativity) both sit ABOVE entry 6 + // (Sundays of Advent/Lent/Passiontide, Low Sunday) -- unlike an + // ORDINARY I-class feast (entry 11, e.g. St Joseph, the Precious + // Blood), which yields to a I-class Sunday, these win the tie. + // Found while verifying defect 4 (Sunday ranks): before that fix, + // Advent/Lent Sundays were wrongly II class, so these feasts beat + // them outright on base class alone, accidentally right; once + // Sundays became I class the tie-break mattered, and without this + // branch the Sunday would wrongly win. Two live cases in the fixed + // calendar: the Immaculate Conception (8 December) on an Advent + // Sunday, and the Vigil of Christmas (24 December) on Advent IV -- + // the latter is not merely a wrong winner but a WORSE bug without + // this branch: transferIfImpededEF has no way to re-place a + // transfer that crosses the Dec31/Jan1 boundary (celebrationDate + // re-resolves a fixed date using the YEAR OF THE DAY BEING QUERIED, + // so a walk landing in January of the following year can never + // match the query that produced it), so the Vigil simply vanished + // for the year instead of landing on the wrong day. The Assumption + // (15 August) never falls in Advent or Lent, so its own share of + // this branch is citation-complete but not live. + p -= 2 } return p } +// beatsClass1Sunday reports whether slug is one of RG 91's entries 4 or 5 -- +// the Immaculate Conception, the Assumption, or the Vigil of Christmas -- +// all of which sit above entry 6's Sundays in the Table of Precedence. +func beatsClass1Sunday(slug string) bool { + return strings.Contains(slug, "immaculate-conception") || + strings.Contains(slug, "assumption-of-the-blessed-virgin-mary") || + slug == "vigil-of-christmas" +} + // pickEF returns the observed EF celebration and the commemorations, ordered // deterministically by precedence then slug. func pickEF(cands []candidate) (candidate, []candidate) { diff --git a/internal/calendar/precedence_ef_repro_test.go b/internal/calendar/precedence_ef_repro_test.go index 77919d9..09cadbd 100644 --- a/internal/calendar/precedence_ef_repro_test.go +++ b/internal/calendar/precedence_ef_repro_test.go @@ -81,3 +81,42 @@ func TestMatthewBeatsSeptemberEmberWednesday(t *testing.T) { } } } + +// TestImmaculateConceptionBeatsAdventSunday: found, not named among the +// seven, while verifying defect 4 (Sunday ranks) against the real sanctoral +// data. RG 91 entry 4 (Immaculate Conception, Assumption BVM) sits ABOVE +// entry 6 (Sundays of Advent/Lent/Passiontide) -- unlike an ORDINARY +// I-class feast (entry 11, e.g. St Joseph), the Immaculate Conception +// (8 December) is not impeded by the Advent Sunday it falls on at all. Before +// defect 4's own fix, Advent Sundays were wrongly II class, so this was +// accidentally right (I class beats II class outright); once Sundays became +// I class the tie mattered for the first time. +func TestImmaculateConceptionBeatsAdventSunday(t *testing.T) { + for _, year := range []string{"2013", "2019", "2024"} { + day := efCompute(year + "-12-08") + if day.Observed.Slug != "immaculate-conception-of-the-blessed-virgin-mary" { + t.Errorf("%s-12-08 observed = %q want immaculate-conception-of-the-blessed-virgin-mary (RG 91 entry 4 beats entry 6)", year, day.Observed.Slug) + } + } +} + +// TestVigilOfChristmasSurvivesAdventSunday: found, not named among the +// seven, while verifying defect 4 against the real sanctoral data -- and +// worse than a wrong winner. RG 91 entry 5 (Vigil & Octave day of the +// Nativity) also sits above entry 6, so the Vigil of Christmas (24 December) +// is not impeded by falling on Advent IV either. Without this, defect 4 +// alone would have made the Advent Sunday win a genuine tie, sending the +// Vigil into transferIfImpededEF's forward walk -- which has no way to +// re-place a transfer that crosses the Dec31/Jan1 boundary (celebrationDate +// re-resolves a fixed date using the year of whatever day is being queried, +// so a walk landing in the following January can never match the query that +// produced it). The Vigil did not move to the wrong day; it vanished for the +// whole year. +func TestVigilOfChristmasSurvivesAdventSunday(t *testing.T) { + for _, year := range []string{"2006", "2017", "2023", "2028"} { + day := efCompute(year + "-12-24") + if day.Observed.Slug != "vigil-of-christmas" { + t.Errorf("%s-12-24 observed = %q want vigil-of-christmas (RG 91 entry 5 beats entry 6; must not vanish)", year, day.Observed.Slug) + } + } +} |
