summaryrefslogtreecommitdiff
path: root/lib/kernel/calendar.mli
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 21:03:00 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 21:03:00 +0200
commitf15e44dd4c1b871c2daeb952b1c8c848274ea1f1 (patch)
tree2e2f73260780f43a9d9a3e38ed9c181f94c9d50b /lib/kernel/calendar.mli
parent953427d8d1e3a34994be53b60e18662ec26fef4e (diff)
downloadcolitur-f15e44dd4c1b871c2daeb952b1c8c848274ea1f1.tar.gz
colitur-f15e44dd4c1b871c2daeb952b1c8c848274ea1f1.zip
kernel(calendar): place transferred celebrations (RG 96-98)
Calendar.year now runs a placement pass after resolving every day: each deferred candidate (RG 95's I-class-only right of translation, via Precedence's Transfer disposition) is placed on the next day the rite's new Rite.t.transfer_target names as admissible, transferred_in/out are set on the two ends of the move, and the whole year is re-resolved to a fixed point, bounded by a hard max_transfer_rounds = 64 guard. transfer_target is rite-supplied rather than a generic search Calendar drives itself: RG 96's 'not I or II class' is not derivable from band or disposition alone (RG 91's own table lets a universal I-class feast outrank an ordinary Sunday in a raw contest, yet RG 96 forbids landing a translation there regardless), and the search's starting point is rite-specific too (the Annunciation exception). It takes an occupant callback exposing what Calendar currently resolves as observed on any date, so the rite never has to re-implement occurrence resolution. Two correctness properties drove most of the design: - A candidate's permanent natural loss at its own origin (the layer entry never moves) is rediscovered every round; left unfiltered this oscillates a placed candidate between two dates forever, since its own rank makes it look 'occupied' to a fresh search from its origin. Both the round loop's gather and the final per-day omitted accounting filter this out, keeping only sightings that are either brand new or losing at a candidate's *current* target (a fresh RG 97-98 bump). - RG 97-98's sort has to actually decide something, not just happen to agree with Precedence.resolve's own tie-break next round: a claimed-this-round overlay lets earlier-processed candidates in one round block later ones in the same pass, so two coinciding I-class feasts land on consecutive admissible days in the one round they collide, in band order. Also folds in Task 5's review finding: year_bounds clamps y to [1582, 9999] once, up front, rather than guarding start and stop independently (each guard only ever covered one of the two rite.year_start calls, leaving year 999 and year 100000 each able to call it out of domain through the other branch).
Diffstat (limited to 'lib/kernel/calendar.mli')
-rw-r--r--lib/kernel/calendar.mli29
1 files changed, 17 insertions, 12 deletions
diff --git a/lib/kernel/calendar.mli b/lib/kernel/calendar.mli
index 2469f2a..9fbd7e7 100644
--- a/lib/kernel/calendar.mli
+++ b/lib/kernel/calendar.mli
@@ -7,23 +7,28 @@
one pass -- and [day] is derived: it finds the liturgical year containing
a date and indexes into it. Both are pure; neither caches.
- This module resolves each day's temporal-vs-sanctoral contest but does
- not yet place deferred transfers (RG 96-98): a losing candidate the
- rite's rules send to [Precedence.Transfer] is not observed and not
- commemorated on the day it lost, and [transferred_in]/[transferred_out]
- both stay [None] everywhere -- but it is not silently dropped either. It
- lands in that day's [Liturgical_day.omitted] with the reason ["deferred:
- transfer placement not yet implemented (Task 6)"], alongside
- [Precedence]'s own native omissions (yielded to a higher day; admission
- limit reached), each with its own reason. Task 6 adds the fixed-point
- placement pass that actually places these; until then, this is the
- day's complete, honest accounting of what happened to every candidate. *)
+ Once every day's temporal-vs-sanctoral contest is resolved, [year] places
+ every deferred candidate (RG 96-98): a losing I-class candidate the
+ rite's rules send to [Precedence.Transfer] does not stay put -- it moves
+ to the next day [rite.transfer_target] names as admissible, and both
+ ends of the move are recorded ([transferred_in] on the day it arrives,
+ [transferred_out] on the day it left). Every deferred candidate is
+ accounted for exactly once: placed, or -- only if the placement fixed
+ point is not reached within the round guard, which nothing in the 1962
+ calendar is expected to trigger -- left in [Liturgical_day.omitted] with
+ a reason that says so, never silently dropped. See [calendar.ml]'s
+ [place_transfers] for the algorithm and its termination argument. *)
(** [year rite layer y] resolves every day of the liturgical year that opens
in civil year [y]: from [rite.year_start y] through the day before
[rite.year_start (y + 1)], inclusive of both ends.
- Total over 1583..9999, including the boundary years:
+ Total over 1583..9999, including the boundary years, and beyond them too:
+ [y] is clamped to [1582, 9999] before either bound is computed (not just
+ guarded near the two edges independently -- see [year_bounds] in
+ [calendar.ml] for why that distinction matters), so [year] never raises
+ regardless of the [y] it is given, not only for values near the domain
+ edge.
- At [y] = 9999, [rite.year_start (y + 1)] would ask for civil year
10000, out of {!Date}'s domain (this is the bug Plan 2 shipped in
[Validate] and later fixed). The end of the walk clamps to 31 December