(* 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