summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 13:59:02 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 13:59:02 +0200
commitba5b6b54819b5dbea10213138d9f210373b2c629 (patch)
treef5965c73eaf1bea444f6ddc8a16173c914f7b1d1
parent3bda15bfe7786ae97fac1cbf779b713cf4238846 (diff)
downloadlectio-ba5b6b54819b5dbea10213138d9f210373b2c629.tar.gz
lectio-ba5b6b54819b5dbea10213138d9f210373b2c629.zip
feat(calendar): precedenceEF (1960 occurrence by class + temporal-before-sanctoral)
-rw-r--r--internal/calendar/precedence_ef.go49
-rw-r--r--internal/calendar/precedence_ef_test.go29
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)
+ }
+}