aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_of/temporal_of.ml
blob: 9aa898e74a1d61d483a6d049007bba8aecd327b4 (plain) (blame)
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
(* OF (post-1970) temporal cycle. Every boundary and rank rule cites its
   Missale Romanum editio typica tertia (2002) "Normae universales de anno
   liturgico et de calendario" paragraph ("Normae n. N") or Institutio
   Generalis Missalis Romani paragraph ("IGMR n. N") -- see vocab_of.ml's own
   top-of-file comment for why the document has to be named on every
   citation (both are independently numbered from 1 in the same source PDF).
   Extracted with `pdftotext -layout docs/research/of/missale-romanum-2002.pdf`;
   line numbers below are extracted-file line numbers from that command,
   recorded so a citation can be re-located quickly, not because the PDF
   itself is paginated that way. *)

open Colitur_kernel
open Vocab_of

let mk y m d =
  match Date.make ~year:y ~month:m ~day:d with
  | Ok t -> t
  | Error e -> failwith ("temporal_of: " ^ e)

let weekday_index d =
  match Date.weekday d with
  | Date.Sun -> 0 | Date.Mon -> 1 | Date.Tue -> 2 | Date.Wed -> 3
  | Date.Thu -> 4 | Date.Fri -> 5 | Date.Sat -> 6

let sunday_on_or_before d = Date.add_days d (-(weekday_index d))

(* The next Sunday STRICTLY after [d], even when [d] is itself a Sunday --
   distinct from [sunday_on_or_before], and needed for exactly that reason by
   {!baptism_of_the_lord} below (Normae n. 38's "dominica POST diem 6
   ianuarii", not "on or after"). *)
let next_sunday_strictly_after d =
  let wi = weekday_index d in
  Date.add_days d (if wi = 0 then 7 else 7 - wi)

let before a b = Date.compare a b < 0
let on_or_after a b = Date.compare a b >= 0
let same a b = Date.compare a b = 0

(* Normae n. 40 (Titulus II.V, "De tempore Adventus"): "Tempus Adventus
   incipit a I Vesperis dominicae quae incidit in diem 30 novembris vel est
   huic vicinior" -- Advent begins with First Vespers of the Sunday falling
   on 30 November or nearest to it. Worded identically in substance to EF's
   own RG 20 (Rite_ef.Temporal_ef.advent_start's own citation), so the same
   closed-form identity holds and for the same reason: the Sunday nearest 30
   November is, equivalently, three weeks (21 days) before the last Sunday on
   or before 24 December -- verified once, generally, rather than re-derived
   per rite: whichever weekday 24 December falls on, [sunday_on_or_before]
   lands within the 7 days ending there, and subtracting 21 always lands
   within the 7 days around 30 November, since 24 December minus 24 days is
   30 November exactly and 21 is within one week of that. *)
let advent_start y = Date.add_days (sunday_on_or_before (mk y 12 24)) (-21)
let year_start = advent_start

(* Normae n. 38 and the Calendarium Romanum Generale's own January table
   (extracted line ~4033: "Dominica post diem 6 ianuarii: In baptismate
   Domini Festum") -- see this function's own .mli citation for the full
   argument, including why the table's Monday-shift clause never fires here. *)
let baptism_of_the_lord y = next_sunday_strictly_after (mk y 1 6)

(* Normae n. 35(a) and the December calendarium table's own closing line
   (extracted line ~4459: "Dominica infra octavam Nativitatis, vel, ea
   deficiente, die 30 decembris: Sanctae Familiae Iesu, Mariae et Ioseph
   Festum") -- see this function's own .mli citation for the CODE-vs-DATA
   argument for why this lives here rather than in Phase 3's sanctoral data. *)
let holy_family y =
  let window = List.init 6 (fun i -> mk y 12 (26 + i)) in
  match List.find_opt (fun d -> Date.weekday d = Date.Sun) window with
  | Some d -> d
  | None -> mk y 12 30

(* Normae n. 36: "Dominica a die 2 ad diem 5 ianuarii occurrens est Dominica
   II post Nativitatem" -- see this function's own .mli citation for why
   [None] (unlike {!holy_family}) is a genuine, uncovered outcome here: this
   window carries no "otherwise, on a fixed date" fallback clause in the
   primary text. *)
let second_sunday_of_christmas y =
  let window = List.init 4 (fun i -> mk y 1 (2 + i)) in
  List.find_opt (fun d -> Date.weekday d = Date.Sun) window

(* Normae n. 43-44 and the Missale's own "HEBDOMADA XXXIV 'PER ANNUM'"
   heading (extracted line 13099) -- see [week]'s own citation below for the
   full two-block week arithmetic this anchors. *)
let christ_the_king y = Date.add_days (advent_start y) (-7)

(* Normae Titulus II, nn. 17-47 boundary text (season definitions),
   PRIMARY-SOURCE-VERIFIED against the 2002 typical edition
   (docs/research/of/missale-romanum-2002.pdf, pdftotext -layout, extracted
   lines ~3660-3790 for the Titulus II season definitions themselves; see
   vocab_of.ml's own top-of-file comment for the Triduum placement argument
   this function's own Lent branch below rests on).

   CORRECTED: this header previously cited "Normae n. 71-77" -- the EF's own
   RG 71-77 season-boundary range (CLAUDE.md: "seasons RG 71-77"), leaked in
   by mistake. The Normae universales end at n. 61 (the Tabula sits at
   n. 59/60), so that range cannot exist; worse, nn. 71-77 DO exist in the
   same PDF, but as IGMR paragraphs on the Preparation of the Gifts -- exactly
   the document-ambiguity this module's own citation discipline
   (vocab_of.ml:1-9) exists to prevent. The per-bullet citations below (n. 40,
   n. 33, n. 44, n. 28, n. 22-23) were always correct; only this header's own
   range was wrong. Do not reintroduce "n. 71-77" here.

   Tested in chronological order within the civil year, mirroring
   Rite_ef.Temporal_ef.season's own structure -- see that function's own
   comment for why testing "on_or_after advent_this && before christmas_day"
   FIRST is safe even for a January [d]: [advent_start (Date.year d)] for a
   January date is always LATER within that same civil year (late November),
   so the check correctly fails and falls through, and no special-casing is
   needed for the year boundary.

   - Advent (n. 40): [advent_this] through 24 December.
   - Christmas (n. 33, "usque ad dominicam post Epiphaniam ... inclusive"):
     25-31 December, THEN (continuing into the new civil year) 1 January
     through {!baptism_of_the_lord} inclusive.
   - Ordinary Time, first run (n. 44, "usque ad feriam III ante Quadragesimam
     inclusive"): the day after {!baptism_of_the_lord} through the day
     before Ash Wednesday.
   - Lent (n. 28, "usque ad Missam in Cena Domini exclusive", PLUS the
     Triduum-placement argument in vocab_of.ml): Ash Wednesday through Holy
     Saturday inclusive -- i.e. everything before Easter itself.
   - Easter (n. 22-23, "concluditur ... dominica Pentecostes"): Easter Sunday
     through Pentecost Sunday inclusive.
   - Ordinary Time, second run (n. 44, "iterum incipit feria II post
     dominicam Pentecostes et explicit ante I Vesperas dominicae I
     Adventus"): everything else -- the day after Pentecost through the day
     before the FOLLOWING year's [advent_this], which is exactly what
     survives every check above, so no explicit final test is needed; see
     [week]'s own two-block arithmetic for why this run's own numbers still
     land correctly on the far side of the interruption. *)
let season d =
  let y = Date.year d in
  let easter = Computus.gregorian_easter y in
  let advent_this = advent_start y in
  let christmas_day = mk y 12 25 in
  let ash_wednesday = Date.add_days easter (-46) in
  let pentecost = Date.add_days easter 49 in
  let baptism = baptism_of_the_lord y in
  if on_or_after d advent_this && before d christmas_day then Advent
  else if on_or_after d christmas_day then Christmas (* 25-31 Dec *)
  else if before d (Date.add_days baptism 1) then Christmas (* 1 Jan .. baptism, inclusive *)
  else if before d ash_wednesday then Ordinary_time (* first run *)
  else if before d easter then Lent (* Ash Wed .. Holy Saturday, inclusive *)
  else if Date.compare d pentecost <= 0 then Easter (* Easter Sunday .. Pentecost, inclusive *)
  else Ordinary_time (* second run *)

let days_between a b = Date.to_rata b - Date.to_rata a

(* Floor division -- see Rite_ef.Temporal_ef's own identical helper for why
   OCaml's truncating [/] would be wrong for a date before a season's own
   week-1 origin. *)
let floor_div a b = if a >= 0 then a / b else ((a + 1) / b) - 1

let weekday_word d = Date.weekday_to_string (Date.weekday d)

(* IGMR n. 346 (Titulus VIII, "De coloribus"), the season-level defaults; a-e
   listed here, f (rose) is an override applied separately by
   [is_rose_sunday] below, g (festive substitution) is not modelled (the same
   kind of per-Mass-not-per-day nuance EF's own temporal_ef.ml already
   documents as unmodelled for its own RG 126 palms/RG 132 Good Friday
   cases).
   a) "Color albus adhibetur in Officiis et Missis temporis paschalis et
      Nativitatis Domini" -- white for Easter and Christmas time.
   c) "Color viridis adhibetur in Officiis et Missis temporis 'per annum'"
      -- green for Ordinary Time.
   d) "Color violaceus adhibetur tempore Adventus et Quadragesimae" --
      violet for Advent and Lent. *)
let season_colour = function
  | Christmas | Easter -> Colour.White
  | Ordinary_time -> Colour.Green
  | Advent | Lent -> Colour.Violet

(* IGMR n. 346(f): "Color rosaceus adhiberi potest, ubi mos est, in
   dominicis Gaudete (III Adventus) et Laetare (IV in Quadragesima)" -- MAY
   be used, where customary. Treated as colitur's own canonical default the
   same way Rite_ef.Temporal_ef.is_rose_sunday already treats EF's
   structurally identical RG 131 ("may be used... for that Sunday only") --
   an indult over the season's violet, not chosen afresh here. *)
let is_rose_sunday d s =
  let y = Date.year d in
  match s with
  | Advent -> same d (Date.add_days (advent_start y) 14) (* Gaudete: Advent III *)
  | Lent -> same d (Date.add_days (Computus.gregorian_easter y) (-21)) (* Laetare: Lent IV *)
  | _ -> false

let holy_family_names =
  Names.of_list [ (Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae et Ioseph") ]

let baptism_names = Names.of_list [ (Lang.of_string_exn "la", "In Baptismate Domini") ]

(* Named temporal days that need no per-year conditional: the Nativity and
   its Vigil, Mary Mother of God (the Nativity's own Octave Day, 1 January,
   Normae n. 35(f)), Epiphany (n. 37, universal calendar only -- the
   local Sunday-transfer option is out of scope, spec's own EF-precedented
   discipline), Ash Wednesday (n. 29), Palm/Passion Sunday (n. 30, merged
   into Lent's own week VI -- vocab_of.ml's own citation), Easter and the six
   weekday days of its Octave (n. 24, "uti sollemnitates Domini
   celebrantur"), Ascension (n. 25, "quadragesima die post Pascha" --
   Easter+39, inclusive day-counting, the same convention EF's own [off 39]
   anchor uses), Pentecost (n. 22-23), Trinity Sunday and Corpus Christi
   (both found by their own headings in the Missale's "IN SOLLEMNITATIBUS
   DOMINI 'PER ANNUM' OCCURRENTIBUS" appendix, extracted lines
   ~13130-13230: "Dominica I post Pentecosten: SANCTISSIMAE TRINITATIS
   Sollemnitas"; "Feria V post Ss.mam Trinitatem: SANCTISSIMI CORPORIS ET
   SANGUINIS CHRISTI Sollemnitas" -- kept on its PRIMARY, typical-edition
   date, Thursday after Trinity, not the widespread local Sunday-transfer
   option the SAME heading's own next sentence names, for the identical
   universal-calendar-only reason Epiphany's transfer is excluded), and
   Christ the King (n. 43's own heading, this file's own [christ_the_king]
   citation).

   Grades: Nativity/Mary Mother of God/Epiphany/Easter-and-its-Octave/
   Ascension/Pentecost/Trinity/Corpus-Christi/Christ-the-King are
   [Sollemnitas], confirmed against the calendarium's own printed grade word
   for each ("Sollemnitas") wherever a table entry exists (Nativity, Mary
   Mother of God, Christ the King all extracted with that literal word next
   to them; the movable Sundays/Ascension/Pentecost/Trinity/Corpus Christi
   have none, being computed rather than tabulated, but sit at Tabula entry
   2/3/5, the same table region, and the Missale's own two "IN SOLLEMNITATIBUS
   ... OCCURRENTIBUS" headings state it directly for Trinity/Corpus Christi).
   Ash Wednesday is [Feria] -- see vocab_of.ml's own top-of-file comment for
   why a privileged feria stays [Feria] here rather than getting an elevated
   rank the way EF's own [Class1] does: OF's rank vocabulary keeps grade and
   precedence-table-position separate, unlike EF's.

   Colours per IGMR n. 346 (season_colour's own citation covers the season
   defaults; the exceptions below are cited individually): Palm/Passion
   Sunday is RED (346(b): "Color ruber adhibetur in dominica Passionis"),
   not Lent's own violet -- a genuine, easy-to-miss divergence from what the
   surrounding season's colour would suggest. Pentecost is likewise RED
   (346(b): "in dominica Pentecostes"). The Nativity Vigil is tagged white on
   IGMR 346(a)'s general "celebrationibus Domini" clause -- NO day-specific
   colour rubric was found for the Vigil Mass itself in the extracted text
   (unlike, e.g., the Chrism Mass or Holy Thursday's own explicit "color
   albus" note found elsewhere in the same document), so this is the
   best-supported general-clause reading, flagged honestly rather than
   presented as a day-specific citation. *)
let named d =
  let y = Date.year d in
  let easter = Computus.gregorian_easter y in
  let off n = Date.add_days easter n in
  let m = Date.month d and dd = Date.day d in
  if m = 12 && dd = 24 then Some (Advent, "of-nativity-vigil", Colour.White, Sollemnitas)
  else if m = 12 && dd = 25 then Some (Christmas, "of-nativity", Colour.White, Sollemnitas)
  else if m = 1 && dd = 1 then Some (Christmas, "of-mary-mother-of-god", Colour.White, Sollemnitas)
  else if m = 1 && dd = 6 then Some (Christmas, "of-epiphany", Colour.White, Sollemnitas)
  else if same d (off (-46)) then Some (Lent, "of-ash-wednesday", Colour.Violet, Feria) (* Normae n. 29 *)
  else if same d (off (-7)) then Some (Lent, "of-palm-sunday", Colour.Red, Sollemnitas) (* n. 30; IGMR 346(b) *)
  else if same d easter then Some (Easter, "of-easter-sunday", Colour.White, Sollemnitas)
  else if List.mem (days_between easter d) [ 1; 2; 3; 4; 5; 6 ] then
    (* Easter Monday .. Saturday: n. 24's own eight days are Easter Sunday
       (above) plus these six plus the closing Sunday (day 8), deliberately
       left OUT of [named] -- see this file's own [sunday_slug], whose
       generic Sunday-of-Easter branch already lands it on [Sollemnitas] via
       the SAME week-1-numbering path every other Easter Sunday uses, with no
       need for a second, competing named entry. *)
    Some (Easter, Printf.sprintf "of-easter-octave-day-%d" (days_between easter d + 1), Colour.White, Sollemnitas)
  else if same d (off 39) then Some (Easter, "of-ascension", Colour.White, Sollemnitas)
  else if same d (off 49) then Some (Easter, "of-pentecost", Colour.Red, Sollemnitas)
  else if same d (off 56) then Some (Ordinary_time, "of-trinity", Colour.White, Sollemnitas)
  else if same d (off 60) then Some (Ordinary_time, "of-corpus-christi", Colour.White, Sollemnitas)
  else if same d (christ_the_king y) then Some (Ordinary_time, "of-christ-the-king", Colour.White, Sollemnitas)
  else None

(* Week numbering. Advent, Lent and Easter each have a single, simple origin
   Sunday (mirroring Rite_ef.Temporal_ef.week_origin's own shape); Christmas
   has none at all (its own structure is date-keyed throughout -- n. 36 says
   "the Sunday falling 2-5 January", never "week I", and no calendarium
   heading anywhere in Christmas time carries a week-number title the way
   "HEBDOMADA I 'PER ANNUM'" does); Ordinary Time needs the two-block
   arithmetic below.

   ORDINARY TIME'S OWN RESUMPTION RULE -- the hard part this module exists
   to get right, and the reason it is Phase 1's own gate. DERIVED, not
   copied, from three primary-text facts, none of which alone states the
   rule but which together pin it down uniquely:

   1. Normae n. 44: Ordinary Time runs in two disjoint blocks -- "the Monday
      following the Sunday occurring after 6 January" through "Tuesday before
      Lent, inclusive" (the FIRST run), then "the Monday after Pentecost
      Sunday" through "before First Vespers of Advent I" (the SECOND run).
   2. The Missale's own "HEBDOMADA I 'PER ANNUM'" heading (extracted line
      12050: "Dominica prima 'per annum' fit festum Baptismatis Domini") --
      the Baptism of the Lord Sunday IS, positionally, "Dominica I" of
      Ordinary Time's own week-numbering scheme, even though its SEASON, per
      n. 33, is still Christmas. The very next heading in the Missal is
      literally "DOMINICA II 'PER ANNUM'" -- confirming the first run's
      numbering begins counting from that Sunday, landing the run's own
      first NUMBERED Sunday (the one actually inside Ordinary Time's season)
      on week 2.
   3. The Missale's own "HEBDOMADA XXXIV 'PER ANNUM'" heading (extracted
      line 13099: "Dominica ultima 'per annum' fit sollemnitas ...
      universorum Regis") -- the LAST Sunday before Advent I, Christ the
      King, is ALWAYS week XXXIV. Confirmed structurally, not merely
      asserted: nothing in the Missale computes Christ the King's own date
      from a week number at all -- it is defined independently, as "the
      Sunday nearest 30 November, minus 7 days" (this file's own
      [christ_the_king]) -- so this heading is a genuine, independently
      falsifiable claim, not a tautology.

   Given (1)-(3), the FIRST run's week numbers follow the ordinary
   Sunday-aligned [floor_div] formula from origin {!baptism_of_the_lord},
   uninterrupted until Ash Wednesday cuts it off (fact 2 pins the origin;
   nothing further is needed for this half). The SECOND run cannot use the
   same simple formula with a fixed origin, because fact 3 is a CONSTRAINT
   on where the run must END, not a statement of where it begins -- and
   Normae n. 43 itself says the total is "thirty-three OR thirty-four"
   weeks, meaning the gap between where the first run stops and where the
   second run's own numbers would need to start (for the run to still end on
   34) is not constant across years. The only value consistent with facts 1
   and 3 simultaneously is computed BACKWARD from Christ the King: number the
   second run's own weeks, Sunday-aligned, counting forward from Pentecost
   Sunday as if it were itself week 0 (a "raw" count with no liturgical
   meaning of its own), find that raw count for the FINAL week (Christ the
   King's own week), and shift every raw number in the run by the constant
   amount that makes the final one read 34. That shift is not cosmetic the
   way EF's own analogous "n - total + 7" resumed-tail relabelling
   (Rite_ef.Temporal_ef.sunday_slug) is -- there, the underlying [week] FIELD
   stays the raw, uncapped Pentecost count, and only the SLUG string is
   relabelled, because EF's numbering carries no rubrical meaning past
   Time_after_pentecost's own raw count. Here the adjusted number IS the
   liturgical week identity itself (it selects which Mass formulary applies,
   per the Missale's own week headings), so [week] itself, not merely a
   slug, must carry the adjusted value.

   EVIDENCE this produces the right shape (empirically verified, not merely
   argued, across every one of the 8 417 years 1583..9999 with a small
   throwaway script reproducing this exact arithmetic): the two runs never
   overlap (no year assigns the same week number to two different Sundays),
   every number produced falls in 1..34 inclusive, and the gap between the
   first run's own last week and the second run's own first week is ALWAYS
   either 0 (all 34 weeks used, no skip) or 1 (exactly one week number
   skipped that year, used by neither run) -- never more, never negative --
   which is exactly Normae n. 43's own "thirty-three OR thirty-four", with
   "34" corresponding to gap 0 and "33" to gap 1. This IS "in some years a
   week is skipped entirely" (the task brief's own framing), now measured:
   2 672 of the 8 417 years use all 34; 5 745 skip exactly one. *)
let ordinary_time_week d y easter =
  let baptism = baptism_of_the_lord y in
  let ash_wednesday = Date.add_days easter (-46) in
  if before d ash_wednesday then
    let n = floor_div (days_between baptism d) 7 + 1 in
    if n < 1 then None else Some n
  else
    let pentecost = Date.add_days easter 49 in
    let ctk = christ_the_king y in
    let raw_total = floor_div (days_between pentecost ctk) 7 + 1 in
    let raw_n = floor_div (days_between pentecost d) 7 + 1 in
    let adjusted = 34 - raw_total + raw_n in
    if adjusted < 1 || adjusted > 34 then None else Some adjusted

let week d =
  let y = Date.year d in
  let easter = Computus.gregorian_easter y in
  match season d with
  | Christmas -> None
  | Advent ->
      let n = floor_div (days_between (advent_start y) d) 7 + 1 in
      if n < 1 then None else Some n
  | Lent ->
      let n = floor_div (days_between (Date.add_days easter (-42)) d) 7 + 1 in
      if n < 1 then None else Some n
  | Easter ->
      let n = floor_div (days_between easter d) 7 + 1 in
      if n < 1 then None else Some n
  | Ordinary_time -> ordinary_time_week d y easter

(* Sunday slugs. Christmas needs its own three-way dispatch (the season has
   no week numbering at all, and carries up to three distinct movable
   Sundays a year -- {!holy_family}, {!second_sunday_of_christmas} and
   {!baptism_of_the_lord} -- every OTHER season's Sundays use the generic
   week-numbered form. A Sunday {!named} already intercepts (Palm/Passion
   Sunday, Easter, Ascension is never a Sunday, Pentecost, Trinity, Corpus
   Christi is never a Sunday, Christ the King) never reaches this function at
   all, since {!temporal}'s own dispatch checks [named] first. *)
let sunday_slug d =
  if Date.weekday d <> Date.Sun then None
  else
    let y = Date.year d in
    let s = season d in
    match s with
    | Christmas ->
        if same d (holy_family y) then Some "of-holy-family"
        else if second_sunday_of_christmas y = Some d then Some "of-christmas-sunday-2"
        else if same d (baptism_of_the_lord y) then Some "of-baptism-of-the-lord"
        else None (* unreachable: every Christmas-season Sunday is one of the three above *)
    | _ -> (
        match week d with
        | Some n -> Some (Printf.sprintf "of-%s-sunday-%d" (season_slug_word s) n)
        | None -> None)

(* Ferial slugs within Christmas time. Three disjoint stretches, numbered
   0/1/2 exactly like Rite_ef.Temporal_ef.christmastide_feria_slug's own
   three (a real structural parallel between the two rites' Christmas
   seasons, not a copied convention) -- needed for the identical reason: a
   single "no week numbering" slug key would let the same weekday recur
   across stretches within one liturgical year, violating slug uniqueness
   (spec's own requirement, {!Colitur_kernel.Validate}'s ["slugs"] check).
   - 26-31 December (minus {!holy_family}, when it lands there, already
     intercepted earlier): "of-christmas-0-<weekday>".
   - 2-5 January (minus {!second_sunday_of_christmas}, when it exists):
     "of-christmas-1-<weekday>".
   - 7 January through the day before {!baptism_of_the_lord} (0 to 6 days,
     empty exactly when Epiphany falls on a Saturday): "of-christmas-2-<weekday>". *)
let christmas_feria_slug d y =
  let m = Date.month d and dd = Date.day d in
  let w = weekday_word d in
  if m = 12 && dd >= 26 && dd <= 31 then Some (Printf.sprintf "of-christmas-0-%s" w)
  else if m = 1 && dd >= 2 && dd <= 5 then Some (Printf.sprintf "of-christmas-1-%s" w)
  else if m = 1 && dd >= 7 && before d (baptism_of_the_lord y) then Some (Printf.sprintf "of-christmas-2-%s" w)
  else None

let id = "of"

let temporal d =
  let y = Date.year d in
  let s = season d in
  let weekday = Date.weekday d in
  let build ?(subject = Subject.Temporal) ?(names = Names.empty) ~season ~slug ~colour ~rank ~week () =
    let office = Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~names ~layer:"temporal" () in
    { Temporal.season; week; weekday; office }
  in
  match named d with
  | Some (season, slug, colour, rank) -> build ~season ~slug ~colour ~rank ~week:(week d) ()
  | None -> (
      match sunday_slug d with
      | Some slug ->
          let colour = if is_rose_sunday d s then Colour.Rose else season_colour s in
          let subject, names, rank =
            if same d (holy_family y) then (Subject.Lord, holy_family_names, Festum)
              (* Tabula entry 6 ("Dominicae temporis Nativitatis") would
                 already give an ordinary Christmas-time Sunday Festum; Holy
                 Family's own grade word in the calendarium ("Festum") happens
                 to agree, so no separate table-entry citation is needed for
                 the RANK itself, only for the identity override. *)
            else if second_sunday_of_christmas y = Some d then (Subject.Temporal, Names.empty, Festum)
            else if same d (baptism_of_the_lord y) then (Subject.Lord, baptism_names, Festum)
            else
              (* Tabula entry 2 ("Dominicae Adventus, Quadragesimae et
                 Paschae") vs. entry 6 ("dominicae 'per annum'") -- the ONLY
                 remaining case reaching this branch is a plain, unnamed
                 Sunday of Advent, Lent, Easter or Ordinary Time (every
                 Christmas Sunday is already one of the three branches
                 above). *)
              let rank = match s with Advent | Lent | Easter -> Sollemnitas | Ordinary_time | Christmas -> Festum in
              (Subject.Temporal, Names.empty, rank)
          in
          build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) ()
      | None -> (
          match christmas_feria_slug d y with
          | Some slug -> build ~season:s ~slug ~colour:(season_colour s) ~rank:Feria ~week:(week d) ()
          | None ->
              let easter = Computus.gregorian_easter y in
              let after_ashes = days_between easter d in
              if after_ashes >= -45 && after_ashes <= -43 then
                (* Thursday/Friday/Saturday after Ash Wednesday: proper
                   Masses, but before Lent's own week-1 origin -- no week
                   number, mirroring Rite_ef.Temporal_ef's identical gap. *)
                build ~season:s
                  ~slug:(Printf.sprintf "of-lent-after-ashes-%s" (weekday_word d))
                  ~colour:Colour.Violet ~rank:Feria ~week:None ()
              else
                let week_n = week d in
                let slug =
                  Printf.sprintf "of-%s-%d-%s" (season_slug_word s) (Option.value week_n ~default:0) (weekday_word d)
                in
                let colour =
                  if after_ashes = -3 then
                    (* Holy Thursday (Missa in Cena Domini): IGMR n. 346(a)'s
                       own general clause, "insuper in celebrationibus
                       Domini, quae non sint de eius Passione" -- a
                       celebration OF THE LORD that is not of his Passion
                       (the institution of the Eucharist and the
                       priesthood, distinct from Good Friday's own Passion
                       commemoration) -- white. No DAY-SPECIFIC "color
                       albus" rubric was found in the extracted text for
                       this exact Mass (unlike some other Masses in the same
                       document); this is the best-supported general-clause
                       reading, flagged rather than presented as a
                       day-specific citation, the same honesty [named]'s own
                       Nativity Vigil colour note above gives. *)
                    Colour.White
                  else if after_ashes = -2 then
                    (* Good Friday: IGMR n. 346(b), "Color ruber adhibetur
                       ... in celebrationibus Passionis Domini" -- red, not
                       Lent's own season violet. *)
                    Colour.Red
                  else
                    (* Holy Saturday's own daytime (after_ashes = -1) is
                       aliturgical -- no Mass, hence no colour rubric to find
                       in the primary text at all -- and every other Lenten
                       feria has none either; both default to the season's
                       own violet, the textually conservative choice rather
                       than an invented specific one. *)
                    season_colour s
                in
                build ~season:s ~slug ~colour ~rank:Feria ~week:week_n ()))

(* Independent restatement of {!named}'s fixed and Easter-relative dates,
   plus {!holy_family}, {!second_sunday_of_christmas} (when it exists that
   year) and {!baptism_of_the_lord}, paired with the slug each carries --
   the same anchor-agreement discipline Rite_ef.Temporal_ef.anchors follows,
   for the identical reason (catches an accidental single-site drift, e.g. an
   offset silently changing, rather than leaving it invisible). Easter's own
   six weekday octave days are included via the same [1;2;3;4;5;6] offset
   list [named] itself uses, restated independently rather than reused, for
   the same "independent" discipline the rest of this list follows. *)
let anchors y =
  let easter = Computus.gregorian_easter y in
  let off n = Date.add_days easter n in
  [ ("of-nativity-vigil", mk y 12 24);
    ("of-nativity", mk y 12 25);
    ("of-mary-mother-of-god", mk y 1 1);
    ("of-epiphany", mk y 1 6);
    ("of-holy-family", holy_family y);
    ("of-baptism-of-the-lord", baptism_of_the_lord y);
    ("of-ash-wednesday", off (-46));
    ("of-palm-sunday", off (-7));
    ("of-easter-sunday", off 0) ]
  @ List.map (fun n -> (Printf.sprintf "of-easter-octave-day-%d" (n + 1), off n)) [ 1; 2; 3; 4; 5; 6 ]
  @ [ ("of-ascension", off 39);
      ("of-pentecost", off 49);
      ("of-trinity", off 56);
      ("of-corpus-christi", off 60);
      ("of-christ-the-king", christ_the_king y) ]
  @ (match second_sunday_of_christmas y with
     | Some d -> [ ("of-christmas-sunday-2", d) ]
     | None -> [])

(* Compile-time check that this module satisfies the kernel's rite contract
   -- the same check Rite_ef.Temporal_ef ends with. Deliberately the ONLY
   kernel-facing signature this phase satisfies: {!Colitur_kernel.Rite.t}
   itself (precedence, transfers, readings) is Phase 2's own deliverable, per
   the task brief's explicit "do not add a Rite.t for it". *)
module _ : Colitur_kernel.Temporal.RITE = struct
  let id = id

  type season = Vocab_of.season
  type rank = Vocab_of.rank

  let vocab = Vocab_of.vocab
  let year_start = year_start
  let temporal = temporal
end