aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/calendar/temporal_ef.go25
-rw-r--r--internal/calendar/temporal_ef_test.go54
2 files changed, 76 insertions, 3 deletions
diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go
index e8630b9..26ad930 100644
--- a/internal/calendar/temporal_ef.go
+++ b/internal/calendar/temporal_ef.go
@@ -279,6 +279,18 @@ func temporalEF(date time.Time) temporalDay {
return efCel(season, "ef-"+string(slugSeason)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week)
}
+// 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
+// loop starts on 7 January rather than on the 6th.
+func firstSundayAfterEpiphany(y int) time.Time {
+ d := time.Date(y, time.January, 7, 0, 0, 0, 0, time.UTC)
+ for d.Weekday() != time.Sunday {
+ d = d.AddDate(0, 0, 1)
+ }
+ return d
+}
+
// thirdSundayOfSeptember returns the third Sunday in September of year y.
func thirdSundayOfSeptember(y int) time.Time {
d := time.Date(y, time.September, 1, 0, 0, 0, 0, time.UTC)
@@ -348,8 +360,17 @@ func efWeek(date time.Time, season Season, y int, easter time.Time) int {
case Lent:
return daysBetween(easter.AddDate(0, 0, -42), date)/7 + 1 // from 1st Sunday of Lent
case TimeAfterEpiphany:
- jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC)
- return daysBetween(jan6, date)/7 + 1
+ // Weeks after Epiphany are numbered from the FIRST SUNDAY after
+ // Epiphany, not from Epiphany itself. Counting from 6 January put every
+ // week one too high: 14 January 2026 came out as week 2 when the first
+ // Sunday after Epiphany was 11 January and the week running from it is
+ // the first.
+ //
+ // The Sunday is the anchor because it is the Sunday that carries the
+ // Mass the week is named for -- "Dominica I post Epiphaniam" and then
+ // its ferias. Epiphany itself is a feast inside Christmas Time (RG 72),
+ // not the head of a numbered week.
+ return daysBetween(firstSundayAfterEpiphany(y), date)/7 + 1
case Septuagesima:
return daysBetween(easter.AddDate(0, 0, -63), date)/7 + 1
case Easter_:
diff --git a/internal/calendar/temporal_ef_test.go b/internal/calendar/temporal_ef_test.go
index 7dd7400..a7fa8b0 100644
--- a/internal/calendar/temporal_ef_test.go
+++ b/internal/calendar/temporal_ef_test.go
@@ -1,6 +1,9 @@
package calendar
-import "testing"
+import (
+ "testing"
+ "time"
+)
func TestTemporalEFSeasons(t *testing.T) {
cases := []struct {
@@ -204,3 +207,52 @@ func TestTemporalEFChristmastideBoundary(t *testing.T) {
}
}
}
+
+// TestTemporalEFEpiphanyWeekAnchor pins where the weeks after Epiphany are
+// counted from. They are numbered from the FIRST SUNDAY after Epiphany, not
+// from Epiphany itself -- the Sunday carries the Mass the week is named for
+// ("Dominica I post Epiphaniam"), while Epiphany is a feast inside Christmas
+// Time (RG 72), not the head of a numbered week.
+//
+// Counting from 6 January put every week one too high, all year until
+// Septuagesima cut the season short.
+func TestTemporalEFEpiphanyWeekAnchor(t *testing.T) {
+ // 2026: Epiphany is a Tuesday, so the first Sunday after it is 11 January.
+ for _, c := range []struct {
+ date string
+ week int
+ }{
+ {"2026-01-14", 1}, // Wednesday of the week running from 11 January
+ {"2026-01-17", 1}, // still that week
+ {"2026-01-20", 2},
+ {"2026-01-27", 3},
+ } {
+ if got := temporalEF(d(c.date)).Week; got != c.week {
+ t.Errorf("%s: week = %d, want %d (weeks after Epiphany count from the "+
+ "first Sunday after it, not from 6 January)", c.date, got, c.week)
+ }
+ }
+ // 2028: Epiphany falls ON a Thursday and 9 January is the Sunday; but the
+ // year that matters for the off-by-one is one where Epiphany is itself a
+ // Sunday, since then "the first Sunday AFTER Epiphany" is the following
+ // week and a naive same-day anchor would be seven days out.
+ y := 0
+ for c := 2005; c <= 2050; c++ {
+ if time.Date(c, time.January, 6, 0, 0, 0, 0, time.UTC).Weekday() == time.Sunday {
+ y = c
+ break
+ }
+ }
+ if y == 0 {
+ t.Fatal("no year in 2005..2050 has Epiphany on a Sunday -- adjust this test")
+ }
+ anchor := firstSundayAfterEpiphany(y)
+ if anchor.Day() == 6 {
+ t.Errorf("%d: Epiphany is a Sunday and the anchor landed on it (%s); the "+
+ "first Sunday AFTER Epiphany is the next one", y, anchor.Format("2006-01-02"))
+ }
+ if got := temporalEF(anchor).Week; got != 1 {
+ t.Errorf("%d: the first Sunday after Epiphany (%s) should be week 1, got %d",
+ y, anchor.Format("2006-01-02"), got)
+ }
+}