1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
(* 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-<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.
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-<season>-sunday-<n>" 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 "any Sunday or a I-class feast": every RG 91 entry that can ever
outrank a II-class vigil (entry 21) without itself being a Sunday is I
class by the table's own structure (entries 1-13), so [rank = Class1]
alone already covers every way a I-class vigil (entries 5, 9 -- Nativity,
Pentecost) can be impeded at all; the slug check is what a II-class vigil
impeded by an ordinary Sunday (entry 15, rank Class2) needs, since that
winner's own rank is not Class1. *)
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
|