diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:01:24 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:04:59 +0200 |
| commit | 436ba75e27d2aa61b1c6035a22157b40f1a9834b (patch) | |
| tree | 4ec34520d9627409e72298ab8bfe087527ad07b9 /lib/rites | |
| parent | 1d6be4ac281e62accfb905b137b3c6b494183ee4 (diff) | |
| download | colitur-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.
Diffstat (limited to 'lib/rites')
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 173 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 35 |
2 files changed, 208 insertions, 0 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 |
