diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/cli.t | 67 | ||||
| -rw-r--r-- | test/dune | 2 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 130 |
3 files changed, 196 insertions, 3 deletions
@@ -17,7 +17,7 @@ A year outside the supported domain is rejected (exit 2): No/garbage arguments give a usage error (exit 2): $ colitur - colitur: usage: colitur easter <year> | colitur temporal <year> + colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> [2] The EF temporal cycle for a year, one line per day: @@ -41,3 +41,68 @@ A year outside the supported domain is rejected (exit 2): $ colitur temporal 1000 colitur: year 1000 out of range 1583..9999 [2] + +The resolved EF calendar for a year (Task 11) -- temporal AND sanctoral, +occurrence and transfers applied: one line per civil-year day, +"YYYY-MM-DD weekday season week slug rank colour [+commemoration-slug]...". + + $ colitur day 2026 | wc -l + 365 + +Easter is the observed day exactly once, and carries no commemoration (an +impeded I class day admits at most one PRIVILEGED commemoration, RG 111, and +nothing outranks Easter to be impeded by it in the first place): + + $ colitur day 2026 | grep -c '^2026-04-05 ' + 1 + $ colitur day 2026 | grep '^2026-04-05 ' + 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white + +Ash Wednesday: I class (RG 91 entry 7), violet, no numbered week (it falls 4 +days before Lent I's own origin -- rite_ef/temporal_ef.ml's [week]): + + $ colitur day 2026 | grep '^2026-02-18 ' + 2026-02-18 wednesday lent - ef-ash-wednesday class-1 violet + +Christmas: I class, white (RG 91 entry 1): + + $ colitur day 2026 | grep '^2026-12-25 ' + 2026-12-25 friday christmastide - ef-nativity class-1 white + +All Souls (2 Nov, RG 91 entry 8) end to end: 2 Nov 2025 is a Sunday (verified +independently -- 1 Jan 2025 is a Wednesday, day-of-year 306, (3+305) mod 7 = +0 = Sunday), so entry 8 yields to it (still an ordinary II-class Sunday, +"sunday" in season time-after-pentecost, colour green -- the week number +itself is not re-asserted here, already covered by test_temporal_ef.ml); RG +95 then transfers All Souls (I class, not a Sunday, not a vigil) and RG 96 +places it on 3 Nov, the next day that is not I or II class: + + $ colitur day 2025 | grep -c 'commemoration-of-all-souls' + 1 + $ colitur day 2025 | grep '^2025-11-02 ' | sed -E 's/ [0-9]+ ef-time-after-pentecost-sunday-[0-9]+ / <week> ef-time-after-pentecost-sunday-<n> /' + 2025-11-02 sunday time-after-pentecost <week> ef-time-after-pentecost-sunday-<n> class-2 green + $ colitur day 2025 | grep '^2025-11-03 ' + 2025-11-03 monday time-after-pentecost 21 commemoration-of-all-souls class-1 black + +(week 21: Pentecost 2025 is 8 June (colitur easter 2025); 8 Jun - 3 Nov is +148 days, floor_div(148, 7) = 21 -- rite_ef/temporal_ef.ml's own [week] +formula, hand-verified before promoting this line, not merely printed and +trusted.) + +All Souls observed directly on 2 Nov in a year where it does not fall on a +Sunday: 2 Nov 2026 is a Monday (1 Jan 2026 is a Thursday, same day-of-year +306 offset, (4+305) mod 7 = 1 = Monday): + + $ colitur day 2026 | grep -c 'commemoration-of-all-souls' + 1 + $ colitur day 2026 | grep '^2026-11-02 ' + 2026-11-02 monday time-after-pentecost 23 commemoration-of-all-souls class-1 black + +(week 23: Pentecost 2026 is 24 May (colitur easter 2026); 24 May - 2 Nov is +162 days, floor_div(162, 7) = 23 -- same formula, same independent check.) + +A year outside the supported domain is rejected (exit 2): + + $ colitur day 1000 + colitur: year 1000 out of range 1583..9999 + [2] @@ -6,4 +6,4 @@ (pps ppx_sexp_conv))) (cram - (deps %{bin:colitur})) + (deps %{bin:colitur} ../data/ef/sanctoral.sexp ../data/ef/adjustments.sexp)) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index a95b321..084d708 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -222,6 +222,22 @@ let cases = ( "26 III-class vigil (non-base layer)", mk 2026 8 10, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil", 26 ); + (* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp) + names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not + [PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly + the mismatch Task 7's review predicted. These two rows use the real + bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug), + proving [band] recognises the prefix convention too: without it, both + would misfile at 16/24 (an ordinary feast of the same rank) instead + of 21/26. *) + ( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", + mk 2026 6 28, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul", + 21 ); + ( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", + mk 2026 8 9, + cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", + 26 ); (* Entry 27 -- register line 352: an otherwise-unoccupied IV-class Saturday. *) ( "27 Office of the BVM on Saturday", off 62, @@ -370,6 +386,21 @@ let disposition_cases = an_ordinary_sunday, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", "Commemorate(Ordinary)" ); + (* Task 11, issue (a): [disposition]'s own [is_vigil] check (the RG 33 + omission test) is a SEPARATE call site from [band]'s -- both read the + same private [is_vigil], but each needed its own witness, since a fix + to one call site could in principle miss the other. Real bootstrapped + slug (data/ef/sanctoral.sexp's "vigil-of-the-assumption", 14 Aug), + not a hand-typed one, for the same coupling-safety reason [of_temporal] + rows use real data elsewhere in this file. Before the fix this vigil + was invisible to [is_vigil] entirely, so it would have fallen through + to the ordinary Commemorate branch below instead of Omit -- the exact + failure the task brief describes. *) + ( "RG33 (prefix convention): a \"vigil-of-X\"-named II-class vigil loses \ + to an ordinary Sunday -> Omit", + an_ordinary_sunday, + cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-the-assumption", + "Omit" ); (* Brief: a Commemoration_only loser is ALWAYS Commemorate -- checked here with a loser that ALSO carries a Class1 rank and a vigil-suffixed slug losing to a Sunday, so this row only passes if the @@ -778,6 +809,94 @@ let test_all_souls_transfers_end_to_end () = Alcotest.(check int) "nothing commemorated" 0 (List.length resolution.P.commemorations); Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted) +(* Task 11: [PE.transfer_target] -- RG 96 ("the next following day that is + not I or II class") plus its Annunciation exception. [occupant] is a + synthetic callback ({!Colitur_kernel.Rite.t.transfer_target}'s own + [occupant] parameter), not a real [Calendar]-driven one -- the CLI's own + end-to-end proof (colitur day, All Souls landing on 3 Nov 2025 and the + Annunciation landing on 5 Apr 2027, see test/cli.t and the task report) + is what wires this against real data; these rows isolate the search + function itself. *) + +(* [blocked] returns Class1 (blocking) for exactly the listed dates, Class4 + (not blocking) everywhere else -- enough to exercise [is_blocking]'s own + two-way test (RG 96 speaks of I OR II class; Class1 alone is enough to + prove the blocking side, [test_transfer_target_terminates...] below adds + nothing by varying it further). *) +let occupant_blocking_on blocked_dates (d : D.t) : V.rank Cel.t = + let blocking = List.exists (fun bd -> D.compare bd d = 0) blocked_dates in + Cel.make ~slug:(S.of_string_exn "occupant") ~rank:(if blocking then V.Class1 else V.Class4) + ~colour:Col.Green ~layer:"synthetic" () + +let occupant_always_blocking (_ : D.t) : V.rank Cel.t = + Cel.make ~slug:(S.of_string_exn "occupant") ~rank:V.Class1 ~colour:Col.Green ~layer:"synthetic" () + +(* General RG 96 search: two consecutive blocked days past [origin], proving + the search walks past MORE than one ineligible day rather than only + trying [origin + 1] and stopping (the same shape Calendar's own + synthetic fixture pins for the abstraction -- this pins it for the real + EF search function). *) +let test_transfer_target_general_multi_step_search () = + let origin = mk 2026 1 10 in + let occupant = occupant_blocking_on [ mk 2026 1 11; mk 2026 1 12 ] in + let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-some-i-class-feast" in + let target = PE.transfer_target c origin occupant in + Alcotest.(check string) "lands on the first day past the blocked run" + "2026-01-13" (D.to_iso8601 target) + +(* RG 96's Annunciation exception: starts the search at the Monday after Low + Sunday, NOT [origin + 1] -- occupant is unconditionally free, so a + general-path implementation would return [origin + 1] (26 March), a date + this test explicitly rules out as well as pinning the real expected one, + so the assertion genuinely discriminates the two starting points rather + than merely checking "some date after origin". *) +let test_transfer_target_annunciation_starts_at_monday_after_low_sunday () = + let origin = mk 2026 3 25 in + let occupant = occupant_blocking_on [] in + let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in + let target = PE.transfer_target c origin occupant in + let monday_after_low_sunday = D.add_days (Comp.gregorian_easter 2026) 8 in + Alcotest.(check string) "lands on the Monday after Low Sunday (Easter + 8)" + (D.to_iso8601 monday_after_low_sunday) (D.to_iso8601 target); + Alcotest.(check bool) "NOT the general path's origin + 1 (discriminates the branch)" true + (D.compare target (D.add_days origin 1) <> 0) + +(* RG 96's own qualifier on the exception -- "searching onward from there + only if that day is itself blocked" (rite.mli) -- is [search_from]'s + ORDINARY behaviour, not a second mechanism: block the Monday after Low + Sunday itself and confirm the search continues exactly one more day. *) +let test_transfer_target_annunciation_searches_onward_if_blocked () = + let origin = mk 2026 3 25 in + let monday_after_low_sunday = D.add_days (Comp.gregorian_easter 2026) 8 in + let occupant = occupant_blocking_on [ monday_after_low_sunday ] in + let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in + let target = PE.transfer_target c origin occupant in + Alcotest.(check string) "searches onward one more day when that Monday is itself blocked" + (D.to_iso8601 (D.add_days monday_after_low_sunday 1)) (D.to_iso8601 target) + +(* rite.mli's own obligations on [transfer_target] (Task 11 brief): the call + must TERMINATE and its result must be STRICTLY AFTER [origin], even for a + rite/data shape this function cannot have anticipated -- an occupant that + reports every single day as blocking, forever. Calendar's own round guard + (max_transfer_rounds) does not cover this: it bounds ROUNDS across a + whole year, not the internal walk one call to this function makes (see + precedence_ef.ml's own comment on [search_from] and [max_search_days]). + Deliberately NOT pinning the exact returned date against the private + [max_search_days] constant -- that would coalesce a behavioural contract + (terminates, makes forward progress) with an internal tuning value this + function is free to change; a generous, test-owned ceiling (1000 days, + comfortably past any realistic bound) is enough to prove termination is + genuine and not merely "didn't hang during this particular run". *) +let test_transfer_target_terminates_under_pathological_occupant () = + let origin = mk 2026 1 1 in + let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-pathological-case" in + let target = PE.transfer_target c origin occupant_always_blocking in + Alcotest.(check bool) "strictly after origin" true (D.compare target origin > 0); + Alcotest.(check bool) + "terminates within a generous bound (proves the internal search is bounded, not merely lucky)" + true + (D.compare target (D.add_days origin 1000) <= 0) + let suite = ( "Precedence_ef", List.map @@ -819,4 +938,13 @@ let suite = test_privileged_lent_feria_admitted_end_to_end; Alcotest.test_case "II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end" - `Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end ] ) + `Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end; + Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" + `Quick test_transfer_target_general_multi_step_search; + Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" + `Quick test_transfer_target_annunciation_starts_at_monday_after_low_sunday; + Alcotest.test_case + "transfer_target: Annunciation exception searches onward if that Monday is blocked" `Quick + test_transfer_target_annunciation_searches_onward_if_blocked; + Alcotest.test_case "transfer_target: terminates and stays forward under a pathological occupant" + `Quick test_transfer_target_terminates_under_pathological_occupant ] ) |
