diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 13:59:02 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 13:59:02 +0200 |
| commit | ba5b6b54819b5dbea10213138d9f210373b2c629 (patch) | |
| tree | f5965c73eaf1bea444f6ddc8a16173c914f7b1d1 /internal | |
| parent | 3bda15bfe7786ae97fac1cbf779b713cf4238846 (diff) | |
| download | lectio-ba5b6b54819b5dbea10213138d9f210373b2c629.tar.gz lectio-ba5b6b54819b5dbea10213138d9f210373b2c629.zip | |
feat(calendar): precedenceEF (1960 occurrence by class + temporal-before-sanctoral)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/calendar/precedence_ef.go | 49 | ||||
| -rw-r--r-- | internal/calendar/precedence_ef_test.go | 29 |
2 files changed, 78 insertions, 0 deletions
diff --git a/internal/calendar/precedence_ef.go b/internal/calendar/precedence_ef.go new file mode 100644 index 0000000..81862b7 --- /dev/null +++ b/internal/calendar/precedence_ef.go @@ -0,0 +1,49 @@ +package calendar + +import "sort" + +// efRankOrder orders EF (1960) ranks: class-1 highest, then class-2..4, +// commemoration, ferial lowest. +func efRankOrder(r Rank) int { + switch r { + case RankClass1: + return 5 + case RankClass2: + return 4 + case RankClass3: + return 3 + case RankClass4: + return 2 + case RankCommemoration: + return 1 + default: // ferial / unset + return 0 + } +} + +// 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). +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 + } + return p +} + +// pickEF returns the observed EF celebration and the commemorations, ordered +// deterministically by precedence then slug. +func pickEF(cands []candidate) (candidate, []candidate) { + sorted := make([]candidate, len(cands)) + copy(sorted, cands) + sort.SliceStable(sorted, func(i, j int) bool { + pi, pj := precedenceEF(sorted[i]), precedenceEF(sorted[j]) + if pi != pj { + return pi < pj + } + return sorted[i].Cel.Slug < sorted[j].Cel.Slug + }) + return sorted[0], sorted[1:] +} diff --git a/internal/calendar/precedence_ef_test.go b/internal/calendar/precedence_ef_test.go new file mode 100644 index 0000000..818b452 --- /dev/null +++ b/internal/calendar/precedence_ef_test.go @@ -0,0 +1,29 @@ +package calendar + +import "testing" + +func efCand(rank Rank, temporal bool) candidate { + return candidate{Cel: Celebration{Rank: rank}, Temporal: temporal} +} + +func TestPrecedenceEF(t *testing.T) { + c1 := efCand(RankClass1, false) + c3 := efCand(RankClass3, false) + c4 := efCand(RankClass4, false) + comm := efCand(RankCommemoration, false) + if !(precedenceEF(c1) < precedenceEF(c3) && precedenceEF(c3) < precedenceEF(c4) && precedenceEF(c4) < precedenceEF(comm)) { + t.Error("EF class ordering broken (class-1 > class-3 > class-4 > commemoration)") + } + // at equal class, the temporal office wins. + temp := efCand(RankClass2, true) + saint := efCand(RankClass2, false) + obs, _ := pickEF([]candidate{saint, temp}) + if !obs.Temporal { + t.Error("equal-class: temporal office should be observed") + } + // a I-class feast beats a II-class Sunday. + obs2, others := pickEF([]candidate{efCand(RankClass2, true), efCand(RankClass1, false)}) + if obs2.Cel.Rank != RankClass1 || len(others) != 1 { + t.Errorf("I-class feast should win over II-class Sunday, got %+v", obs2.Cel) + } +} |
