diff options
Diffstat (limited to 'internal/calendar')
| -rw-r--r-- | internal/calendar/temporal_ef.go | 25 | ||||
| -rw-r--r-- | internal/calendar/temporal_ef_test.go | 54 |
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) + } +} |
