diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-18 16:16:07 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-18 16:16:07 +0200 |
| commit | 7657c4126339606c7c70d4ccd336ed14e78010bc (patch) | |
| tree | fe39db3f5d630049d76b4b694f57a67ed1a74732 /internal/calendar | |
| parent | b7b28ab0ba96717b7222bb892806d7dc828a966f (diff) | |
| download | lectio-7657c4126339606c7c70d4ccd336ed14e78010bc.tar.gz lectio-7657c4126339606c7c70d4ccd336ed14e78010bc.zip | |
fix(ef): weeks after Epiphany count from the first Sunday, not from Epiphany
efWeek anchored the Time-after-Epiphany week number at 6 January, which put
every week of the season 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 error persisted from Epiphany until Septuagesima cut the
season short, so it was wrong for most of January and February every year.
The Sunday is the anchor because it carries the Mass the week is named for,
"Dominica I post Epiphaniam", and its ferias follow it. Epiphany itself is a
feast inside Christmas Time (RG 72), not the head of a numbered week -- which
is the same distinction the Christmas Time boundary fix just made, showing up
again one function along.
Week numbers now match colitur day for day through the season.
The test pins the ordinary years AND the edge that a naive fix gets wrong:
when Epiphany itself falls on a Sunday, "the first Sunday AFTER Epiphany" is
the following week, so the helper starts its search on 7 January rather than
the 6th. It finds such a year in 2005..2050 rather than hardcoding one.
Mutation-tested: restoring the 6 January anchor reddens it.
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) + } +} |
