(* 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, All Souls) 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. All Souls (entry 8, the one non-temporal-origin member of this group) additionally reads the context's weekday for its own register-stated exception -- see entry 8 below. - A sanctoral feast's entry is decided by its [rank], and -- except at entry 14 (see its own comment below, where the register draws no such line) -- per the brief's structural insight, also by 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 (11-13 I class; 16/19/20 II class; 23/24 III class; 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 (* Not an RG citation either -- see [universal_layer] above. Nothing in {!Celebration.t} otherwise marks "this is a vigil, not an ordinary office of the same rank" (see the file's top comment), so entries 21/26 read it off the temporal cycle's own slug suffix (rite_ef/temporal_ef.ml's [named], e.g. "ef-ascension-vigil"). Exposed so a future task naming a sanctoral vigil (Task 10: Assumption, John Baptist, Peter & Paul, Lawrence -- only Ascension exists today) uses the same suffix; a differently-named vigil would band 16/24 instead of 21/26, silently. *) let vigil_suffix = "-vigil" let is_vigil slug = String.ends_with ~suffix:vigil_suffix slug (* Not an RG citation -- see [universal_layer]. Entry 18's Ember days are identified by the temporal cycle's own slug convention (rite_ef/ temporal_ef.ml's [ember]: "ef--ember-"), 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. Exposed for the same reason as [vigil_suffix]: a rename of temporal_ef's format has somewhere to be caught other than a silently-wrong entry 18. *) let ember_prefixes = [ "ef-advent-ember-"; "ef-lent-ember-"; "ef-september-ember-" ] let is_ember_18 slug = List.exists (fun prefix -> String.starts_with ~prefix slug) ember_prefixes 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 = is_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 (* Named so entry 8's Sunday exception below can read "one worse than the Sunday it must yield to" rather than a bare integer that happens to equal entry 15's own value; entry 15's own branch returns this same binding, not a second literal, so the two can never drift apart. *) let entry_15_band = 15 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 -- register line 334's own text carries a qualifier this transcription must honour: "yields to an occurring Sunday". 2 November is always Time_after_pentecost (well clear of Advent/Lent/Passiontide and of every other entry's own Easter-relative or fixed date), so a Sunday landing on it is always an ordinary entry-15 II-class Sunday -- the one and only rival this exception ever has to lose to. On such a Sunday this returns [entry_15_band + 1]: strictly worse than 15 (an exact tie would fall to Precedence.resolve's slug tie-break, which for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug would make All Souls WIN -- the precise bug this guards against), but otherwise not a citation to any other RG 91 row -- nothing else can ever occur on 2 November to be confused with it. Entry 8's own [rank] is untouched by this, so Task 8's disposition (RG 95: only I-class feasts transfer) still sees the true I-class candidate it needs to move to 3 November. *) else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then if is_sunday then entry_15_band + 1 else 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 -- register line 341, deliberately UNQUALIFIED (contrast entry 16 at line 342, which explicitly says "not of the Lord"; RG 37c, register line 393, speaks of "II-class feasts of the Lord" replacing an occurring II-class Sunday with no universal qualifier either). No layer test here, unlike 11/12/13 and 16/19/20: the register does not split this entry into universal/proper/indult, so a proper or indult feast of the Lord still bands 14, not 19/20. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 && 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 entry_15_band (* 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 (* Task 8: what happens to the day's LOSING candidate (docs/research/ rules-register.md §4, "Occurrence" RG 92-95 and "Vigils" RG 33, plus RG 94). [band] above decides who wins; this decides the loser's fate, which turns on the LOSER's own rank and status (RG 95), except RG 33's vigil omission, which also has to read the winner. Nothing here ever returns [Precedence.Repose]: that disposition denotes RG 100-102's *repositio* (perpetual impediment from a proper/diocesan calendar), out of this plan's scope -- see calendar.mli's own note that nothing in the EF ruleset currently emits it. *) (* RG 33 (register line 383-384): a I- or II-class vigil falling on any Sunday or a I-class feast is entirely omitted. Every Sunday slug this rite's temporal cycle produces -- named (temporal_ef.ml's [named], e.g. "ef-easter-sunday") or the generic "ef--sunday-" fallback ([sunday_slug]) -- contains this marker; nothing else [band] classifies does. Not an RG citation itself -- see [universal_layer]'s note on this file's own naming conventions -- exposed for the same reason as {!vigil_suffix}: a future rename of temporal_ef's Sunday-slug format has somewhere to be caught other than a silently-wrong RG 33 disposition. *) let sunday_marker = "-sunday" let contains_substring s ~needle = let ls = String.length s and ln = String.length needle in let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in ln = 0 || at 0 let is_sunday_slug slug = contains_substring slug ~needle:sunday_marker (* RG 33's own two conditions, taken directly from its text ("any Sunday or a I-class feast") -- NOT derived from anything about which RG 91 entries can numerically outrank a vigil. [rank = Class1] is the "I-class feast" half. [is_sunday_slug] is the "any Sunday" half, and it is not redundant with the rank check: RG 91 entries 14 and 16-20 (Feasts of the Lord II class, universal/proper/indult II-class feasts, days within the Nativity octave) are all [Class2], all outrank a II-class vigil (entry 21), and none of them is a Sunday -- a winner of that shape satisfies neither condition here, so [impedes_vigil] correctly returns [false] and such a vigil falls through to RG 95's ordinary commemorate-or-omit branch instead of RG 33's omission, exactly as the rubric requires. *) let impedes_vigil (winner : Vocab_ef.rank Precedence.candidate) = let cel = winner.Precedence.cel in cel.Celebration.rank = Vocab_ef.Class1 || is_sunday_slug (Slug.to_string cel.Celebration.slug) (* Not yet RG 109 (Task 9's job: the closed list of privileged commemorations and RG 108-111's admission counts). Every [Commemorate] this function returns carries this one placeholder rather than a silent default, so the choice is visible and grep-able. [Ordinary] chosen over [Privileged] deliberately: it grants no admission entitlement RG 111 has not earned, so code that trusts this value before Task 9 replaces it under-privileges a commemoration rather than over-privileges one -- the safer direction to be wrong in. *) let interim_privilege = Precedence.Ordinary let disposition ~(winner : Vocab_ef.rank Precedence.candidate) ~(loser : Vocab_ef.rank Precedence.candidate) : Precedence.disposition = let open Vocab_ef in let cel = loser.Precedence.cel in if cel.Celebration.status = Celebration.Commemoration_only then (* Always -- checked before RG 33's omission and RG 95's transfer so neither can override it: a Commemoration_only entry can never win (Precedence.resolve holds it out of the band contest entirely, see that module's [resolve]) and, per the brief, can never transfer either. *) Precedence.Commemorate interim_privilege else if (cel.Celebration.rank = Class1 || cel.Celebration.rank = Class2) && is_vigil (Slug.to_string cel.Celebration.slug) && impedes_vigil winner then (* RG 33. Checked before the generic Class1 -> Transfer rule below, or a I-class vigil (Nativity, Pentecost) impeded on its own Sunday/ I-class-feast terms would wrongly transfer instead of vanishing. *) Precedence.Omit else if cel.Celebration.rank = Class1 then (* RG 95: only I-class feasts have the right of translation. This is the branch that completes Task 7's All Souls fix (register line 334, RG 91 entry 8): All Souls is I class and not a vigil, so once it loses to an occurring Sunday it reaches here and transfers -- to 3 November per the register, but WHERE it lands is Rite.transfer_target's job (RG 96), not this function's; disposition only says THAT it moves. *) Precedence.Transfer else (* RG 95's other branch, for everything below I class: "aut commemorantur aut penitus omittuntur" -- commemorated or wholly omitted. Which of the two survives is RG 108-111's admission count (Task 9's [admit]), not this function's decision; this only opens the commemoration. RG 94 (a fixed-day commemoration is not carried along with a transferred feast) needs no code here: [Precedence.resolve] calls this function once per loser, always against the day's actual [observed] winner -- never against a fellow loser that itself transferred away -- so no mechanism exists by which a commemoration could ride along with a departing feast in the first place; there is nothing to suppress. *) Precedence.Commemorate interim_privilege