aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 17:02:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 17:02:47 +0200
commit9572942a0db4a89aaddd42cd33182d44a6e1fefb (patch)
tree48ec2621fed89b561e15d60da7ada37ad7ff60d5 /internal/calendar
parent74d557580e747cf309b8217d34ab6b9336939fca (diff)
downloadlectio-9572942a0db4a89aaddd42cd33182d44a6e1fefb.tar.gz
lectio-9572942a0db4a89aaddd42cd33182d44a6e1fefb.zip
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.
Diffstat (limited to 'internal/calendar')
-rw-r--r--internal/calendar/temporal_ef.go30
-rw-r--r--internal/calendar/temporal_ef_test.go46
2 files changed, 76 insertions, 0 deletions
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)
+ }
+ }
+}