diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 08:49:57 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 08:49:57 +0200 |
| commit | 70b805018f0ae0d8424c3c7de8f1169476385a35 (patch) | |
| tree | 82de4bf52e588ab0c1f31ce0fc68d4b97b3d0de6 /internal/calendar/calendar.go | |
| parent | d3b7f41c7a823bda6b7bafae3a156348c4ce687f (diff) | |
| download | lectio-70b805018f0ae0d8424c3c7de8f1169476385a35.tar.gz lectio-70b805018f0ae0d8424c3c7de8f1169476385a35.zip | |
feat: complete OF+EF calendar coverage (movable feasts, vigils, ember days, transfers)
Ordinary Form:
- Add the movable Marian memorials that have no fixed date: Mary, Mother of the
Church (Monday after Pentecost) and the Immaculate Heart (Saturday after the
Sacred Heart), computed in computeOF.
- General Norms 60: two coinciding obligatory memorials both become optional and
the ferial is observed -- except Mary, Mother of the Church, which keeps
precedence per its 2018 decree.
Extraordinary Form:
- Capture fixed-date vigils in the sanctoral harvest (Christmas, the Nativity of
St John the Baptist, Sts Peter & Paul, St Lawrence, the Assumption, All Saints,
the Immaculate Conception) -- previously filtered out.
- temporalEF: the movable vigils of the Ascension (I class eve) and Pentecost
(Whitsun Eve), and the September/Advent Ember days (II class violet ferias).
- transferIfImpededEF: the Annunciation transfers past Holy Week/the Easter
octave to the Monday after Low Sunday; All Souls moves to Nov 3 when Nov 2 is a
Sunday.
- genlect harvests proper Masses for the new Ember/vigil days.
Adds TestEFCoverage + TestOFMovableMemorials. Bumps to 0.39.0.
Verified per-day: OF 0 real errors vs calapi (2026-2028, 100% correct-observed);
EF 0 real errors vs missalemeum (2025-2027), residual = deep-tail vigil-occurrence
and St Joseph Passiontide-transfer edges (~1-2 days/yr, documented).
Diffstat (limited to 'internal/calendar/calendar.go')
| -rw-r--r-- | internal/calendar/calendar.go | 62 |
1 files changed, 61 insertions, 1 deletions
diff --git a/internal/calendar/calendar.go b/internal/calendar/calendar.go index e0f5217..99a24b3 100644 --- a/internal/calendar/calendar.go +++ b/internal/calendar/calendar.go @@ -29,7 +29,8 @@ func computeEF(date time.Time, sel Selection, layers []Layer) LiturgicalDay { if !ok { continue } - if sameDay(when, date) { + effective := transferIfImpededEF(cel, when) + if sameDay(effective, date) { cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season}) } } @@ -75,6 +76,13 @@ func computeOF(date time.Time, sel Selection, layers []Layer) LiturgicalDay { cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season}) } } + // Movable Marian memorials that have no fixed date (so they cannot live in the + // sanctoral file): Mary, Mother of the Church, and the Immaculate Heart. + for _, mm := range movableMemorialsOF(date.Year()) { + if sameDay(mm.when, date) { + cands = append(cands, candidate{Cel: mm.cel, Temporal: false, Season: td.Season}) + } + } obs, others := pick(cands) day := LiturgicalDay{ @@ -104,6 +112,31 @@ func celebrationDate(cel Celebration, year int, sel Selection) (time.Time, bool) return resolveDate(cel.Date, year, Easter(year)) } +// movableCel is a celebration whose date is computed (relative to Easter), not +// read from a fixed-date layer. +type movableCel struct { + when time.Time + cel Celebration +} + +// movableMemorialsOF returns the Ordinary Form's two movable Marian memorials, +// which have no fixed calendar date: Mary, Mother of the Church (the Monday after +// Pentecost) and the Immaculate Heart of Mary (the Saturday after the Sacred +// Heart, i.e. easter+69). +func movableMemorialsOF(y int) []movableCel { + e := Easter(y) + mk := func(off int, slug, en, pl string) movableCel { + return movableCel{when: e.AddDate(0, 0, off), cel: Celebration{ + Slug: slug, Rank: RankMemorial, Class: ClassBVM, Colour: White, Layer: "universal", + Name: map[string]string{"en": en, "pl": pl}, + }} + } + return []movableCel{ + mk(50, "mary-mother-of-the-church", "Mary, Mother of the Church", "Najświętszej Maryi Panny, Matki Kościoła"), + mk(69, "the-immaculate-heart-of-the-blessed-virgin-mary", "The Immaculate Heart of the Blessed Virgin Mary", "Niepokalanego Serca Najświętszej Maryi Panny"), + } +} + // transferIfImpeded moves a solemnity off a band-1/2 temporal day to the next // free day. Non-solemnities are never transferred (they yield instead). func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time { @@ -126,6 +159,33 @@ func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time return day } +// transferIfImpededEF moves an EF sanctoral feast off a date it cannot be kept +// on. Two rules cover the 1962 cases: All Souls, when Nov 2 is a Sunday, is +// transferred to the Monday (Nov 3); and a I class feast impeded by a I class +// temporal day (Holy Week, the Easter octave) is pushed forward to the next free +// day — for the Annunciation in Holy Week this lands on the Monday after Low +// Sunday, past the whole privileged octave. Lower-rank feasts are only +// commemorated, never transferred. +func transferIfImpededEF(cel Celebration, when time.Time) time.Time { + if when.Month() == time.November && when.Day() == 2 && when.Weekday() == time.Sunday && + strings.Contains(cel.Slug, "all-souls") { + return when.AddDate(0, 0, 1) + } + if cel.Rank != RankClass1 { + return when + } + day := when + for i := 0; i < 30; i++ { + b := temporalEF(day) + if precedenceEF(candidate{Cel: b.Cel, Temporal: true, Season: b.Season}) <= 2 { + day = day.AddDate(0, 0, 1) // impeded by a I class temporal day; push forward + continue + } + return day + } + return day +} + // buildCelebration types a RawCelebration. func buildCelebration(slug string, rc RawCelebration) Celebration { c := Celebration{Slug: strings.TrimSpace(slug), Name: map[string]string{}, Layer: rc.Fields["layer"]} |
