diff options
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 173 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 35 | ||||
| -rw-r--r-- | test/test_colitur.ml | 2 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 202 |
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 ) |
