aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/rites/rite_ef/temporal_ef.ml48
-rw-r--r--lib/rites/rite_ef/temporal_ef.mli12
-rw-r--r--test/test_temporal_ef.ml39
3 files changed, 98 insertions, 1 deletions
diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml
new file mode 100644
index 0000000..c975316
--- /dev/null
+++ b/lib/rites/rite_ef/temporal_ef.ml
@@ -0,0 +1,48 @@
+(* EF (1962) temporal cycle. Every boundary and rank rule cites its Rubricae
+ Generales paragraph; see docs/research/rules-register.md §3-§4. *)
+
+open Colitur_kernel
+open Vocab_ef
+
+let mk y m d =
+ match Date.make ~year:y ~month:m ~day:d with
+ | Ok t -> t
+ | Error e -> failwith ("temporal_ef: " ^ e)
+
+let weekday_index d =
+ match Date.weekday d with
+ | Date.Sun -> 0 | Date.Mon -> 1 | Date.Tue -> 2 | Date.Wed -> 3
+ | Date.Thu -> 4 | Date.Fri -> 5 | Date.Sat -> 6
+
+(* The Sunday on or before [d]. *)
+let sunday_on_or_before d = Date.add_days d (-(weekday_index d))
+
+(* RG 71: Advent I is the Sunday nearest 30 November -- equivalently the fourth
+ Sunday before Christmas, i.e. three weeks before the last Sunday on or before
+ 24 December. *)
+let advent_start y = Date.add_days (sunday_on_or_before (mk y 12 24)) (-21)
+let year_start = advent_start
+
+let before a b = Date.compare a b < 0
+let on_or_after a b = Date.compare a b >= 0
+
+(* RG 71-77. Tested in chronological order within the civil year. *)
+let season d =
+ let y = Date.year d in
+ let easter = Computus.gregorian_easter y in
+ let advent_this = advent_start y in
+ let christmas_this = mk y 12 25 in
+ let jan14 = mk y 1 14 in
+ let septuagesima_sunday = Date.add_days easter (-63) in
+ let ash_wednesday = Date.add_days easter (-46) in
+ let passion_sunday = Date.add_days easter (-14) in
+ let paschal_end = Date.add_days easter 55 in
+ if on_or_after d advent_this && before d christmas_this then Advent (* RG 71 *)
+ else if on_or_after d christmas_this then Christmastide (* RG 72-73: 25-31 Dec *)
+ else if before d jan14 then Christmastide (* RG 72-73: 1-13 Jan inclusive *)
+ else if before d septuagesima_sunday then Time_after_epiphany (* RG 77: from 14 Jan *)
+ else if before d ash_wednesday then Septuagesima (* RG 73 *)
+ else if before d passion_sunday then Lent (* RG 74 *)
+ else if before d easter then Passiontide (* RG 75; Holy Saturday included *)
+ else if Date.compare d paschal_end <= 0 then Paschaltide (* RG 76 *)
+ else Time_after_pentecost (* RG 77 *)
diff --git a/lib/rites/rite_ef/temporal_ef.mli b/lib/rites/rite_ef/temporal_ef.mli
new file mode 100644
index 0000000..7ee75e5
--- /dev/null
+++ b/lib/rites/rite_ef/temporal_ef.mli
@@ -0,0 +1,12 @@
+(** The EF (1962) temporal cycle, per Rubricae Generales 71-77 and 91. *)
+
+open Colitur_kernel
+
+(** RG 71: Advent I Sunday of the Advent opening in civil year [y]. *)
+val advent_start : int -> Date.t
+
+(** First day of the liturgical year opening in civil year [y]. Same as
+ {!advent_start}. *)
+val year_start : int -> Date.t
+
+val season : Date.t -> Vocab_ef.season
diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml
index f7851c1..eb672d9 100644
--- a/test/test_temporal_ef.ml
+++ b/test/test_temporal_ef.ml
@@ -22,7 +22,44 @@ let test_slug_words () =
(V.season_slug_word V.Christmastide);
Alcotest.(check string) "season name differs" "paschaltide" (V.season_to_string V.Paschaltide)
+module T = Rite_ef.Temporal_ef
+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 season_str dt = V.season_to_string (T.season dt)
+
+let test_advent_start () =
+ (* Advent 2026 begins Sunday 29 November 2026. *)
+ Alcotest.(check string) "advent 2026" "2026-11-29" (D.to_iso8601 (T.advent_start 2026));
+ Alcotest.(check bool) "always a Sunday" true (D.weekday (T.advent_start 2026) = D.Sun)
+
+let test_seasons () =
+ (* Easter 2026 is 5 April. *)
+ 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" "christmastide" (season_str (d 2026 12 25));
+ (* RG 72-73: Christmas Time runs to 13 January INCLUSIVE -- the deliberate
+ divergence from lectio, which starts time-after-epiphany at 6 January. *)
+ Alcotest.(check string) "6 Jan 2026" "christmastide" (season_str (d 2026 1 6));
+ Alcotest.(check string) "13 Jan 2026" "christmastide" (season_str (d 2026 1 13));
+ Alcotest.(check string) "14 Jan 2026" "time-after-epiphany" (season_str (d 2026 1 14));
+ Alcotest.(check string) "Septuagesima 1 Feb 2026" "septuagesima" (season_str (d 2026 2 1));
+ Alcotest.(check string) "Ash Wed 18 Feb 2026" "lent" (season_str (d 2026 2 18));
+ Alcotest.(check string) "Passion Sun 22 Mar 2026" "passiontide" (season_str (d 2026 3 22));
+ (* Holy Saturday stays in Passiontide: the Vigil is a night Mass (spec §4.2). *)
+ Alcotest.(check string) "Holy Sat 4 Apr 2026" "passiontide" (season_str (d 2026 4 4));
+ Alcotest.(check string) "Easter 5 Apr 2026" "paschaltide" (season_str (d 2026 4 5));
+ Alcotest.(check string) "Sat after Pentecost 30 May 2026" "paschaltide" (season_str (d 2026 5 30));
+ Alcotest.(check string) "Trinity 31 May 2026" "time-after-pentecost" (season_str (d 2026 5 31))
+
+let suite_extra_seasons =
+ [ Alcotest.test_case "advent start" `Quick test_advent_start;
+ Alcotest.test_case "seasons" `Quick test_seasons ]
+
let suite =
( "Rite_ef",
[ Alcotest.test_case "vocab roundtrips" `Quick test_vocab_roundtrips;
- Alcotest.test_case "slug words" `Quick test_slug_words ] )
+ Alcotest.test_case "slug words" `Quick test_slug_words ]
+ @ suite_extra_seasons )