summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cli.t67
-rw-r--r--test/dune2
-rw-r--r--test/test_precedence_ef.ml130
3 files changed, 196 insertions, 3 deletions
diff --git a/test/cli.t b/test/cli.t
index feb7c49..77968e6 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -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]
diff --git a/test/dune b/test/dune
index bb8e474..be24839 100644
--- a/test/dune
+++ b/test/dune
@@ -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 ] )