diff options
Diffstat (limited to 'internal/calendar')
| -rw-r--r-- | internal/calendar/oracle_ef_test.go | 37 | ||||
| -rw-r--r-- | internal/calendar/temporal_ef.go | 48 | ||||
| -rw-r--r-- | internal/calendar/temporal_ef_test.go | 57 |
3 files changed, 129 insertions, 13 deletions
diff --git a/internal/calendar/oracle_ef_test.go b/internal/calendar/oracle_ef_test.go index b74d935..b765a34 100644 --- a/internal/calendar/oracle_ef_test.go +++ b/internal/calendar/oracle_ef_test.go @@ -249,11 +249,18 @@ var efAllowList = []efAllow{ // defects, and not touched here: efSeason is a season-boundary // function with much wider reach than any of the seven, and changing // it is out of this task's scope. + // CORRECTED 2026-08-18: lectio now follows RG 72 and reads 6-13 January + // as Christmas Time, so this divergence has CHANGED SIDES. It is + // missalemeum that calls those days "time after Epiphany" now, and + // what this excuses is a SEASON difference rather than the colour one + // it was written for -- the colour follows correctly once the season + // is right, which is why the six colour rows this used to cover are + // simply gone. match: func(date string, day time.Time, o efOracleDay) bool { return day.Month() == time.January && day.Day() >= 6 && day.Day() <= 13 }, - field: "colour", - reason: "RG 72-73/RG 119(a): 6-13 Jan is Christmastide, not lectio's time-after-Epiphany reading (efSeason, not one of the seven defects; see colitur rules-register.md §3c item 1)", + field: "season", + reason: "RG 72: Christmas Time runs 'usque ad diem 13 ianuarii inclusive', so 6-13 Jan is the Epiphany section OF Christmas Time; missalemeum reads it as time after Epiphany (colitur C1)", }, { // RG 33: "Vigilia II aut III classis penitus omittitur, si occurrat in @@ -330,7 +337,7 @@ func TestOracleEF(t *testing.T) { } sort.Strings(dates) - var seasonMiss, rankMiss, colourMiss, rankAllowed, colourAllowed, skipped, total int + var seasonMiss, seasonAllowed, rankMiss, colourMiss, rankAllowed, colourAllowed, skipped, total int shownSeason, shownRank, shownColour := 0, 0, 0 for _, date := range dates { od := oracle[date] @@ -356,10 +363,25 @@ func TestOracleEF(t *testing.T) { if want == "" { skipped++ } else if got.Season != want { - seasonMiss++ - if shownSeason < 40 { - t.Errorf("%s: EF season got %q want %q (from %q)", date, got.Season, want, src) - shownSeason++ + // The season axis consults the allow-list, and an excused day is + // NOT counted in seasonMiss. Neither was true before: until the + // RG 72 boundary was fixed no cited divergence had ever been a + // SEASON one -- the 6-13 January window showed up as a colour + // difference, because the wrong season produced the wrong colour + // too. With the season right the colour follows, and what is left + // is a season difference with no way to say "this one is known". + // Counting an excused day in seasonMiss would be worse than + // useless: the threshold below is a t.Fatalf, so the allow-list + // would print a reason and fail the suite anyway. + if ok, why := efAllowed(date, day, od, "season"); ok { + seasonAllowed++ + t.Logf("[allow-listed] %s: EF season got %q want %q — %s", date, got.Season, want, why) + } else { + seasonMiss++ + if shownSeason < 40 { + t.Errorf("%s: EF season got %q want %q (from %q)", date, got.Season, want, src) + shownSeason++ + } } } @@ -419,6 +441,7 @@ func TestOracleEF(t *testing.T) { t.Logf("EF oracle: %d checked, %d skipped(unmapped), %d season mismatches, "+ "%d rank mismatches (%d allow-listed), %d colour mismatches (%d allow-listed)", total, skipped, seasonMiss, rankMiss, rankAllowed, colourMiss, colourAllowed) + t.Logf("EF oracle: %d season mismatches allow-listed", seasonAllowed) if seasonMiss > 0 { t.Fatalf("%d/%d EF season mismatches vs missalemeum — temporalEF is wrong", seasonMiss, total) } diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go index 3d6c72c..e8630b9 100644 --- a/internal/calendar/temporal_ef.go +++ b/internal/calendar/temporal_ef.go @@ -35,7 +35,13 @@ func efColour(s Season) Colour { } // efSeason returns the 1962 season for date, by chronological date boundaries. -func efSeason(date time.Time, y int, easter time.Time) Season { +// efSlugSeason is the season used to NAME slugs and NUMBER weeks. It is not +// always the liturgical season: 6-13 January is liturgically Christmas Time +// (RG 72, see efSeason) while its days are still named and numbered from +// Epiphany, which is what the lectionary keys and every downstream consumer +// expect. Splitting the two is the whole point -- before this, one function +// served both and the wrong boundary in one was the wrong boundary in both. +func efSlugSeason(date time.Time, y int, easter time.Time) Season { adventThis := adventStart(y) christmasThis := time.Date(y, 12, 25, 0, 0, 0, 0, time.UTC) jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC) @@ -86,10 +92,34 @@ func efSunday(season Season, slug string, col Colour, rank Rank, week int) tempo // temporalEF computes the 1962 temporal identity of date. Season is exact // (oracle-validated); ranks/weeks are best-effort for display. +// efSeason is the LITURGICAL season, which is what the colour follows. +// +// It differs from efSlugSeason on exactly one window. RG 72: "Tempus +// natalicium decurrit a I Vesperis Nativitatis Domini usque ad diem 13 +// ianuarii INCLUSIVE" -- Christmas Time runs to 13 January, so 6-13 January is +// the Epiphany SECTION OF Christmas Time, not the start of Time after +// Epiphany. RG 119(a) backs the same boundary from the colour side, white +// "usque ad expletum tempus Epiphaniae". Those eight days were coming out +// green because this distinction did not exist. +// +// The slug season is deliberately left alone there: the days keep their +// Epiphany names and week numbers, which is what the lectionary keys and the +// generated downstream tables are built on. colitur, which models the two +// axes separately from the start, does the same -- season christmastide, +// slug ef-time-after-epiphany-sunday-1. +func efSeason(date time.Time, y int, easter time.Time) Season { + if s := efSlugSeason(date, y, easter); s == TimeAfterEpiphany && + date.Month() == time.January && date.Day() <= 13 { + return Christmas + } + return efSlugSeason(date, y, easter) +} + func temporalEF(date time.Time) temporalDay { date = date.UTC().Truncate(24 * time.Hour) y := date.Year() easter := Easter(y) + slugSeason := efSlugSeason(date, y, easter) season := efSeason(date, y, easter) col := efColour(season) // The Octave of Pentecost (Whit Monday through the Ember Saturday, easter+50 @@ -130,7 +160,9 @@ func temporalEF(date time.Time) temporalDay { case date.Month() == time.January && date.Day() == 1: return efCel(Christmas, "ef-circumcision", White, RankClass1, 0) case date.Month() == time.January && date.Day() == 6: - return efCel(TimeAfterEpiphany, "ef-epiphany", White, RankClass1, 0) + // Christmas, not TimeAfterEpiphany: RG 72 puts Epiphany INSIDE Christmas + // Time, which runs to 13 January. The slug is unchanged. + return efCel(Christmas, "ef-epiphany", White, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, -46)): return efCel(Lent, "ef-ash-wednesday", Violet, RankClass1, 0) case sameDay(date, easter.AddDate(0, 0, -14)): @@ -161,7 +193,7 @@ func temporalEF(date time.Time) temporalDay { // Sundays vs ferias of the current season. if sun { - week := efWeek(date, season, y, easter) + week := efWeek(date, slugSeason, y, easter) rank := RankClass2 sundayColour := col if season == Advent || season == Lent { @@ -196,14 +228,18 @@ func temporalEF(date time.Time) temporalDay { return efSunday(TimeAfterPentecost, "ef-time-after-epiphany-sunday-"+strconv.Itoa(e), Green, rank, e) } } - return efSunday(season, "ef-"+string(season)+"-sunday-"+strconv.Itoa(week), sundayColour, rank, week) + // Season reported liturgically, slug named from the slug season -- the + // same split the resumed-Sunday branch just above already makes for + // its own reason ("Season stays time-after-pentecost; the Epiphany + // slug only routes the readings"). + return efSunday(season, "ef-"+string(slugSeason)+"-sunday-"+strconv.Itoa(week), sundayColour, rank, week) } // The days between Ash Wednesday and the 1st Sunday of Lent have their own // proper Masses (not part of a numbered Lenten week). if season == Lent && date.Before(easter.AddDate(0, 0, -42)) { return efCel(Lent, "ef-lent-after-ashes-"+weekdayLower(date), Violet, RankClass3, 0) } - week := efWeek(date, season, y, easter) + week := efWeek(date, slugSeason, y, easter) rank := RankClass4 // per-annum feria if season == Advent || season == Lent || season == Passiontide { // III class ferias (1960 rubrics): Advent to Dec 16, and Lent/Passiontide. @@ -240,7 +276,7 @@ func temporalEF(date time.Time) temporalDay { // Unique ferial slug (season-week-weekday) so proper ferias (Lent, Advent, // Holy Week, Ember days) can key their own readings; green-season ferias // simply have no lectionary entry and fall back to the Sunday. - return efCel(season, "ef-"+string(season)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week) + return efCel(season, "ef-"+string(slugSeason)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week) } // thirdSundayOfSeptember returns the third Sunday in September of year y. diff --git a/internal/calendar/temporal_ef_test.go b/internal/calendar/temporal_ef_test.go index db6e6aa..7dd7400 100644 --- a/internal/calendar/temporal_ef_test.go +++ b/internal/calendar/temporal_ef_test.go @@ -147,3 +147,60 @@ func TestTemporalEFEmberDayRanks(t *testing.T) { } } } + +// TestTemporalEFChristmastideBoundary pins RG 72's boundary and, just as +// importantly, the SEPARATION it required. +// +// RG 72: "Tempus natalicium decurrit a I Vesperis Nativitatis Domini usque ad +// diem 13 ianuarii INCLUSIVE." Christmas Time runs to 13 January, so 6-13 +// January is the Epiphany section OF Christmas Time, not the beginning of Time +// after Epiphany. RG 119(a) backs it from the colour side, white "usque ad +// expletum tempus Epiphaniae". +// +// lectio ended Christmas Time on 5 January, so those eight days came out +// green. The fix could not be the boundary constant alone: efSeason both named +// the slugs and reported the season, so moving it renamed every slug in the +// window and orphaned the ef-time-after-epiphany-sunday-1 lectionary key. The +// season and the slug are now separate axes -- efSeason and efSlugSeason -- +// and this test pins BOTH, because a future "simplification" that merges them +// again would pass any test that checked only one. +func TestTemporalEFChristmastideBoundary(t *testing.T) { + for _, c := range []struct { + date, season, slug string + }{ + // Epiphany itself is inside Christmas Time, not the start of what + // follows it. Its slug is unchanged. + {"2026-01-06", "christmas", "ef-epiphany"}, + // The window: season christmas, slugs and week numbers still Epiphany's. + {"2026-01-07", "christmas", "ef-time-after-epiphany-1-wednesday"}, + {"2026-01-11", "christmas", "ef-time-after-epiphany-sunday-1"}, + {"2026-01-12", "christmas", "ef-time-after-epiphany-1-monday"}, + // 13 January is INCLUSIVE -- the last day of Christmas Time. It carries + // the Commemoration of the Baptism, so only the season is asserted. + {"2026-01-13", "christmas", ""}, + // 14 January is the first day that is genuinely Time after Epiphany. + {"2026-01-14", "time-after-epiphany", ""}, + // The other end, unchanged: 5 January is Christmas Time by the old + // boundary too, so a regression that moved the boundary the wrong way + // would not show here -- which is why 14 January above is asserted. + {"2026-01-05", "christmas", ""}, + } { + got := temporalEF(d(c.date)) + if string(got.Season) != c.season { + t.Errorf("%s: season = %q, want %q (RG 72: Christmas Time runs to 13 January inclusive)", + c.date, got.Season, c.season) + } + if c.slug != "" && got.Cel.Slug != c.slug { + t.Errorf("%s: slug = %q, want %q -- the SLUG season must stay Epiphany's here; "+ + "if this fails with an ef-christmas-* slug, efSeason and efSlugSeason have been "+ + "merged again and the lectionary keys are orphaned", c.date, got.Cel.Slug, c.slug) + } + } + // The colour follows from the season, and is the reason any of this + // matters: eight days a year were green that should be white. + for _, day := range []string{"2026-01-07", "2026-01-09", "2026-01-12"} { + if got := temporalEF(d(day)).Cel.Colour; got != White { + t.Errorf("%s: colour = %s, want white (RG 119(a), following from the season)", day, got) + } + } +} |
