aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/caldata/readings.go34
-rw-r--r--internal/caldata/tridentine-lectionary.ini11
-rw-r--r--internal/calendar/temporal_ef.go30
-rw-r--r--internal/calendar/temporal_ef_test.go46
4 files changed, 121 insertions, 0 deletions
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)
+ }
+ }
+}