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 /test/test_precedence_ef.ml | |
| 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 'test/test_precedence_ef.ml')
| -rw-r--r-- | test/test_precedence_ef.ml | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml new file mode 100644 index 0000000..fb05582 --- /dev/null +++ b/test/test_precedence_ef.ml @@ -0,0 +1,202 @@ +(* RG 91's Table of Precedence, transcribed by Rite_ef.Precedence_ef.band. + Table-driven, one row (hence one Alcotest.test_case) per RG 91 entry, so a + misplaced or missing entry names itself in the failure output instead of + failing anonymously (docs/research/rules-register.md §4). Each row's date + is checked against the register to make sure it is not ALSO an instance of + some other entry at the same band (the vacuous-test trap this project has + caught before -- see the Advent-Ember-day note on entry 18 below). *) + +module P = Colitur_kernel.Precedence +module Cel = Colitur_kernel.Celebration +module S = Colitur_kernel.Slug +module Col = Colitur_kernel.Colour +module D = Colitur_kernel.Date +module Sub = Colitur_kernel.Subject +module Comp = Colitur_kernel.Computus +module T = Rite_ef.Temporal_ef +module V = Rite_ef.Vocab_ef +module PE = Rite_ef.Precedence_ef + +let mk y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> failwith e + +(* [T.season] is the same function Calendar itself would use to build a + context, so a row's [season]/[weekday] are exactly what the real engine + would compute for that date, not a hand-picked value that might not + actually occur together with it. *) +let ctx date = { P.date; season = T.season date; weekday = D.weekday date } + +let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(subject = Sub.Temporal) ?(layer = "temporal") + slug = + { P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~colour:Col.White ~subject ~layer (); + origin } + +(* Every Easter-relative date below is anchored to this single computed + Easter rather than a hand-typed calendar date, so an arithmetic slip in a + test date cannot silently pass by accident. *) +let easter = Comp.gregorian_easter 2026 +let off n = D.add_days easter n + +(* (description, date, candidate, expected RG 91 entry). *) +let cases = + [ (* Entry 1 -- register line 327: Nativity, Easter Sunday, Pentecost Sunday. *) + ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1); + ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1); + ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1); + (* Entry 2 -- register line 328: Sacred Triduum. Thu-Sat of Holy Week, + NOT entry 7 (which stops at Wednesday -- see entry 7 below). *) + ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2); + ("2 Good Friday", off (-2), cand "ef-good-friday", 2); + ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2); + (* Entry 3 -- register line 329. *) + ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3); + ("3 Ascension", off 39, cand "ef-ascension", 3); + ("3 Trinity", off 56, cand "ef-trinity", 3); + ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3); + ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3); + ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3); + (* Entry 4 -- register line 330. Sanctoral-origin: neither feast is part + of temporal_ef's movable cycle. *) + ( "4 Immaculate Conception", mk 2026 12 8, + cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer + "ef-immaculate-conception", + 4 ); + ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4); + (* Entry 5 -- register line 331. *) + ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5); + ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5); + (* Entry 6 -- register line 332. *) + ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6); + ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6); + ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6); + ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6); + ("6 Low Sunday", off 7, cand "ef-low-sunday", 6); + (* Entry 7 -- register line 333: Ash Wednesday and Mon/Tue/Wed of Holy + Week ONLY -- Thu-Sat are entry 2 above, not this entry. *) + ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7); + ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7); + ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7); + ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7); + (* Entry 8 -- register line 334. *) + ("8 All Souls", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8); + (* Entry 9 -- register line 335. *) + ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9); + (* Entry 10 -- register line 336: both range boundaries, to guard the + off-by-one an inclusive Easter-offset window invites. *) + ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10); + ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10); + ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10); + ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10); + (* Entry 11 -- register line 337. *) + ( "11 Universal I-class feast", mk 2026 6 29, + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul", + 11 ); + (* Entry 12 -- register line 338. The one non-base-layer case the brief + asks for explicitly: same date/rank/subject as 11, only the layer + differs, so this row isolates the layer test as the deciding factor. *) + ( "12 Proper I-class feast (non-base layer)", mk 2026 6 29, + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron", + 12 ); + (* Entry 13 -- register line 339. *) + ( "13 Indult I-class feast", mk 2026 6 29, + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant") + "ef-indult-feast-1", + 13 ); + (* Entry 14 -- register line 341. *) + ( "14 Feast of the Lord, II class", mk 2026 7 1, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer + "ef-precious-blood", + 14 ); + (* Entry 15 -- register line 342: an ordinary Sunday not named at entry 6 + -- Septuagesima is II class (RG 11-12 names only Advent/Lent/ + Passiontide/Easter/Low/Pentecost as I class). *) + ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15); + (* Entry 16 -- register line 342. *) + ( "16 Universal II-class feast, not of the Lord", mk 2026 1 20, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-some-saint", + 16 ); + (* Entry 17 -- register line 343: days WITHIN the Nativity octave (26-28 + Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is + entry 5's Octave DAY, not this entry either). *) + ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17); + ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17); + (* Entry 18 -- register line 343-344: Advent 17-23 Dec ferias AND the + Ember days of Advent/Lent/September share this one entry. The second + row is deliberately a Lent date (season Lent, NOT Advent) to prove the + Ember-slug path fires on its own, not merely because it also happens + to fall in the Dec 17-23 window -- the exact trap the brief warns + about, worked the other way round: this Ember day must NOT be + mistaken for an ordinary entry-22 Lent feria either. *) + ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18); + ("18 Lent Ember Wednesday", off (-39), cand ~rank:V.Class2 "ef-lent-ember-wed", 18); + (* Entry 19 -- register line 344. *) + ( "19 Proper II-class feast", mk 2026 1 20, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw" + "ef-local-saint-2", + 19 ); + (* Entry 20 -- register line 345. *) + ( "20 Indult II-class feast", mk 2026 1 20, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint + ~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2", + 20 ); + (* Entry 21 -- register line 345 (RG 28-34). Two rows: the Ascension + Vigil is the one II-class vigil temporal_ef already produces today + (temporal-origin); the Assumption Vigil stands in for the + sanctoral-origin case no task has loaded data for yet -- proving + [band] does not gate this entry on [origin] (see precedence_ef.ml's + file comment). *) + ("21 Ascension Vigil (temporal-origin)", off 38, cand ~rank:V.Class2 "ef-ascension-vigil", 21); + ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil", + 21 ); + (* Entry 22 -- register line 347-348 (corrected: ends at Palm Sunday, not + Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash + Wednesday, Holy Week and the Ember days. *) + ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22); + ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22); + (* Entry 23 -- register line 349. NOTE the table's own order here is the + REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular + calendars) is numbered BELOW entry 24 (universal), so a proper + III-class feast outranks a universal one -- transcribed as the + register states it, not "corrected" to match the other classes. *) + ( "23 Proper III-class feast (non-base layer)", mk 2026 6 30, + cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3", + 23 ); + (* Entry 24 -- register line 349. *) + ( "24 Universal III-class feast", mk 2026 6 30, + cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3", + 24 ); + (* Entry 25 -- register line 350. *) + ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25); + (* Entry 26 -- register line 350. *) + ( "26 III-class vigil", mk 2026 8 9, + cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", + 26 ); + (* Entry 27 -- register line 352: an otherwise-unoccupied IV-class + Saturday. *) + ( "27 Office of the BVM on Saturday", off 62, + cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", + 27 ); + (* Entry 28 -- register line 352: the unqualified IV-class catch-all. *) + ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 28); + (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which + is not the Nativity or Pentecost (entries 5/9, the only I-class + vigils the table names) and so has no entry to fall into. Proves the + documented fallback -- not entry 11/12/13, which the [not is_vigil] + guard exists specifically to keep this out of. *) + ( "unclassified: I-class vigil outside Nativity/Pentecost", mk 2026 3 10, + cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-mystery-vigil", + PE.unclassified ); + (* RG 91's own vigil list (register lines 381-384) stops at III class -- + there is no IV-class vigil for entry 28's ferial catch-all to absorb. *) + ( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20, + cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified ) + ] + +let suite = + ( "Precedence_ef", + List.map + (fun (desc, date, c, expect) -> + Alcotest.test_case desc `Quick (fun () -> + Alcotest.(check int) desc expect (PE.band (ctx date) c))) + cases ) |
