diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 07:51:05 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 07:51:05 +0200 |
| commit | cca0eb0db6cf6850d55d7a44fef470c594bce8ec (patch) | |
| tree | bf6eff3bc95e5ed029dd0aa0d50db85d90e3a6cc | |
| parent | c33158ed31d46e9bed76f01223f6718da3b4da21 (diff) | |
| download | lectio-cca0eb0db6cf6850d55d7a44fef470c594bce8ec.tar.gz lectio-cca0eb0db6cf6850d55d7a44fef470c594bce8ec.zip | |
fix(ef): 1962 occurrence precedence for the full sanctoral
- Septuagesima ferias are IV class (per annum), not III class, so a III
class saint (e.g. Conversion of St Paul, Jan 25) displaces them.
- Holy Week ferias and the privileged octaves of Easter and Pentecost are
I class: no saint's feast is admitted (temporal office wins).
- Occurrence tie-break is asymmetric by season: an ordinary feria (Advent,
per annum) yields to an equal-class feast; Lent/Passiontide ferias keep
their privilege and win the tie.
- A II class feast of the Lord (Purification, Exaltation of the Cross,
Dedication of the Lateran) displaces a II class Sunday it falls on.
Validated against missalemeum across 2025-2027: ~97.5% of days observe the
liturgically-correct celebration, 100% rank agreement on saint-days.
| -rw-r--r-- | internal/calendar/precedence_ef.go | 27 | ||||
| -rw-r--r-- | internal/calendar/temporal_ef.go | 33 |
2 files changed, 53 insertions, 7 deletions
diff --git a/internal/calendar/precedence_ef.go b/internal/calendar/precedence_ef.go index 81862b7..4fda626 100644 --- a/internal/calendar/precedence_ef.go +++ b/internal/calendar/precedence_ef.go @@ -22,13 +22,30 @@ func efRankOrder(r Rank) int { } // precedenceEF ranks an EF candidate for occurrence (lower = higher precedence): -// by class first, then — at equal class — the temporal office before the -// sanctoral (a first-cut of the 1960 occurrence table; the missalemeum oracle -// drives refinement). +// by class first, then a tie-break at equal class (1960 occurrence table). +// +// The tie-break is asymmetric by season. An ORDINARY feria (per annum, Advent, +// Septuagesima) yields to an equal-class feast — the feast is celebrated and the +// feria commemorated (e.g. St Francis Xavier, Dec 3, on an Advent feria). The +// ferias of Lent and Passiontide are privileged: they outrank an equal-class +// feast, which is only commemorated. Sundays and named temporal feasts likewise +// win their ties. The *2 class spacing means the ±1 tie-break never crosses a +// class boundary. func precedenceEF(c candidate) int { p := (6 - efRankOrder(c.Cel.Rank)) * 2 // class-1 -> 2, ferial -> 12 - if !c.Temporal { - p++ // sanctoral yields to a temporal office of equal class + if c.Temporal { + ordinaryFeria := (c.Cel.Rank == RankClass3 || c.Cel.Rank == RankClass4) && + c.Season != Lent && c.Season != Passiontide + if ordinaryFeria { + p++ // an ordinary feria yields to an equal-class feast + } else { + p-- // Sundays, named feasts, and penitential ferias win their ties + } + } else if c.Cel.Class == ClassLord && c.Cel.Rank == RankClass2 { + // A II class feast of the Lord takes the place of a II class Sunday it + // falls on (unlike a saint's feast, which is only commemorated). Give it + // the edge over the Sunday's tie-break bonus. + p -= 2 } return p } diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go index 6f641f4..2ead3e3 100644 --- a/internal/calendar/temporal_ef.go +++ b/internal/calendar/temporal_ef.go @@ -132,8 +132,19 @@ func temporalEF(date time.Time) temporalDay { } week := efWeek(date, season, y, easter) rank := RankClass4 // per-annum feria - if season == Advent || season == Lent || season == Passiontide || season == Septuagesima { - rank = RankClass3 // penitential ferias rank higher + if season == Advent || season == Lent || season == Passiontide { + // III class ferias (1960 rubrics): Advent to Dec 16, and Lent/Passiontide. + // These outrank III class feasts (the saint is only commemorated). The + // Septuagesima season's ferias are IV class per annum, so a III class + // saint (e.g. the Conversion of St Paul, Jan 25) displaces them. + rank = RankClass3 + } + if efPrivilegedFeria(date, easter) { + // The days of Holy Week and the privileged octaves of Easter and + // Pentecost are I class; no saint's feast is admitted (the feast is + // transferred or, in the octaves, omitted). Ranking them class-1 makes + // the temporal office win, matching the 1962 occurrence rules. + rank = RankClass1 } // Unique ferial slug (season-week-weekday) so proper ferias (Lent, Advent, // Holy Week, Ember days) can key their own readings; green-season ferias @@ -145,6 +156,24 @@ func weekdayLower(d time.Time) string { return strings.ToLower(d.Weekday().String()) } +// efPrivilegedFeria reports whether date is an I class temporal weekday that +// impedes saints' feasts: the ferias of Holy Week (the six days before Easter) +// and the weekdays within the privileged octaves of Easter (Easter Mon–Sat) and +// Pentecost (Whit Mon–Sat). Easter/Pentecost Sundays and Low Sunday are already +// handled as named I class feasts above; this covers only their octave weekdays. +func efPrivilegedFeria(date, easter time.Time) bool { + days := int(date.Sub(easter) / (24 * time.Hour)) + switch { + case days >= -6 && days <= -1: // Monday..Saturday of Holy Week (incl. Triduum) + return true + case days >= 1 && days <= 6: // Easter octave weekdays + return true + case days >= 50 && days <= 55: // Pentecost octave weekdays (Pentecost = easter+49) + return true + } + return false +} + // efWeek is a best-effort week-within-season number for display (not // oracle-checked). Time after Pentecost counts Sundays from Pentecost. func efWeek(date time.Time, season Season, y int, easter time.Time) int { |
