aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/calendar/oracle_ef_test.go37
-rw-r--r--internal/calendar/temporal_ef.go48
-rw-r--r--internal/calendar/temporal_ef_test.go57
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)
+ }
+ }
+}