From 6559a9b19451ce82a7d71281e25899acbcee2d49 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 25 Aug 2026 15:35:59 +0200 Subject: feat(of): Phase 1 of the OF rite module โ€” vocab_of, temporal_of MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first code for a second rite. lib/rites/rite_of/vocab_of implements the OF's five-way rank vocabulary (Sollemnitas/Festum/Memoria_obligatoria/ Memoria_ad_libitum/Feria) and five-season vocabulary (Advent/Christmas/ Lent/Easter/Ordinary_time), each constructor cited against the 2002 Missale Romanum's Normae universales and the Tabula dierum liturgicorum. temporal_of implements season boundaries, week numbering โ€” including Ordinary Time's own two-block resumption, derived from the Missale's own "HEBDOMADA I/XXXIV 'per annum'" headings and empirically verified across the whole 1583-9999 domain โ€” named temporal days, and Sunday/ferial slugs. Both satisfy Colitur_kernel.Temporal.RITE with no kernel change. No Rite.t is assembled and no CLI wiring is added (Phase 2's own scope); lib/rites/rite_ef and data/ef are untouched, verified byte-identical against the v1.0.0 tag across a domain-spanning sample. test/test_temporal_of.ml adds unit tests for every boundary and named day plus seven QCheck properties (seasons contiguous, Ordinary Time weeks in 1..34, Advent always four Sundays, slug uniqueness, weekday agreement, determinism, consecutive liturgical years partition civil time), with a committed exhaustive sweep mirroring test_validate.ml's own convention. Full account, including the Ordinary Time derivation's evidence and a list of EF concepts that do not carry over, in .superpowers/sdd/of-phase1-report.md (untracked, gitignored). --- test/test_temporal_of.ml | 393 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 test/test_temporal_of.ml (limited to 'test/test_temporal_of.ml') diff --git a/test/test_temporal_of.ml b/test/test_temporal_of.ml new file mode 100644 index 0000000..7560413 --- /dev/null +++ b/test/test_temporal_of.ml @@ -0,0 +1,393 @@ +(* Phase 1 of the OF rite module: Vocab_of + Temporal_of. Modelled on + test_temporal_ef.ml's own shape (example-based unit tests for the named + days and boundaries, QCheck properties for the domain-wide invariants), + scoped to what Phase 1 actually delivers -- no Rite.t exists yet, so + these properties are written directly against Temporal_of rather than + through Colitur_kernel.Validate.run (which needs a full Rite.t bundle, + Phase 2's own deliverable). *) + +module V = Rite_of.Vocab_of +module T = Rite_of.Temporal_of +module D = Colitur_kernel.Date + +let d y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> Alcotest.failf "%s" e +let iso = D.to_iso8601 + +(* ---- vocabulary ---- *) + +let test_vocab_roundtrips () = + List.iter + (fun s -> Alcotest.(check bool) "season string roundtrip" true (V.season_of_string (V.season_to_string s) = Some s)) + V.seasons; + List.iter + (fun r -> Alcotest.(check bool) "rank string roundtrip" true (V.rank_of_string (V.rank_to_string r) = Some r)) + V.ranks; + Alcotest.(check int) "five seasons" 5 (List.length V.seasons); + Alcotest.(check int) "five ranks" 5 (List.length V.ranks) + +(* ---- season boundaries (Normae n. 28/30/33/40/44 -- see temporal_of.ml's + own citations) ---- *) + +let season_str dt = V.season_to_string (T.season dt) + +let test_advent_start () = + Alcotest.(check string) "advent 2026" "2026-11-29" (iso (T.advent_start 2026)); + Alcotest.(check bool) "always a Sunday" true (D.weekday (T.advent_start 2026) = D.Sun) + +let test_seasons () = + (* Easter 2026 = 5 April; Ash Wednesday = 18 Feb; Palm Sunday = 29 Mar. *) + Alcotest.(check string) "1 Dec 2026" "advent" (season_str (d 2026 12 1)); + Alcotest.(check string) "24 Dec 2026" "advent" (season_str (d 2026 12 24)); + Alcotest.(check string) "25 Dec 2026" "christmas" (season_str (d 2026 12 25)); + Alcotest.(check string) "1 Jan 2027 (Mary Mother of God)" "christmas" (season_str (d 2027 1 1)); + (* Baptism of the Lord 2027 = 10 Jan: Christmas Time runs through it inclusive. *) + Alcotest.(check string) "10 Jan 2027 (Baptism Sunday)" "christmas" (season_str (d 2027 1 10)); + Alcotest.(check string) "11 Jan 2027 (Monday after Baptism)" "ordinary-time" (season_str (d 2027 1 11)); + Alcotest.(check string) "Ash Wed 18 Feb 2026" "lent" (season_str (d 2026 2 18)); + Alcotest.(check string) "Palm Sunday 29 Mar 2026" "lent" (season_str (d 2026 3 29)); + (* Normae n. 30 makes Holy Week Lent's own week VI: Holy Thursday, Good + Friday and Holy Saturday all stay Lent (vocab_of.ml's own citation). *) + Alcotest.(check string) "Holy Thursday 2 Apr 2026" "lent" (season_str (d 2026 4 2)); + Alcotest.(check string) "Good Friday 3 Apr 2026" "lent" (season_str (d 2026 4 3)); + Alcotest.(check string) "Holy Saturday 4 Apr 2026" "lent" (season_str (d 2026 4 4)); + Alcotest.(check string) "Easter 5 Apr 2026" "easter" (season_str (d 2026 4 5)); + Alcotest.(check string) "Pentecost 24 May 2026" "easter" (season_str (d 2026 5 24)); + Alcotest.(check string) "day after Pentecost 25 May 2026" "ordinary-time" (season_str (d 2026 5 25)); + Alcotest.(check string) "last Sunday before Advent, 22 Nov 2026" "ordinary-time" (season_str (d 2026 11 22)) + +(* ---- the three movable Christmas-time Sundays ---- *) + +let test_baptism_of_the_lord () = + Alcotest.(check string) "2026" "2026-01-11" (iso (T.baptism_of_the_lord 2026)); + Alcotest.(check string) "2027" "2027-01-10" (iso (T.baptism_of_the_lord 2027)); + Alcotest.(check string) "2024 (6 Jan is a Saturday)" "2024-01-07" (iso (T.baptism_of_the_lord 2024)); + (* 2030: 6 January itself is a Sunday. Normae n. 38 says "dominica POST + diem 6 ianuarii" -- strictly after, never 6 January itself -- so the + Baptism jumps a full week to 13 January, not 6. *) + Alcotest.(check bool) "2030: 6 Jan is a Sunday" true (D.weekday (d 2030 1 6) = D.Sun); + Alcotest.(check string) "2030 (Epiphany itself a Sunday)" "2030-01-13" (iso (T.baptism_of_the_lord 2030)); + Alcotest.(check bool) "always a Sunday" true (D.weekday (T.baptism_of_the_lord 2026) = D.Sun) + +let test_holy_family () = + Alcotest.(check string) "2026 (26 Dec is a Sunday)" "2026-12-27" (iso (T.holy_family 2026)); + (* 2022: no Sunday falls 26-31 December that year -- the n. 35(a) fallback, + 30 December, fires. *) + Alcotest.(check bool) "2022: no Sunday 26-31 Dec" + (List.for_all (fun dd -> D.weekday (d 2022 12 dd) <> D.Sun) [ 26; 27; 28; 29; 30; 31 ]) + true; + Alcotest.(check string) "2022 fallback" "2022-12-30" (iso (T.holy_family 2022)) + +let test_second_sunday_of_christmas () = + Alcotest.(check (option string)) "2026" (Some "2026-01-04") (Option.map iso (T.second_sunday_of_christmas 2026)); + (* 2024: Baptism is already 7 January (6 Jan a Saturday), so no Sunday + falls in the 2-5 January window that year -- a genuine, uncovered + [None], with no fallback in the primary text. *) + Alcotest.(check (option string)) "2024 (none)" None (Option.map iso (T.second_sunday_of_christmas 2024)) + +let test_christ_the_king () = + Alcotest.(check string) "2026" "2026-11-22" (iso (T.christ_the_king 2026)); + Alcotest.(check bool) "always a Sunday" true (D.weekday (T.christ_the_king 2026) = D.Sun); + (* Normae n. 43's own heading (temporal_of.ml's own citation): always week 34. *) + Alcotest.(check (option int)) "week 34" (Some 34) (T.week (T.christ_the_king 2026)) + +(* ---- named days ---- *) + +let named_slug dt = match T.named dt with Some (_, slug, _, _) -> slug | None -> "" +let named_colour dt = match T.named dt with Some (_, _, c, _) -> Some c | None -> None +let named_rank dt = match T.named dt with Some (_, _, _, r) -> Some r | None -> None + +let test_named_feasts () = + (* Easter 2026 = 5 April. *) + Alcotest.(check string) "nativity vigil" "of-nativity-vigil" (named_slug (d 2026 12 24)); + Alcotest.(check string) "nativity" "of-nativity" (named_slug (d 2026 12 25)); + Alcotest.(check string) "mary mother of god" "of-mary-mother-of-god" (named_slug (d 2027 1 1)); + Alcotest.(check string) "epiphany" "of-epiphany" (named_slug (d 2027 1 6)); + Alcotest.(check string) "ash wednesday" "of-ash-wednesday" (named_slug (d 2026 2 18)); + Alcotest.(check string) "palm sunday" "of-palm-sunday" (named_slug (d 2026 3 29)); + Alcotest.(check string) "easter" "of-easter-sunday" (named_slug (d 2026 4 5)); + Alcotest.(check string) "ascension" "of-ascension" (named_slug (d 2026 5 14)); + Alcotest.(check string) "pentecost" "of-pentecost" (named_slug (d 2026 5 24)); + Alcotest.(check string) "trinity" "of-trinity" (named_slug (d 2026 5 31)); + Alcotest.(check string) "corpus christi" "of-corpus-christi" (named_slug (d 2026 6 4)); + Alcotest.(check string) "christ the king" "of-christ-the-king" (named_slug (d 2026 11 22)); + Alcotest.(check string) "an ordinary day is not named" "" (named_slug (d 2026 7 15)) + +let test_named_colours_and_ranks () = + (* IGMR 346(b): Palm/Passion Sunday and Pentecost are red, not their + season's own colour. *) + Alcotest.(check (option string)) "palm sunday is red" (Some "red") + (Option.map Colitur_kernel.Colour.to_string (named_colour (d 2026 3 29))); + Alcotest.(check (option string)) "pentecost is red" (Some "red") + (Option.map Colitur_kernel.Colour.to_string (named_colour (d 2026 5 24))); + Alcotest.(check (option string)) "nativity is white" (Some "white") + (Option.map Colitur_kernel.Colour.to_string (named_colour (d 2026 12 25))); + (* Ash Wednesday: rank Feria, not an elevated grade -- vocab_of.ml's own + top-of-file argument for why OF's rank vocabulary keeps grade and + precedence-table position separate, unlike EF's. *) + Alcotest.(check (option string)) "ash wednesday is Feria" (Some "feria") + (Option.map V.rank_to_string (named_rank (d 2026 2 18))); + Alcotest.(check (option string)) "christmas is Sollemnitas" (Some "sollemnitas") + (Option.map V.rank_to_string (named_rank (d 2026 12 25))) + +let test_easter_octave () = + (* Easter 2026 = 5 April; the octave's six weekdays are 6-11 April. *) + Alcotest.(check string) "easter monday" "of-easter-octave-day-2" (named_slug (d 2026 4 6)); + Alcotest.(check string) "easter saturday" "of-easter-octave-day-7" (named_slug (d 2026 4 11)); + Alcotest.(check (option string)) "all octave weekdays are Sollemnitas white" (Some "sollemnitas white") + (match T.named (d 2026 4 6) with + | Some (_, _, c, r) -> Some (V.rank_to_string r ^ " " ^ Colitur_kernel.Colour.to_string c) + | None -> None); + (* The closing Sunday (12 Apr) is deliberately NOT in [named] -- it falls + through to the generic Sunday-of-Easter slug, week 2. *) + Alcotest.(check string) "closing Sunday is not in named" "" (named_slug (d 2026 4 12)); + Alcotest.(check string) "closing Sunday's own slug" "of-easter-sunday-2" + (match T.sunday_slug (d 2026 4 12) with Some s -> s | None -> "") + +(* ---- week numbering ---- *) + +let test_week_numbers () = + Alcotest.(check (option int)) "Advent I 2026" (Some 1) (T.week (T.advent_start 2026)); + Alcotest.(check (option int)) "Advent IV 2026" (Some 4) (T.week (D.add_days (T.advent_start 2026) 21)); + Alcotest.(check (option int)) "Christmas has no week" None (T.week (d 2026 12 27)); + (* Lent I Sunday = Easter - 42 = 2026-02-22; Ash Wednesday and the three + days after it belong to no numbered week. *) + Alcotest.(check (option int)) "Ash Wednesday has no week" None (T.week (d 2026 2 18)); + Alcotest.(check (option int)) "Lent I Sunday is week 1" (Some 1) (T.week (d 2026 2 22)); + (* Normae n. 30: Holy Week is Lent's own week VI. *) + Alcotest.(check (option int)) "Palm Sunday is Lent week 6" (Some 6) (T.week (d 2026 3 29)); + Alcotest.(check (option int)) "Holy Saturday is Lent week 6" (Some 6) (T.week (d 2026 4 4)); + Alcotest.(check (option int)) "Easter Sunday is Easter week 1" (Some 1) (T.week (d 2026 4 5)); + Alcotest.(check (option int)) "Pentecost is Easter week 8" (Some 8) (T.week (d 2026 5 24)) + +(* ---- Ordinary Time's own resumption arithmetic: the hard part, pinned + concretely (temporal_of.ml's own [ordinary_time_week] citation has the + full derivation and its evidence). ---- *) + +let test_ordinary_time_block_a () = + (* 2026: Baptism Sunday 11 Jan is week 1's own origin; the Missale's own + heading ("Dominica prima 'per annum' fit festum Baptismatis Domini") + puts that Sunday itself at week 1, so the following Monday (12 Jan, + the first day actually IN the Ordinary Time season) is ALSO week 1, + and the next Sunday (18 Jan, "Dominica II 'per annum'") is week 2. *) + Alcotest.(check (option int)) "12 Jan 2026 (Mon after Baptism) is week 1" (Some 1) (T.week (d 2026 1 12)); + Alcotest.(check (option int)) "18 Jan 2026 (Dominica II) is week 2" (Some 2) (T.week (d 2026 1 18)); + (* Ash Wednesday 2026 = 18 Feb; Shrove Tuesday (17 Feb) is block A's own + last day, week 6. *) + Alcotest.(check (option int)) "17 Feb 2026 (Shrove Tuesday) is week 6" (Some 6) (T.week (d 2026 2 17)) + +(* Every day of CIVIL year [y] -- deliberately not a liturgical-year walk + ([T.year_start y] through [T.year_start (y + 1)]): that span crosses into + the FOLLOWING civil year's January-onward Ordinary Time, governed by a + DIFFERENT year's Easter, which would silently test the wrong year's own + resumption arithmetic. *) +let civil_year_days y = + let start = d y 1 1 in + List.init (if Colitur_kernel.Date.is_leap y then 366 else 365) (fun i -> D.add_days start i) + +let test_ordinary_time_resumption () = + (* 2026: block A tops out at week 6; Pentecost is 24 May; the Monday + after (25 May) resumes at week 8, SKIPPING week 7 entirely that + year -- a concrete, verified instance of Normae n. 43's own "thirty- + three OR thirty-four weeks". *) + Alcotest.(check (option int)) "2026: 25 May (Mon after Pentecost) resumes at week 8" (Some 8) + (T.week (d 2026 5 25)); + Alcotest.(check bool) "2026: Ordinary Time week 7 never occurs (Easter's own week 7, 17-23 May, is a + different axis and does occur that year -- this checks only Ordinary-Time-season days)" true + (List.for_all + (fun dt -> T.season dt <> V.Ordinary_time || T.week dt <> Some 7) + (civil_year_days 2026)); + (* 2024: block A tops out at week 6 (Ash Wed 14 Feb, Shrove Tue 13 Feb); + Pentecost is 19 May; the Monday after (20 May) resumes at week 7 -- + NO skip that year, all 34 weeks used. *) + Alcotest.(check (option int)) "2024: 13 Feb (Shrove Tuesday) is week 6" (Some 6) (T.week (d 2024 2 13)); + Alcotest.(check (option int)) "2024: 20 May (Mon after Pentecost) resumes at week 7" (Some 7) + (T.week (d 2024 5 20)); + Alcotest.(check bool) "2024: every Ordinary Time week 1..34 occurs somewhere in civil 2024" true + (let weeks = + List.filter_map (fun dt -> if T.season dt = V.Ordinary_time then T.week dt else None) (civil_year_days 2024) + in + List.for_all (fun n -> List.mem n weeks) (List.init 34 (fun i -> i + 1))) + +(* ---- Sunday and ferial slugs ---- *) + +let sunday_slug_of dt = match T.sunday_slug dt with Some s -> s | None -> "" +let slug_of dt = match T.temporal dt with t -> Colitur_kernel.Slug.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.slug + +let test_sunday_slugs () = + Alcotest.(check string) "Advent I 2026" "of-advent-sunday-1" (sunday_slug_of (T.advent_start 2026)); + Alcotest.(check string) "Lent I 2026" "of-lent-sunday-1" (sunday_slug_of (d 2026 2 22)); + Alcotest.(check string) "18 Jan 2026 (Dominica II per annum)" "of-ordinary-time-sunday-2" (sunday_slug_of (d 2026 1 18)); + Alcotest.(check string) "holy family 2026" "of-holy-family" (sunday_slug_of (T.holy_family 2026)); + Alcotest.(check string) "baptism of the lord 2026" "of-baptism-of-the-lord" (sunday_slug_of (T.baptism_of_the_lord 2026)); + Alcotest.(check string) "2nd sunday of christmas 2026" "of-christmas-sunday-2" + (sunday_slug_of (Option.get (T.second_sunday_of_christmas 2026))) + +let test_ferial_slugs () = + Alcotest.(check string) "26 Dec 2026 (stretch 0)" "of-christmas-0-saturday" (slug_of (d 2026 12 26)); + Alcotest.(check string) "2 Jan 2027 (stretch 1)" "of-christmas-1-saturday" (slug_of (d 2027 1 2)); + Alcotest.(check string) "7 Jan 2027 (stretch 2)" "of-christmas-2-thursday" (slug_of (d 2027 1 7)); + Alcotest.(check string) "after-ashes Thursday 2026" "of-lent-after-ashes-thursday" (slug_of (d 2026 2 19)); + Alcotest.(check string) "ordinary weekday" "of-ordinary-time-2-monday" (slug_of (d 2026 1 19)); + Alcotest.(check string) "lent weekday" "of-lent-6-monday" (slug_of (d 2026 3 30)) + +let test_colours () = + let colour_of dt = (T.temporal dt).Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.colour in + Alcotest.(check string) "Good Friday is red" "red" (Colitur_kernel.Colour.to_string (colour_of (d 2026 4 3))); + Alcotest.(check string) "Holy Thursday is white" "white" (Colitur_kernel.Colour.to_string (colour_of (d 2026 4 2))); + Alcotest.(check string) "ordinary time weekday is green" "green" + (Colitur_kernel.Colour.to_string (colour_of (d 2026 1 19))); + Alcotest.(check string) "advent weekday is violet" "violet" (Colitur_kernel.Colour.to_string (colour_of (d 2026 12 1))); + Alcotest.(check string) "gaudete sunday is rose" "rose" + (Colitur_kernel.Colour.to_string (colour_of (D.add_days (T.advent_start 2026) 14))); + Alcotest.(check string) "laetare sunday is rose" "rose" + (Colitur_kernel.Colour.to_string (colour_of (D.add_days (Colitur_kernel.Computus.gregorian_easter 2026) (-21)))) + +(* ---- properties over the whole domain ---- *) + +(* Walk one liturgical year, [T.year_start y] through the day before + [T.year_start (y + 1)]. Callers must keep [y] within 1583..9998 -- + [T.year_start (y + 1)] calls [Date.make] internally (via [mk]) and + raises for civil year 10000, out of the kernel's domain, the same + reason Colitur_kernel.Validate.run's own walk clamps at 9999. *) +let walk_year y = + let start = T.year_start y in + let stop = D.add_days (T.year_start (y + 1)) (-1) in + let rec go dt acc = if D.compare dt stop > 0 then List.rev acc else go (D.add_days dt 1) ((dt, T.temporal dt) :: acc) in + go start [] + +let compressed_seasons days = + List.fold_left + (fun acc (_, t) -> match acc with s :: _ when s = t.Colitur_kernel.Temporal.season -> acc | _ -> t.Colitur_kernel.Temporal.season :: acc) + [] days + |> List.rev + +let expected_season_runs = [ V.Advent; V.Christmas; V.Ordinary_time; V.Lent; V.Easter; V.Ordinary_time ] + +(* Normae n. 43-44: seasons contiguous, no gaps or overlaps, over the whole + liturgical year -- the run-length-compressed sequence is always these + six runs, in this order (Ordinary Time genuinely appears twice, per + design 2026-08-24-colitur-of-rite-module-design.md ยง4.2). *) +let prop_season_runs_contiguous y = compressed_seasons (walk_year y) = expected_season_runs + +(* Ordinary Time weeks are always 1..34 (Normae n. 43), and every + Ordinary-Time-season day carries a week number at all (the two-block + arithmetic never silently drops one). *) +let prop_ordinary_time_week_in_range y = + List.for_all + (fun (_, t) -> + match t.Colitur_kernel.Temporal.season with + | V.Ordinary_time -> ( match t.Colitur_kernel.Temporal.week with Some n -> n >= 1 && n <= 34 | None -> false) + | _ -> true) + (walk_year y) + +(* Advent always has exactly four Sundays (Normae n. 41). *) +let prop_advent_four_sundays y = + List.length + (List.filter + (fun (dt, t) -> t.Colitur_kernel.Temporal.season = V.Advent && D.weekday dt = D.Sun) + (walk_year y)) + = 4 + +(* No two dates within one liturgical year share a slug (spec's own + requirement 3; Colitur_kernel.Validate's own ["slugs"] check, for EF, + asserts the identical thing once a Rite.t exists -- this is that same + invariant, checked directly here since Phase 1 has no Rite.t yet). *) +let prop_slug_uniqueness y = + let slugs = + List.map (fun (_, t) -> Colitur_kernel.Slug.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.slug) (walk_year y) + in + List.length slugs = List.length (List.sort_uniq String.compare slugs) + +(* [Temporal.t.weekday] always agrees with [Date.weekday] of the date it was + computed for. *) +let prop_weekday_agrees y = List.for_all (fun (dt, t) -> t.Colitur_kernel.Temporal.weekday = D.weekday dt) (walk_year y) + +(* [temporal] is a pure, deterministic function: calling it twice on the + same date gives structurally equal results. *) +let prop_determinism y = List.for_all (fun (dt, t) -> T.temporal dt = t) (walk_year y) + +(* The year is covered exactly once: three consecutive liturgical years' + walks, concatenated and sorted by date, form one strictly consecutive + run with no duplicate and no gap -- catches an off-by-one in + [advent_start]/[season] that a single year's own walk could not (it + would show up only as a wrong boundary DATE within that walk, not as a + missing or doubled civil day, since [walk_year] itself always produces a + contiguous sequence by construction). *) +let prop_liturgical_years_partition_civil_time y = + let dates = List.concat_map (fun yy -> List.map fst (walk_year yy)) [ y - 1; y; y + 1 ] in + let sorted = List.sort D.compare dates in + let rec check = function + | a :: (b :: _ as rest) -> D.to_rata b - D.to_rata a = 1 && check rest + | _ -> true + in + check sorted + +let year_gen = QCheck.int_range 1584 9997 (* leaves room for [y - 1] and [y + 1] *) + +let make_prop name f = QCheck.Test.make ~count:200 ~name year_gen f + +let prop_tests = + [ make_prop "OF: season runs are Advent,Christmas,Ordinary_time,Lent,Easter,Ordinary_time" prop_season_runs_contiguous; + make_prop "OF: Ordinary Time weeks are always 1..34" prop_ordinary_time_week_in_range; + make_prop "OF: Advent always has exactly four Sundays" prop_advent_four_sundays; + make_prop "OF: no two dates in one liturgical year share a slug" prop_slug_uniqueness; + make_prop "OF: temporal's weekday always agrees with Date.weekday" prop_weekday_agrees; + make_prop "OF: temporal is deterministic" prop_determinism; + make_prop "OF: consecutive liturgical years partition civil time with no gap or overlap" + prop_liturgical_years_partition_civil_time ] + +(* ---- the committed exhaustive sweep, mirroring test_validate.ml's own + COLITUR_EXHAUSTIVE_SWEEP convention: every year 1583..9998, not a + 200-year sample, gated behind the same environment variable so the + default `dune test` stays fast and reports the skip honestly. *) +let colitur_exhaustive_sweep_env = "COLITUR_EXHAUSTIVE_SWEEP" + +let test_exhaustive_domain_sweep () = + if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip () + else + for y = 1584 to 9997 do + let days = walk_year y in + if compressed_seasons days <> expected_season_runs then + Alcotest.failf "%d: season runs %s" y + (String.concat "," (List.map V.season_to_string (compressed_seasons days))); + if not (prop_ordinary_time_week_in_range y) then Alcotest.failf "%d: an Ordinary Time week is out of 1..34" y; + if not (prop_advent_four_sundays y) then Alcotest.failf "%d: Advent does not have exactly four Sundays" y; + if not (prop_slug_uniqueness y) then Alcotest.failf "%d: a slug repeats within the liturgical year" y; + if not (prop_weekday_agrees y) then Alcotest.failf "%d: a weekday disagrees with Date.weekday" y + done + +(* 1583 and 9999 themselves sit at the domain edges, where the [y-1]/[y+1] + probes the properties above use would call [T.year_start] on an + out-of-range civil year and raise -- covered separately here instead, + confirming [T.temporal] itself stays total right up to both edges. *) +let test_domain_edges_do_not_raise () = + let check_civil_year y = + let start = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> Alcotest.failf "%s" e in + for i = 0 to 364 do + ignore (T.temporal (D.add_days start i)) + done + in + check_civil_year 1583; + check_civil_year 9999 + +let suite = + ( "temporal-of", + [ Alcotest.test_case "vocab roundtrips" `Quick test_vocab_roundtrips; + Alcotest.test_case "advent start" `Quick test_advent_start; + Alcotest.test_case "seasons" `Quick test_seasons; + Alcotest.test_case "baptism of the lord" `Quick test_baptism_of_the_lord; + Alcotest.test_case "holy family" `Quick test_holy_family; + Alcotest.test_case "second sunday of christmas" `Quick test_second_sunday_of_christmas; + Alcotest.test_case "christ the king" `Quick test_christ_the_king; + Alcotest.test_case "named feasts" `Quick test_named_feasts; + Alcotest.test_case "named colours and ranks" `Quick test_named_colours_and_ranks; + Alcotest.test_case "easter octave" `Quick test_easter_octave; + Alcotest.test_case "week numbers" `Quick test_week_numbers; + Alcotest.test_case "ordinary time block A" `Quick test_ordinary_time_block_a; + Alcotest.test_case "ordinary time resumption" `Quick test_ordinary_time_resumption; + Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs; + Alcotest.test_case "ferial slugs" `Quick test_ferial_slugs; + Alcotest.test_case "colours" `Quick test_colours; + Alcotest.test_case "domain edges do not raise" `Quick test_domain_edges_do_not_raise; + Alcotest.test_case "exhaustive domain sweep" `Slow test_exhaustive_domain_sweep ] + @ List.map QCheck_alcotest.to_alcotest prop_tests ) -- cgit v1.3