aboutsummaryrefslogtreecommitdiff
path: root/test/test_temporal_ef.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 08:11:26 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 08:11:26 +0200
commitbbc8ce26916ca92b3c2286c57342a6565323b66f (patch)
tree4a78f825d4896c8adff87c5782de16e55e6cc5a0 /test/test_temporal_ef.ml
parentbd7a41b5b83a5fc18e6169327c9176eee2d54a2b (diff)
downloadcolitur-bbc8ce26916ca92b3c2286c57342a6565323b66f.tar.gz
colitur-bbc8ce26916ca92b3c2286c57342a6565323b66f.zip
temporal(ef): the Holy Family, first Sunday after Epiphany (RG 17(b))
RG 17(b), verified against both photographic scans (missale-romanum- 1962.pdf and "Missale Romanum 1962_text.pdf" -- the electronic transcription carries the same paragraph, so this one is not itself a transcription-vs-scan divergence): the Holy Family of Jesus, Mary and Joseph is celebrated on the first Sunday after Epiphany, II class, EVERY year, taking the Sunday's own place "cum omnibus iuribus et privilegiis" -- the same formula RG 16(a) already uses. The slug, rank and colour temporal_ef.ml already computed for that Sunday (ef-time-after-epiphany-sunday-1, Class2, white) turn out to be exactly right by coincidence -- an ordinary, unnamed Sunday and Holy Family share both. The only field that was silently wrong is subject: always Temporal before this, when RG 91 entry 14 ("Festa Domini II classis, primum mobilia, deinde fixa") calls Holy Family a movable Feast of the Lord. temporal's build now takes an optional ~subject, applied only on Holy Family's own date -- every other Sunday, including the still-unbuilt Holy Name of Jesus (RG 17(a)), is unaffected. RG 17(b)'s window (7-13 January) can never be empty of a Sunday -- sunday_on_or_before(6 Jan) + 7 always lands in [7, 13] regardless of 6 January's weekday -- unlike RG 17(a)'s Holy Name (2-5 January, CAN be empty), whose calendarium entry carries an explicit "vel ea deficiente, die 2 ianuarii" fallback right next to Holy Family's own, fallback-free one. No fallback needed or built. Added to anchors (holy_family_sunday is independently computed, not routed through named -- see temporal_ef.ml's own comment on why) and to its own dedicated anchor/erosion test in test_temporal_ef.ml, kept separate from the generic named-days coverage test since this genuinely isn't one of named's own outputs. Does not yet touch precedence: band still has no notion of a movable Class2 Lord feast (entry 14 gates on sanctoral origin only), so on the seven years 13 January is itself Holy Family's date (2008, 2013, 2019, 2030, 2036, 2041, 2047), the fixed Commemoration of the Baptism of the Lord still wins the day exactly as before this commit -- the next commit fixes the ordering RG 91 entry 14's own text states ("primum mobilia, deinde fixa").
Diffstat (limited to 'test/test_temporal_ef.ml')
-rw-r--r--test/test_temporal_ef.ml116
1 files changed, 116 insertions, 0 deletions
diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml
index 751abd2..7837fb5 100644
--- a/test/test_temporal_ef.ml
+++ b/test/test_temporal_ef.ml
@@ -167,13 +167,95 @@ let test_resumed_sundays () =
module Cel = Colitur_kernel.Celebration
module Sl = Colitur_kernel.Slug
module Colr = Colitur_kernel.Colour
+module Sub = Colitur_kernel.Subject
let office dt = (T.temporal dt).Colitur_kernel.Temporal.office
let slug_of dt = Sl.to_string (office dt).Cel.slug
let rank_of dt = V.rank_to_string (office dt).Cel.rank
let colour_of dt = Colr.to_string (office dt).Cel.colour
+let subject_of dt = (office dt).Cel.subject
let temporal_week dt = (T.temporal dt).Colitur_kernel.Temporal.week
+(* RG 17(b) (Caput III, "De Dominicis"), primary text (scan-verified,
+ docs/research/rules-register.md §4): "festum S. Familiae Iesu, Mariae,
+ Ioseph, celebrandum dominica prima post Epiphaniam" -- the Holy Family is
+ celebrated on the first Sunday after Epiphany, EVERY year (unlike RG
+ 17(a)'s Holy Name, whose narrower 2-5 January window can be empty and
+ carries an explicit "vel ea deficiente, die 2 ianuarii" fallback in the
+ calendarium itself -- 7-13 January can never be empty of a Sunday, so
+ Holy Family needs, and the calendarium carries, no such fallback).
+ [T.holy_family_sunday] is [named]'s own [week_origin Time_after_epiphany]
+ formula, restated for its own citation -- not re-derived a second time.
+
+ Deliberately NOT part of [T.named]: [named] is checked before
+ [T.sunday_slug]'s own Christmastide branch in [T.temporal]'s dispatch, but
+ this feast's slug, rank and colour are EXACTLY what that branch already
+ computes for the 7-13 January Sunday (register's own pre-existing note on
+ [sunday_slug]: "the key stays lectio's") -- the only field that actually
+ needed to change is [subject] (silently always [Temporal] before this),
+ so [T.temporal] applies it as a targeted override inside that branch
+ rather than duplicating slug/rank/colour a second time through [named]'s
+ 4-tuple. *)
+let test_holy_family () =
+ (* 2026: Epiphany (6 Jan) is a Tuesday, so Holy Family falls 11 January --
+ an ordinary instance, no collision with the Baptism (13 Jan). *)
+ Alcotest.(check string) "2026: slug is the ordinary I-post-Epiphany Sunday key"
+ "ef-time-after-epiphany-sunday-1" (slug_of (d 2026 1 11));
+ Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 11));
+ Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 11));
+ Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II \
+ classis\" -- the mystery of the Holy Family touches Christ)"
+ true
+ (subject_of (d 2026 1 11) = Sub.Lord);
+ (* 2019: Epiphany itself is a Sunday, so the first Sunday AFTER it is the
+ LATEST possible date, 13 January -- the one date Holy Family collides
+ with the fixed Commemoration of the Baptism of the Lord (data/ef/
+ sanctoral.sexp: "commemoration-of-the-baptism-of-the-lord", also 13
+ Jan, II class, subject Lord). [T.temporal]'s own resolution of WHO wins
+ that day is {!Rite_ef.Precedence_ef}'s business (band/disposition, see
+ test_precedence_ef.ml), not this function's -- this only proves
+ [T.temporal] itself still identifies 13 January 2019 as Holy Family's
+ own date, unconditionally, regardless of what else might compete for
+ the day once precedence is applied. *)
+ Alcotest.(check bool) "2019: 6 January is a Sunday" true (D.weekday (d 2019 1 6) = D.Sun);
+ Alcotest.(check string) "2019: Holy Family falls on the latest possible date, 13 Jan"
+ "2019-01-13" (D.to_iso8601 (T.holy_family_sunday 2019));
+ Alcotest.(check string) "2019: still the same slug" "ef-time-after-epiphany-sunday-1"
+ (slug_of (T.holy_family_sunday 2019));
+ Alcotest.(check bool) "2019: still subject Lord" true (subject_of (T.holy_family_sunday 2019) = Sub.Lord);
+ (* Every possible weekday for 6 January exercised, proving the formula
+ never leaves the 7-13 window empty (register's own citation above) --
+ not merely the two years already checked above. *)
+ List.iter
+ (fun y ->
+ let hf = T.holy_family_sunday y in
+ Alcotest.(check bool) (Printf.sprintf "%d: Holy Family is a Sunday" y) true
+ (D.weekday hf = D.Sun);
+ Alcotest.(check bool) (Printf.sprintf "%d: Holy Family falls 7-13 January" y) true
+ (D.month hf = 1 && D.day hf >= 7 && D.day hf <= 13))
+ [ 2019; 2020; 2021; 2022; 2023; 2024; 2025; 2026; 2027 ];
+ (* Negative control: an ORDINARY Sunday after Epiphany (the 2nd) stays
+ [subject Temporal] -- the override is scoped to exactly one Sunday a
+ year, not every Time-after-Epiphany-shaped Sunday. *)
+ Alcotest.(check bool) "2026: the following Sunday (18 Jan, II after Epiphany) is NOT Lord"
+ true
+ (subject_of (d 2026 1 18) = Sub.Temporal)
+
+(* Three of [test_holy_family]'s own exhaustive-weekday sample, pinned to
+ their literal ISO dates and independently cross-checked against
+ `date -d <iso> +%A` (the same discipline test_golden.ml's own header
+ describes for its own weekday assertions) -- the earliest possible date
+ (Epiphany a Saturday), the latest (Epiphany a Sunday), and one ordinary
+ middle case, rather than trusting the structural "always 7-13" check
+ above on its own without ever anchoring it to real calendar dates. *)
+let test_holy_family_no_gap_year () =
+ Alcotest.(check string) "2024 (6 Jan a Saturday): earliest possible date, 7 Jan" "2024-01-07"
+ (D.to_iso8601 (T.holy_family_sunday 2024));
+ Alcotest.(check string) "2019 (6 Jan a Sunday): latest possible date, 13 Jan" "2019-01-13"
+ (D.to_iso8601 (T.holy_family_sunday 2019));
+ Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12"
+ (D.to_iso8601 (T.holy_family_sunday 2025))
+
(* Regression for a Task 14 Validate finding, reworked (register finding 4):
[named] no longer carries its own week at all -- [temporal] computes it
uniformly via [week] for every day, named or not -- so "a named day inside
@@ -432,6 +514,36 @@ let test_anchors_erosion_is_caught () =
[ "ef-ascension"; "ef-nativity" ]
(missing_from_anchors ~named_slugs ~anchors:eroded_anchors)
+(* Holy Family's own anchor coverage, kept SEPARATE from the two tests above:
+ it is genuinely NOT one of [T.named]'s outputs (test_holy_family's own
+ comment explains why -- [T.temporal] applies it as a targeted override
+ inside [T.sunday_slug]'s branch instead), so [named_slugs_for_year]'s scan
+ cannot see it, and folding it into [test_anchors_cover_all_named_days]/
+ [test_anchors_erosion_is_caught] would misrepresent what those two are
+ actually proving ("everything [named] produces"). This proves the same
+ two properties directly against [T.holy_family_sunday] instead: the entry
+ is present, AND deleting it is caught -- {!Validate}'s own production
+ anchor check (validate.ml, register/spec §5.7) calls [temporal], not
+ [named], so it is unaffected by this distinction and already covers this
+ entry regardless; this is this file's own unit-level guard against the
+ same erosion. *)
+let test_holy_family_anchor_present_and_erosion_is_caught () =
+ let y = 2026 in
+ let expected_slug = "ef-time-after-epiphany-sunday-1" in
+ let expected_date = T.holy_family_sunday y in
+ Alcotest.(check bool) "the anchor entry exists, at the right date" true
+ (List.mem (expected_slug, expected_date) (T.anchors y));
+ (* Erosion: with the entry struck out, [T.temporal] itself still puts
+ [expected_slug] at [expected_date] (unaffected -- deleting an [anchors]
+ row never touches [temporal]'s own computation, only what is CHECKED
+ against it), so a reader who only trusted [anchors] would no longer be
+ told to look there at all. *)
+ let eroded = List.filter (fun (s, dt) -> not (String.equal s expected_slug && dt = expected_date)) (T.anchors y) in
+ Alcotest.(check bool) "the erosion is caught: the entry no longer appears" false
+ (List.mem (expected_slug, expected_date) eroded);
+ Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion"
+ expected_slug (slug_of expected_date)
+
let test_totality () =
(* Every day of 2026 yields an office without raising. Not a slug
re-validation -- Slug.t is a private string validated on construction,
@@ -453,6 +565,8 @@ let suite_extra =
Alcotest.test_case "nativity octave" `Quick test_nativity_octave;
Alcotest.test_case "week numbers" `Quick test_week_numbers;
Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs;
+ Alcotest.test_case "holy family (RG 17(b))" `Quick test_holy_family;
+ Alcotest.test_case "holy family: no gap year (RG 17(b))" `Quick test_holy_family_no_gap_year;
Alcotest.test_case "week/sunday_slug agree" `Quick test_week_sunday_slug_agree;
Alcotest.test_case "resumed sundays" `Quick test_resumed_sundays;
Alcotest.test_case "ferial slugs and ranks" `Quick test_ferial_slugs_and_ranks;
@@ -465,6 +579,8 @@ let suite_extra =
Alcotest.test_case "totality" `Quick test_totality;
Alcotest.test_case "anchors cover all named days" `Quick
test_anchors_cover_all_named_days;
+ Alcotest.test_case "holy family anchor present, erosion caught" `Quick
+ test_holy_family_anchor_present_and_erosion_is_caught;
Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ]
let suite =