aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:01:24 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:04:59 +0200
commit436ba75e27d2aa61b1c6035a22157b40f1a9834b (patch)
tree4ec34520d9627409e72298ab8bfe087527ad07b9
parent1d6be4ac281e62accfb905b137b3c6b494183ee4 (diff)
downloadcolitur-436ba75e27d2aa61b1c6035a22157b40f1a9834b.tar.gz
colitur-436ba75e27d2aa61b1c6035a22157b40f1a9834b.zip
rite(ef): RG 91 Table of Precedence
Precedence_ef.band transcribes RG 91's 28-entry Table of Precedence (rules-register.md §4) for the EF rite: given a day's context and a candidate celebration, returns the table's own entry number, 1-28 (I class 1-13, II class 14-21, III class 22-26, IV class 27-28); lower wins. Every branch carries its entry number and register citation in a comment, checked in the table's own numeric order. Two entries are transcribed as the register states them even though they invert the pattern the rest of the table follows: at III class, 23 (particular calendars) outranks 24 (universal), the reverse of how 11/12 and 14/16/19/20 rank a universal feast ahead of a proper one at I and II class. Sanctoral-origin, layer-decided entries (11-13, 14/16/19/20, 23/24) follow the brief's structural insight: a celebration whose layer is not the universal base is an overlay -- proper, or indult if its layer id also carries the indult prefix. Neither the universal-layer id nor the indult prefix is an RG citation; both are colitur's own data-modelling convention, exposed from the module so whichever task loads the real EF sanctoral overlays can align to them. Vigils (21, 26) are read off the temporal cycle's own -vigil slug suffix rather than gated on origin, since a II/III-class vigil can be either temporal-origin (Ascension, already produced by temporal_ef) or sanctoral-origin (a saint's vigil, no task has loaded yet); Ember days (part of entry 18) are read off temporal_ef's own ember slug prefixes rather than re-derived, since the September anchor is independently flagged there as one of the more contested dates in the calendar. A candidate shape the table has no row for (e.g. a Class1 vigil that is not Nativity or Pentecost, or a Class4 candidate marked as a vigil -- RG 91 has no IV-class vigil either) returns a dedicated unclassified sentinel (max_int) rather than being folded into a same-rank entry it does not belong to. test_precedence_ef.ml is table-driven: one Alcotest.test_case per RG 91 entry (55 rows total, several entries covered by more than one named day so a single missed offset cannot hide behind a passing sibling), each date computed from Computus.gregorian_easter rather than hand-typed, so an arithmetic slip cannot pass by accident.
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml173
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli35
-rw-r--r--test/test_colitur.ml2
-rw-r--r--test/test_precedence_ef.ml202
4 files changed, 411 insertions, 1 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
new file mode 100644
index 0000000..50da4a7
--- /dev/null
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -0,0 +1,173 @@
+(* RG 91's Table of Precedence (docs/research/rules-register.md §4). Each
+ branch below is one of the table's 28 entries, checked in the table's own
+ numeric order -- lower wins, and because occasional entries are true
+ exceptions to a later, broader one (RG 91 entry 18's Ember days are an
+ exception carved out of entry 22's Lent ferias; entry 21/26's vigils are
+ an exception carved out of the generic Class2/Class3 sanctoral-feast
+ entries that would otherwise also match), checking in table order and
+ returning on the first match is what makes the exception actually win
+ without a separate exclusion for every later entry it pre-empts.
+
+ Two kinds of evidence decide an entry:
+ - The temporal cycle's own office (Nativity, a Sunday, a feria, a vigil of
+ the Lord) is identified structurally, from the context's date/season/
+ weekday and the day's Easter offset -- never from its slug, which is
+ just a label. [origin = Temporal] gates every such entry so a sanctoral
+ candidate that happens to share a date (Immaculate Conception can never
+ coincide with the movable cycle, but nothing stops a future rite bug
+ from producing one) cannot be mistaken for the office itself.
+ - A sanctoral feast's entry (11-13 I class, 14/16/19/20 II class, 23/24
+ III class) is decided by its [rank] plus, per the brief's structural
+ insight, its {!Celebration.t}.layer: a celebration whose layer is not
+ the universal base is an overlay, hence "proper" or "indult" rather
+ than the universal entry (see precedence_ef.mli). [origin = Sanctoral]
+ gates these for the same reason: temporal-origin celebrations carry the
+ literal layer id "temporal" (rite_ef/temporal_ef.ml's [build]), which is
+ not [universal_layer] either, and would otherwise be misread as
+ "proper" by the layer test alone.
+
+ Vigils (21, 26) are the one shape neither of those two kinds fully
+ describes on their own: a II/III-class vigil can be temporal-origin (the
+ Ascension Vigil, produced by temporal_ef today) or sanctoral-origin (a
+ saint's vigil, not yet loaded by any task), so its entry cannot be gated
+ on [origin] at all. Nothing in the day's other fields marks "this is a
+ vigil, not an ordinary office of the same rank" either, so this reads it
+ off the temporal cycle's own slug convention (a "-vigil" suffix -- see
+ [named] in temporal_ef.ml) rather than guessing a new one. *)
+
+open Colitur_kernel
+
+(* Not an RG citation -- RG 91 ranks proper and indult feasts, it does not
+ encode how a computer tells them apart. See precedence_ef.mli. *)
+let universal_layer = "ef-universal"
+let indult_prefix = "indult:"
+let unclassified = max_int
+
+let is_indult layer = String.starts_with ~prefix:indult_prefix layer
+let is_universal layer = String.equal layer universal_layer
+
+(* Ember days are identified by the temporal cycle's own slug convention
+ (rite_ef/temporal_ef.ml's [ember]: "ef-<set>-ember-<day>"), not
+ re-derived here: the September anchor in particular is one of the more
+ contested dates in the 1962 calendar (temporal_ef.ml's own comment on
+ [third_sunday_of_september]), and re-deriving it a second time would only
+ create a second place for that same uncertainty to drift. Only the
+ Advent, Lent and September sets are listed: RG 91 entry 18 names exactly
+ those three; the Whitsun (Pentecost) set is I class and falls inside the
+ Pentecost octave, entry 10, matched below before this is ever reached. *)
+let is_ember_18 slug =
+ String.starts_with ~prefix:"ef-advent-ember-" slug
+ || String.starts_with ~prefix:"ef-lent-ember-" slug
+ || String.starts_with ~prefix:"ef-september-ember-" slug
+
+let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedence.candidate) :
+ int =
+ let cel = c.Precedence.cel in
+ let rank = cel.Celebration.rank in
+ let subject = cel.Celebration.subject in
+ let layer = cel.Celebration.layer in
+ let slug = Slug.to_string cel.Celebration.slug in
+ let is_temporal = c.Precedence.origin = Precedence.Temporal in
+ let is_vigil = String.ends_with ~suffix:"-vigil" slug in
+ let date = ctx.Precedence.date in
+ let season = ctx.Precedence.season in
+ let weekday = ctx.Precedence.weekday in
+ let is_sunday = weekday = Date.Sun in
+ let m = Date.month date and d = Date.day date in
+ (* Easter offset, the same convention as temporal_ef.ml's [days_between
+ easter d]: 0 is Easter itself, negative before, positive after. *)
+ let off = Date.to_rata date - Date.to_rata (Computus.gregorian_easter (Date.year date)) in
+ let open Vocab_ef in
+ (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *)
+ if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1
+ (* 2: Sacred Triduum (Thu-Sat of Holy Week). *)
+ else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 2
+ (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart,
+ Christ the King. *)
+ else if is_temporal && rank = Class1
+ && ((m = 1 && d = 6) (* Epiphany *)
+ || off = 39 (* Ascension *) || off = 56 (* Trinity *)
+ || off = 60 (* Corpus Christi *) || off = 68 (* Sacred Heart *)
+ || Date.compare date (Temporal_ef.christ_the_king (Date.year date)) = 0)
+ then 3
+ (* 4: Immaculate Conception, Assumption BVM. *)
+ else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 4
+ (* 5: Vigil & Octave day of the Nativity. *)
+ else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 5
+ (* 6: Sundays of Advent, Lent, Passiontide, and Low Sunday. *)
+ else if is_temporal && rank = Class1 && is_sunday
+ && (season = Advent || season = Lent || season = Passiontide || off = 7)
+ then 6
+ (* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week.
+ Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *)
+ else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 7
+ (* 8: All Souls. *)
+ else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then 8
+ (* 9: Vigil of Pentecost. *)
+ else if is_temporal && rank = Class1 && off = 48 then 9
+ (* 10: Days within the Octaves of Easter and Pentecost. *)
+ else if is_temporal && rank = Class1 && ((off >= 1 && off <= 6) || (off >= 50 && off <= 55))
+ then 10
+ (* 11: I-class feasts of the universal Church not above. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 11
+ (* 12: Proper I-class feasts. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 12
+ (* 13: Indult I-class feasts. By elimination once 11 and 12 have failed:
+ not the universal layer (11), and marked as an indult overlay (12's
+ "not indult" test having just failed). *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class1 then 13
+ (* 14: Feasts of the Lord, II class. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer
+ && subject = Subject.Lord
+ then 14
+ (* 15: Sundays, II class (every Sunday not already named at 6). *)
+ else if is_temporal && rank = Class2 && is_sunday then 15
+ (* 16: II-class feasts of the universal Church, not of the Lord. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 16
+ (* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen,
+ John, the Innocents -- sanctoral, not this entry). *)
+ else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 17
+ (* 18: II-class ferias -- Advent 17-23 Dec; Ember days of Advent, Lent,
+ September. *)
+ else if is_temporal && rank = Class2
+ && ((season = Advent && m = 12 && d >= 17 && d <= 23) || is_ember_18 slug)
+ then 18
+ (* 19: Proper II-class feasts. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 19
+ (* 20: Indult II-class feasts. By elimination, as at 13. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class2 then 20
+ (* 21: II-class vigils (Ascension, Assumption, John Baptist, Peter & Paul
+ -- can be temporal- or sanctoral-origin, see the file comment above). *)
+ else if rank = Class2 && is_vigil then 21
+ (* 22: Ferias of Lent and Passiontide (Thursday after Ash Wednesday to the
+ Saturday before Palm Sunday), except the Ember days (18 above). *)
+ else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 22
+ (* 23: III-class feasts in particular calendars. Unlike 11/12 and 14/16
+ above, the universal entry (24) is the HIGHER number here -- RG 91's
+ own table ranks a particular-calendar III-class feast ahead of a
+ universal one, the reverse of the I/II-class ordering. Transcribed as
+ the register states it, not "corrected" into the other classes'
+ pattern. RG 91 has no indult sub-rank at III class, so every non-base
+ layer lands here, not split further. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 23
+ (* 24: III-class feasts in the universal calendar. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class3 then 24
+ (* 25: Ferias of Advent to 16 Dec, except the Ember days (18 above). *)
+ else if is_temporal && rank = Class3 && season = Advent then 25
+ (* 26: III-class vigils (St Lawrence). *)
+ else if rank = Class3 && is_vigil then 26
+ (* 27: Office of the BVM on Saturday -- every otherwise-unoccupied IV-class
+ Saturday, per the historical default that fills it; ordinary Mass
+ propers still make Rogation Mon/Tue/Wed proper without changing the
+ Office (RG 88, see temporal_ef.ml's [temporal]), so those never carry
+ this entry unless they happen to fall on the Saturday itself. Excludes
+ vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no
+ IV-class vigil at all (its own vigil list, register lines 381-384,
+ stops at III class), so one would be an anomaly, not this entry. *)
+ else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 27
+ (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own
+ comment on [ferial_rank] cites the same primary text, "Feriae IV
+ classis"). Excludes vigils for the same reason as 27 above: a IV-class
+ "feria" that is also a vigil is not a feria RG 91 describes. *)
+ else if (not is_vigil) && rank = Class4 then 28
+ else unclassified
diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli
new file mode 100644
index 0000000..e8d4a11
--- /dev/null
+++ b/lib/rites/rite_ef/precedence_ef.mli
@@ -0,0 +1,35 @@
+(** RG 91's Table of Precedence for the EF (1962) rite: ranks any candidate
+ for a given day by its RG 91 entry number. See
+ docs/research/rules-register.md §4, whose 28-entry transcription this
+ module follows line by line. *)
+
+open Colitur_kernel
+
+(** The universal (base) sanctoral layer's {!Celebration.t}.layer id. A
+ Sanctoral-origin candidate whose layer is anything else is an overlay:
+ "proper" (RG 91 entries 12, 19, 23) unless its layer id also carries
+ {!indult_prefix} ("indult", entries 13, 20). This id and the prefix are
+ colitur's own data-modelling convention, not an RG citation -- RG 91
+ prescribes the ranking, not a machine encoding for it. Whichever task
+ loads the real EF sanctoral base layer and its overlays must either
+ reuse these two constants or this classifier will misfile them. *)
+val universal_layer : string
+
+(** See {!universal_layer}. *)
+val indult_prefix : string
+
+(** Returned for a candidate shape RG 91's 28-entry table has no row for --
+ e.g. a [Class1] vigil that is not the Nativity or Pentecost (entries 5,
+ 9 are the only I-class vigils the table names), or a [Class4] candidate
+ also marked as a vigil. Deliberately outside 1..28 and larger than any
+ real entry, so an unclassified candidate can never win an occurrence
+ contest by accident; a caller that sees it back knows the shape needs a
+ new rule, not a silently wrong one. *)
+val unclassified : int
+
+(** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry
+ number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28;
+ lower wins (see {!Precedence.rules.band}). Total over every candidate
+ {!Precedence.resolve} or {!Calendar} can construct, including shapes the
+ 1962 table itself does not describe (see {!unclassified}). *)
+val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index a97e35c..a9cacd2 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -3,4 +3,4 @@ let () =
Alcotest.run "colitur"
[ Test_date.suite; Test_computus.suite; Test_colour.suite; Test_slug.suite; Test_names.suite;
Test_overlay.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite;
- Test_calendar.suite ]
+ Test_calendar.suite; Test_precedence_ef.suite ]
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
new file mode 100644
index 0000000..fb05582
--- /dev/null
+++ b/test/test_precedence_ef.ml
@@ -0,0 +1,202 @@
+(* RG 91's Table of Precedence, transcribed by Rite_ef.Precedence_ef.band.
+ Table-driven, one row (hence one Alcotest.test_case) per RG 91 entry, so a
+ misplaced or missing entry names itself in the failure output instead of
+ failing anonymously (docs/research/rules-register.md §4). Each row's date
+ is checked against the register to make sure it is not ALSO an instance of
+ some other entry at the same band (the vacuous-test trap this project has
+ caught before -- see the Advent-Ember-day note on entry 18 below). *)
+
+module P = Colitur_kernel.Precedence
+module Cel = Colitur_kernel.Celebration
+module S = Colitur_kernel.Slug
+module Col = Colitur_kernel.Colour
+module D = Colitur_kernel.Date
+module Sub = Colitur_kernel.Subject
+module Comp = Colitur_kernel.Computus
+module T = Rite_ef.Temporal_ef
+module V = Rite_ef.Vocab_ef
+module PE = Rite_ef.Precedence_ef
+
+let mk y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> failwith e
+
+(* [T.season] is the same function Calendar itself would use to build a
+ context, so a row's [season]/[weekday] are exactly what the real engine
+ would compute for that date, not a hand-picked value that might not
+ actually occur together with it. *)
+let ctx date = { P.date; season = T.season date; weekday = D.weekday date }
+
+let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(subject = Sub.Temporal) ?(layer = "temporal")
+ slug =
+ { P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~colour:Col.White ~subject ~layer ();
+ origin }
+
+(* Every Easter-relative date below is anchored to this single computed
+ Easter rather than a hand-typed calendar date, so an arithmetic slip in a
+ test date cannot silently pass by accident. *)
+let easter = Comp.gregorian_easter 2026
+let off n = D.add_days easter n
+
+(* (description, date, candidate, expected RG 91 entry). *)
+let cases =
+ [ (* Entry 1 -- register line 327: Nativity, Easter Sunday, Pentecost Sunday. *)
+ ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1);
+ ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1);
+ ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1);
+ (* Entry 2 -- register line 328: Sacred Triduum. Thu-Sat of Holy Week,
+ NOT entry 7 (which stops at Wednesday -- see entry 7 below). *)
+ ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2);
+ ("2 Good Friday", off (-2), cand "ef-good-friday", 2);
+ ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2);
+ (* Entry 3 -- register line 329. *)
+ ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3);
+ ("3 Ascension", off 39, cand "ef-ascension", 3);
+ ("3 Trinity", off 56, cand "ef-trinity", 3);
+ ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3);
+ ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3);
+ ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3);
+ (* Entry 4 -- register line 330. Sanctoral-origin: neither feast is part
+ of temporal_ef's movable cycle. *)
+ ( "4 Immaculate Conception", mk 2026 12 8,
+ cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer
+ "ef-immaculate-conception",
+ 4 );
+ ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4);
+ (* Entry 5 -- register line 331. *)
+ ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5);
+ ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5);
+ (* Entry 6 -- register line 332. *)
+ ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6);
+ ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6);
+ ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6);
+ ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6);
+ ("6 Low Sunday", off 7, cand "ef-low-sunday", 6);
+ (* Entry 7 -- register line 333: Ash Wednesday and Mon/Tue/Wed of Holy
+ Week ONLY -- Thu-Sat are entry 2 above, not this entry. *)
+ ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7);
+ ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7);
+ ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7);
+ ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7);
+ (* Entry 8 -- register line 334. *)
+ ("8 All Souls", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8);
+ (* Entry 9 -- register line 335. *)
+ ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9);
+ (* Entry 10 -- register line 336: both range boundaries, to guard the
+ off-by-one an inclusive Easter-offset window invites. *)
+ ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10);
+ ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10);
+ ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10);
+ ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10);
+ (* Entry 11 -- register line 337. *)
+ ( "11 Universal I-class feast", mk 2026 6 29,
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul",
+ 11 );
+ (* Entry 12 -- register line 338. The one non-base-layer case the brief
+ asks for explicitly: same date/rank/subject as 11, only the layer
+ differs, so this row isolates the layer test as the deciding factor. *)
+ ( "12 Proper I-class feast (non-base layer)", mk 2026 6 29,
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron",
+ 12 );
+ (* Entry 13 -- register line 339. *)
+ ( "13 Indult I-class feast", mk 2026 6 29,
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant")
+ "ef-indult-feast-1",
+ 13 );
+ (* Entry 14 -- register line 341. *)
+ ( "14 Feast of the Lord, II class", mk 2026 7 1,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
+ "ef-precious-blood",
+ 14 );
+ (* Entry 15 -- register line 342: an ordinary Sunday not named at entry 6
+ -- Septuagesima is II class (RG 11-12 names only Advent/Lent/
+ Passiontide/Easter/Low/Pentecost as I class). *)
+ ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15);
+ (* Entry 16 -- register line 342. *)
+ ( "16 Universal II-class feast, not of the Lord", mk 2026 1 20,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer
+ "ef-some-saint",
+ 16 );
+ (* Entry 17 -- register line 343: days WITHIN the Nativity octave (26-28
+ Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is
+ entry 5's Octave DAY, not this entry either). *)
+ ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17);
+ ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17);
+ (* Entry 18 -- register line 343-344: Advent 17-23 Dec ferias AND the
+ Ember days of Advent/Lent/September share this one entry. The second
+ row is deliberately a Lent date (season Lent, NOT Advent) to prove the
+ Ember-slug path fires on its own, not merely because it also happens
+ to fall in the Dec 17-23 window -- the exact trap the brief warns
+ about, worked the other way round: this Ember day must NOT be
+ mistaken for an ordinary entry-22 Lent feria either. *)
+ ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18);
+ ("18 Lent Ember Wednesday", off (-39), cand ~rank:V.Class2 "ef-lent-ember-wed", 18);
+ (* Entry 19 -- register line 344. *)
+ ( "19 Proper II-class feast", mk 2026 1 20,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw"
+ "ef-local-saint-2",
+ 19 );
+ (* Entry 20 -- register line 345. *)
+ ( "20 Indult II-class feast", mk 2026 1 20,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint
+ ~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2",
+ 20 );
+ (* Entry 21 -- register line 345 (RG 28-34). Two rows: the Ascension
+ Vigil is the one II-class vigil temporal_ef already produces today
+ (temporal-origin); the Assumption Vigil stands in for the
+ sanctoral-origin case no task has loaded data for yet -- proving
+ [band] does not gate this entry on [origin] (see precedence_ef.ml's
+ file comment). *)
+ ("21 Ascension Vigil (temporal-origin)", off 38, cand ~rank:V.Class2 "ef-ascension-vigil", 21);
+ ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil",
+ 21 );
+ (* Entry 22 -- register line 347-348 (corrected: ends at Palm Sunday, not
+ Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash
+ Wednesday, Holy Week and the Ember days. *)
+ ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22);
+ ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22);
+ (* Entry 23 -- register line 349. NOTE the table's own order here is the
+ REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular
+ calendars) is numbered BELOW entry 24 (universal), so a proper
+ III-class feast outranks a universal one -- transcribed as the
+ register states it, not "corrected" to match the other classes. *)
+ ( "23 Proper III-class feast (non-base layer)", mk 2026 6 30,
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3",
+ 23 );
+ (* Entry 24 -- register line 349. *)
+ ( "24 Universal III-class feast", mk 2026 6 30,
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3",
+ 24 );
+ (* Entry 25 -- register line 350. *)
+ ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25);
+ (* Entry 26 -- register line 350. *)
+ ( "26 III-class vigil", mk 2026 8 9,
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
+ 26 );
+ (* Entry 27 -- register line 352: an otherwise-unoccupied IV-class
+ Saturday. *)
+ ( "27 Office of the BVM on Saturday", off 62,
+ cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat",
+ 27 );
+ (* Entry 28 -- register line 352: the unqualified IV-class catch-all. *)
+ ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 28);
+ (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which
+ is not the Nativity or Pentecost (entries 5/9, the only I-class
+ vigils the table names) and so has no entry to fall into. Proves the
+ documented fallback -- not entry 11/12/13, which the [not is_vigil]
+ guard exists specifically to keep this out of. *)
+ ( "unclassified: I-class vigil outside Nativity/Pentecost", mk 2026 3 10,
+ cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-mystery-vigil",
+ PE.unclassified );
+ (* RG 91's own vigil list (register lines 381-384) stops at III class --
+ there is no IV-class vigil for entry 28's ferial catch-all to absorb. *)
+ ( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20,
+ cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified )
+ ]
+
+let suite =
+ ( "Precedence_ef",
+ List.map
+ (fun (desc, date, c, expect) ->
+ Alcotest.test_case desc `Quick (fun () ->
+ Alcotest.(check int) desc expect (PE.band (ctx date) c)))
+ cases )