From 9572942a0db4a89aaddd42cd33182d44a6e1fefb Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 18 Aug 2026 17:02:47 +0200 Subject: feat(ef): the Most Holy Name of Jesus, and the ferias of 2-5 January RG 17(a): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii, secus die 2 ianuarii." The Most Holy Name is kept on the Sunday falling 2-5 January and, when no Sunday falls in that window, on 2 January instead. lectio built neither shape, so the day was an ordinary Sunday within the octave and read Gal 4:1-7. The FALLBACK matters more than it looks. The window is four days wide, so in about three years in seven no Sunday lands in it -- in those years there was no Holy Name office at all, a missing II-class feast rather than a misnamed one. The test pins both shapes, checks the fallback does NOT also fire in a year where the Sunday carries the feast, and asserts across 2005-2050 that every year gets it exactly once by one shape or the other. Separately, the ferial days of that window get their own Mass. The Missal's rubric at the feast says it directly: "Diebus ferialibus a 2 ad 5 ianuarii Missa dicitur ut die 1 ianuarii" -- the Circumcision's Mass, Titus 2:11-15 / Luke 2:21. They had been falling through to the Sunday within the octave. Ferial only, and the exclusions are the interesting part: the Sunday carries the Holy Name and its own Mass, and an unoccupied Saturday carries Our Lady's office and hers. 3 January 2026 is such a Saturday and correctly reads Titus 3:4-7, not 2:11-15 -- which works because the BVM check runs first. All four days of 2-5 January 2026 now match colitur. --- internal/caldata/readings.go | 34 ++++++++++++++++++++++ internal/caldata/tridentine-lectionary.ini | 11 +++++++ internal/calendar/temporal_ef.go | 30 +++++++++++++++++++ internal/calendar/temporal_ef_test.go | 46 ++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/internal/caldata/readings.go b/internal/caldata/readings.go index 44d1bc1..d082e26 100644 --- a/internal/caldata/readings.go +++ b/internal/caldata/readings.go @@ -63,6 +63,9 @@ func Readings(sel calendar.Selection, layers []calendar.Layer, date time.Time, d if r := efNativityOctaveReadings(day); len(r) > 0 { return r } + if r := efEpiphanytideOpeningReadings(day); len(r) > 0 { + return r + } if r := TemporalReadings("old", day.Observed.Slug); len(r) > 0 { return r } @@ -188,3 +191,34 @@ func efNativityOctaveReadings(day calendar.LiturgicalDay) []calendar.Reading { {Part: "gospel", Citation: "Luke 2:15-20"}, } } + +// efEpiphanytideOpeningReadings returns the Mass of a FERIAL day from 2 to 5 +// January. +// +// The Missal's rubric at the Most Holy Name of Jesus says so directly: +// "Diebus ferialibus a 2 ad 5 ianuarii Missa dicitur ut die 1 ianuarii" -- on +// the ferial days from the 2nd to the 5th of January, the Mass of 1 January +// (the Circumcision) is said. Titus 2:11-15 / Luke 2:21. +// +// Without this the days fell back through the walk-back to the Sunday within +// the Octave of the Nativity and read Gal 4:1-7 instead. +// +// FERIAL days only, which excludes two things on purpose. The Sunday in that +// window carries the Holy Name (RG 17(a)) and has its own Mass; and an +// otherwise-unoccupied Saturday carries Our Lady's office (RG 78) and hers, +// which is why the BVM check runs before this one and why 3 January 2026 -- +// a Saturday -- correctly reads Titus 3:4-7 rather than 2:11-15. +func efEpiphanytideOpeningReadings(day calendar.LiturgicalDay) []calendar.Reading { + if day.Date.Month() != time.January || day.Date.Day() < 2 || day.Date.Day() > 5 { + return nil + } + // The feria, not a feast or a Sunday that outranks it: the observed office + // must still be the temporal ferial one. + if !strings.HasPrefix(day.Observed.Slug, "ef-christmas-") || day.Weekday == time.Sunday { + return nil + } + return []calendar.Reading{ + {Part: "first", Citation: "Titus 2:11-15"}, + {Part: "gospel", Citation: "Luke 2:21"}, + } +} diff --git a/internal/caldata/tridentine-lectionary.ini b/internal/caldata/tridentine-lectionary.ini index ff39604..9190941 100644 --- a/internal/caldata/tridentine-lectionary.ini +++ b/internal/caldata/tridentine-lectionary.ini @@ -601,3 +601,14 @@ gospel = John 18:1-40; 19:1-42 [ef-passiontide-2-saturday] first = Col 3:1-4 gospel = Matt 28:1-7 + +; The Most Holy Name of Jesus (RG 17(a)), both shapes -- the Sunday falling +; 2-5 January and, in the years when no Sunday does, 2 January itself. +; Citations verified by colitur against both photographic scans. +[ef-holy-name-sunday] +first = Acts 4:8-12 +gospel = Luke 2:21 + +[ef-holy-name] +first = Acts 4:8-12 +gospel = Luke 2:21 diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go index 6828f35..0cd4b41 100644 --- a/internal/calendar/temporal_ef.go +++ b/internal/calendar/temporal_ef.go @@ -159,6 +159,23 @@ func temporalEF(date time.Time) temporalDay { return efCel(Christmas, "ef-nativity", White, RankClass1, 0) case date.Month() == time.January && date.Day() == 1: return efCel(Christmas, "ef-circumcision", White, RankClass1, 0) + // RG 17(a): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit + // a die 2 ad 5 ianuarii, secus die 2 ianuarii" -- the Most Holy Name of + // Jesus is kept on the Sunday falling 2-5 January, and when no Sunday falls + // in that window, on 2 January instead. + // + // The fallback is not decoration: the window is four days wide, so in + // roughly three years in seven no Sunday lands in it and 2 January carries + // the feast. Without the fallback lectio simply had no Holy Name office at + // all in those years. + // + // Placed before the Epiphany case below and after the Circumcision above, + // so 1 January keeps its own feast and 6 January keeps Epiphany. + case date.Month() == time.January && date.Day() >= 2 && date.Day() <= 5 && + date.Weekday() == time.Sunday: + return efCel(Christmas, "ef-holy-name-sunday", White, RankClass2, 0) + case date.Month() == time.January && date.Day() == 2 && !sundayFallsJan2to5(y): + return efCel(Christmas, "ef-holy-name", White, RankClass2, 0) case date.Month() == time.January && date.Day() == 6: // Christmas, not TimeAfterEpiphany: RG 72 puts Epiphany INSIDE Christmas // Time, which runs to 13 January. The slug is unchanged. @@ -330,6 +347,19 @@ func temporalEF(date time.Time) temporalDay { return efCel(season, "ef-"+string(slugSeason)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week) } +// sundayFallsJan2to5 reports whether any Sunday falls in RG 17(a)'s own window, +// 2 to 5 January inclusive. When one does the Holy Name is kept on it; when +// none does the feast falls back to 2 January, which is what "secus die 2 +// ianuarii" provides for. +func sundayFallsJan2to5(y int) bool { + for day := 2; day <= 5; day++ { + if time.Date(y, time.January, day, 0, 0, 0, 0, time.UTC).Weekday() == time.Sunday { + return true + } + } + return false +} + // firstSundayAfterEpiphany returns the first Sunday strictly after 6 January // of year y -- the anchor the weeks after Epiphany are numbered from. When // Epiphany itself falls on a Sunday the next one is meant, which is why the diff --git a/internal/calendar/temporal_ef_test.go b/internal/calendar/temporal_ef_test.go index d930cc7..c1571b0 100644 --- a/internal/calendar/temporal_ef_test.go +++ b/internal/calendar/temporal_ef_test.go @@ -360,3 +360,49 @@ func TestTemporalEFPassiontideWeeks(t *testing.T) { } } } + +// TestTemporalEFHolyName pins RG 17(a): "festum Ss.mi Nominis Iesu, +// celebrandum dominica quae occurrit a die 2 ad 5 ianuarii, secus die 2 +// ianuarii" -- the Most Holy Name of Jesus is kept on the Sunday falling 2-5 +// January, and when no Sunday falls in that window, on 2 January instead. +// +// The FALLBACK is the half worth testing hardest. The window is four days +// wide, so in about three years in seven no Sunday lands in it; without the +// fallback there is simply no Holy Name office in those years, which is a +// missing II-class feast rather than a misnamed one. +func TestTemporalEFHolyName(t *testing.T) { + // Years where a Sunday does fall in the window, at each end of it. + for _, c := range []struct{ date, slug string }{ + {"2026-01-04", "ef-holy-name-sunday"}, // Sunday the 4th + {"2027-01-03", "ef-holy-name-sunday"}, // Sunday the 3rd + {"2028-01-02", "ef-holy-name-sunday"}, // Sunday the 2nd -- the window's first day + } { + if got := temporalEF(d(c.date)).Cel.Slug; got != c.slug { + t.Errorf("%s: slug = %q, want %q", c.date, got, c.slug) + } + } + // 2029: no Sunday falls 2-5 January, so 2 January carries the feast. + if got := temporalEF(d("2029-01-02")).Cel.Slug; got != "ef-holy-name" { + t.Errorf("2029-01-02: slug = %q, want ef-holy-name -- no Sunday falls 2-5 January "+ + "that year, so RG 17(a)'s \"secus die 2 ianuarii\" applies", got) + } + // And in a year where a Sunday DOES fall in the window, 2 January must NOT + // also carry it -- the fallback is an alternative, not an addition. + if got := temporalEF(d("2026-01-02")).Cel.Slug; got == "ef-holy-name" { + t.Errorf("2026-01-02: slug = %q, but the Sunday the 4th carries the feast that "+ + "year -- the fallback must not fire as well", got) + } + // Sanity on the helper itself, across the whole fixture range: every year + // gets the feast exactly once, by one shape or the other. + for y := 2005; y <= 2050; y++ { + n := 0 + for day := 2; day <= 5; day++ { + if s := temporalEF(time.Date(y, time.January, day, 0, 0, 0, 0, time.UTC)).Cel.Slug; s == "ef-holy-name" || s == "ef-holy-name-sunday" { + n++ + } + } + if n != 1 { + t.Errorf("%d: the Holy Name appears %d times in 2-5 January, want exactly 1", y, n) + } + } +} -- cgit v1.3