aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 12:19:17 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 12:19:17 +0200
commit4c08e94dbcce8dc7757afba3001abe509bd2b6a8 (patch)
treee6d7025c3f96b5a50a0a3565d110fc5013afe16a
parent9839ea9f2257d843c08497f07e9a5d23dc47e0fb (diff)
downloadlectio-4c08e94dbcce8dc7757afba3001abe509bd2b6a8.tar.gz
lectio-4c08e94dbcce8dc7757afba3001abe509bd2b6a8.zip
feat(calendar): Universal Norms precedence bands + pick
-rw-r--r--internal/calendar/precedence.go71
-rw-r--r--internal/calendar/precedence_test.go36
2 files changed, 107 insertions, 0 deletions
diff --git a/internal/calendar/precedence.go b/internal/calendar/precedence.go
new file mode 100644
index 0000000..b013b77
--- /dev/null
+++ b/internal/calendar/precedence.go
@@ -0,0 +1,71 @@
+package calendar
+
+import "sort"
+
+// candidate is one celebration competing for a given day: the temporal day, or
+// a sanctoral celebration landing on it. The flags come from the temporal
+// builder (Task 4) or are empty for sanctoral entries.
+type candidate struct {
+ Cel Celebration
+ Temporal bool
+ Privileged bool // band 1-2 temporal day
+ PrivFeria bool // band 9 privileged feria
+ Sunday bool // Ordinary/Christmas Sunday (band 6)
+ Season Season
+}
+
+// precedence maps a candidate to its band in the Table of Liturgical Days
+// (Universal Norms, 1969) — lower = higher precedence. "Proper" celebrations
+// (a non-universal, non-temporal layer) sit one band below their General
+// Calendar counterpart.
+func precedence(c candidate) int {
+ proper := c.Cel.Layer != "" && c.Cel.Layer != "universal" && c.Cel.Layer != "temporal"
+ if c.Temporal {
+ switch {
+ case c.Season == Triduum:
+ return 1
+ case c.Privileged:
+ return 2
+ case c.Cel.Rank == RankSolemnity: // Trinity, Corpus Christi, Sacred Heart, Christ the King
+ return 3
+ case c.Cel.Rank == RankFeast: // feasts of the Lord (Baptism, Holy Family)
+ return 5
+ case c.Sunday:
+ return 6
+ case c.PrivFeria:
+ return 9
+ default:
+ return 13
+ }
+ }
+ switch c.Cel.Rank {
+ case RankSolemnity:
+ if proper {
+ return 4
+ }
+ return 3
+ case RankFeast:
+ if proper {
+ return 8
+ }
+ return 7
+ case RankMemorial:
+ if proper {
+ return 11
+ }
+ return 10
+ case RankOptional:
+ return 12
+ default:
+ return 13
+ }
+}
+
+// pick returns the highest-precedence candidate as observed and the rest as
+// others (commemorations / optional memorials), stably ordered by precedence.
+func pick(cands []candidate) (candidate, []candidate) {
+ sorted := make([]candidate, len(cands))
+ copy(sorted, cands)
+ sort.SliceStable(sorted, func(i, j int) bool { return precedence(sorted[i]) < precedence(sorted[j]) })
+ return sorted[0], sorted[1:]
+}
diff --git a/internal/calendar/precedence_test.go b/internal/calendar/precedence_test.go
new file mode 100644
index 0000000..6d9dc15
--- /dev/null
+++ b/internal/calendar/precedence_test.go
@@ -0,0 +1,36 @@
+package calendar
+
+import "testing"
+
+func tempCand(rank Rank, class Class, priv bool, s Season) candidate {
+ return candidate{Cel: Celebration{Rank: rank, Class: class}, Temporal: true, Privileged: priv, Season: s}
+}
+func saintCand(rank Rank, class Class) candidate {
+ return candidate{Cel: Celebration{Rank: rank, Class: class}, Temporal: false, Season: Ordinary}
+}
+
+func TestPrecedenceBands(t *testing.T) {
+ sundayLent := tempCand(RankSolemnity, ClassNone, true, Lent)
+ genSolemnity := saintCand(RankSolemnity, ClassSaint)
+ genFeast := saintCand(RankFeast, ClassSaint)
+ genMemorial := saintCand(RankMemorial, ClassSaint)
+ // A Sunday of Lent is band 2; a general solemnity is band 3 → the Sunday wins.
+ if !(precedence(sundayLent) < precedence(genSolemnity)) {
+ t.Error("Sunday of Lent (band 2) must outrank a general solemnity (band 3)")
+ }
+ if !(precedence(genSolemnity) < precedence(genFeast) && precedence(genFeast) < precedence(genMemorial)) {
+ t.Error("solemnity > feast > memorial ordering broken")
+ }
+}
+
+func TestPick(t *testing.T) {
+ sunday := tempCand(RankSolemnity, ClassNone, true, Ordinary)
+ memorial := saintCand(RankMemorial, ClassSaint)
+ obs, others := pick([]candidate{memorial, sunday})
+ if obs.Cel.Rank != RankSolemnity || !obs.Temporal {
+ t.Errorf("Sunday must win, got %+v", obs.Cel)
+ }
+ if len(others) != 1 || others[0].Cel.Rank != RankMemorial {
+ t.Errorf("memorial should be a commemoration, got %+v", others)
+ }
+}