(* Resolution across a whole liturgical year. See calendar.mli for the architectural rationale (why [year] is the primitive and [day] derived). *) (* The kernel's domain floor and ceiling (Date.make's documented 1583..9999 bound). Both are always constructible -- in-range by definition -- so neither of these can itself raise. *) let domain_min_date = match Date.make ~year:1583 ~month:1 ~day:1 with Ok d -> d | Error e -> failwith e let domain_max_date = match Date.make ~year:9999 ~month:12 ~day:31 with Ok d -> d | Error e -> failwith e (* [start, stop] for the liturgical year opening in civil year [y], clamped at both ends of the domain rather than calling [rite.year_start] on a civil year outside 1583..9999. Top: at [y] = 9999, [rite.year_start (y + 1)] would ask for civil year 10000 -- out of Date's domain (Plan 2 shipped exactly this bug in Validate). Clamp [stop] to 31 December 9999 instead: the final liturgical year comes back truncated, not un-computable. Bottom: symmetric case, reachable only through [day] below. A date in civil year 1583 before that year's own [rite.year_start] genuinely belongs to the liturgical year that opened in civil year 1582 for an Advent-anchored rite -- but [rite.year_start 1582] is equally out of domain. [day] only ever decrements a valid date's own (in-domain) civil year by at most one, so [y] = 1582 is the sole way this branch is reached. Clamp [start] to 1 January 1583: "year 1582" becomes the truncated stretch from the domain floor up to the day before [rite.year_start 1583], which is exactly the sliver a date there needs. *) let year_bounds (rite : ('s, 'r) Rite.t) (y : int) : Date.t * Date.t = let start = if y < 1583 then domain_min_date else rite.Rite.year_start y in let stop = if y >= 9999 then domain_max_date else Date.add_days (rite.Rite.year_start (y + 1)) (-1) in (start, stop) (* [resolution.deferred] (RG 96-98 transfer candidates) has nowhere to be PLACED yet -- Task 6 adds the fixed-point pass that does -- but it must still be accounted for on the day it lost, not silently dropped: Task 12's no-celebration-lost invariant reads [Liturgical_day.omitted], so a deferred candidate folds in there too, with its own reason distinct from Precedence's native omissions ("omitted: yielded to a higher day", "omitted: admission limit reached"). *) let deferred_reason = "deferred: transfer placement not yet implemented (Task 6)" (* RG 91's contest for one date: the temporal office against every sanctoral entry whose Date_spec resolves to it. [Layer.on_date] is keyed on exactly (month, day), which for a [Fixed] spec -- the only form Plan 2 ships -- is the same test as resolving the spec against [date]'s own year and comparing, so no separate filter is needed here. *) let resolve_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) (date : Date.t) : ('s, 'r) Liturgical_day.t = let temporal = rite.Rite.temporal date in let temporal_candidate = { Precedence.cel = temporal.Temporal.office; origin = Precedence.Temporal } in let sanctoral = Layer.on_date idx ~month:(Date.month date) ~day:(Date.day date) |> List.map (fun (e : 'r Layer.entry) -> { Precedence.cel = e.Layer.cel; origin = Precedence.Sanctoral }) in let ctx = { Precedence.date; season = temporal.Temporal.season; weekday = temporal.Temporal.weekday } in let resolution = Precedence.resolve rite.Rite.rules ctx ~temporal:temporal_candidate ~sanctoral in let omitted = List.map (fun (c, reason) -> (c.Precedence.cel, reason)) resolution.Precedence.omitted @ List.map (fun c -> (c.Precedence.cel, deferred_reason)) resolution.Precedence.deferred in { Liturgical_day.date; rite = rite.Rite.id; temporal; observed = resolution.Precedence.observed.Precedence.cel; commemorations = List.map (fun (c, p) -> (c.Precedence.cel, p)) resolution.Precedence.commemorations; transferred_in = None; transferred_out = None; omitted; citations = []; } let year (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) (y : int) : ('s, 'r) Liturgical_day.t array = let idx = Layer.index_by_date layer in let start, stop = year_bounds rite y in (* [max 0]: defends [Array.init] against a negative length, which would otherwise arise for a rite whose [year_start] lands exactly on the domain floor (start clamps to the same date, giving [stop] a day before it). Not reachable through [day] -- see calendar.mli -- but [year] is public, and a direct out-of-contract call must not raise either. *) let n = max 0 (Date.to_rata stop - Date.to_rata start + 1) in Array.init n (fun i -> resolve_day rite idx (Date.add_days start i)) let day (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) (date : Date.t) : ('s, 'r) Liturgical_day.t = let cy = Date.year date in let y = if Date.compare date (rite.Rite.year_start cy) >= 0 then cy else cy - 1 in let start, _ = year_bounds rite y in (year rite layer y).(Date.to_rata date - Date.to_rata start)