diff options
Diffstat (limited to 'bin/main.ml')
| -rw-r--r-- | bin/main.ml | 137 |
1 files changed, 136 insertions, 1 deletions
diff --git a/bin/main.ml b/bin/main.ml index 896a6bd..53cae87 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -41,8 +41,142 @@ let temporal_report y = d := D.add_days !d 1 done +(* Task 11: the fully resolved EF calendar (temporal AND sanctoral, + occurrence and transfers applied), one line per civil-year day -- + "YYYY-MM-DD weekday season week slug rank colour [+commemoration-slug]...". + [temporal_report] above only ever showed the temporal cycle in isolation + ([Rite_ef.Temporal_ef.temporal] directly, no sanctoral layer, no + [Precedence] contest); this is the first CLI path that runs every piece + Plan 3 built -- [Colitur_kernel.Layer], [Overlay], [Precedence_ef], + [Calendar] -- against real data. *) + +(* [data/ef/sanctoral.sexp] and [data/ef/adjustments.sexp] are located + relative to the BUILD TREE, not the process's own cwd: cwd varies with + how the binary is invoked (a user's shell for `dune exec colitur --`, a + dune cram test's own sandboxed temp directory for `test/cli.t`) and + nothing in this project's build pins it to the repository root. A + build-time constant substituted via dune's [%{workspace_root}] was tried + first and rejected: it is resolved RELATIVE TO THE BUILD ACTION'S OWN + directory (empirically "." here, not an absolute path -- dune keeps + build actions relocatable), so it silently reproduces the same + cwd-dependence this is trying to eliminate, just baked in at build time + instead of read at run time; confirmed by the resulting `colitur day` + failing to find its own data outside the exact directory the build + happened to run in. + + [Sys.executable_name] does not have that problem -- on Linux it resolves + through /proc/self/exe, which the kernel always reports as the + executable's own canonical absolute path, even when the process was + launched through a symlink (verified against dune's own cram sandbox, + which places exactly such a symlink; see the task report). dune's default + ("no [(sandbox ...)] declared") build context mirrors the ENTIRE source + tree under _build/default/, unconditionally, so climbing from + _build/default/bin/main.exe up two directories and back down into data/ + always finds both files, regardless of the caller's own cwd. + + Known limitation, not yet exercised by this project: a `dune install`- + style deployment (executable copied to a prefix with no adjacent _build/ + default/data/) would need a different resolution strategy; there is no + install story yet (README.md: `dune exec` only), so this is not a + regression against anything this project currently supports. *) +let data_dir () = Filename.dirname (Filename.dirname Sys.executable_name) ^ "/data/ef" + +(* Loads the universal sanctoral layer and applies the one hand-authored + overlay over it (data/ef/adjustments.sexp -- see that file's own header): + [Overlay.apply]'s diagnostics are never silently dropped (Overlay.mli), + so any that come back -- expected to be none in the committed data; see + the overlay file's own comment on when one WOULD fire -- are printed to + stderr, loudly, without aborting the run. *) +let load_ef_layer () = + let dir = data_dir () in + let sanctoral_path = Filename.concat dir "sanctoral.sexp" in + let adjustments_path = Filename.concat dir "adjustments.sexp" in + match Colitur_kernel.Layer.load Rite_ef.Vocab_ef.rank_of_sexp sanctoral_path with + | Error e -> Error (Printf.sprintf "failed to load %s: %s" sanctoral_path e) + | Ok layer -> ( + match Colitur_kernel.Overlay.load Rite_ef.Vocab_ef.rank_of_sexp adjustments_path with + | Error e -> Error (Printf.sprintf "failed to load %s: %s" adjustments_path e) + | Ok overlay -> + let layer, diagnostics = Colitur_kernel.Overlay.apply layer overlay in + List.iter + (fun d -> Printf.eprintf "colitur: %s\n" (Colitur_kernel.Overlay.diagnostic_to_string d)) + diagnostics; + Ok layer) + +let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) + = + let t = d.Colitur_kernel.Liturgical_day.temporal in + let cel = d.Colitur_kernel.Liturgical_day.observed in + let week = + match t.Colitur_kernel.Temporal.week with Some n -> string_of_int n | None -> "-" + in + let commemoration_suffix (c, _) = + " +" ^ Colitur_kernel.Slug.to_string c.Colitur_kernel.Celebration.slug + in + let commemorations = + String.concat "" (List.map commemoration_suffix d.Colitur_kernel.Liturgical_day.commemorations) + in + Printf.printf "%s %s %s %s %s %s %s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) + (D.weekday_to_string t.Colitur_kernel.Temporal.weekday) + (Rite_ef.Vocab_ef.season_to_string t.Colitur_kernel.Temporal.season) + week + (Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug) + (Rite_ef.Vocab_ef.rank_to_string cel.Colitur_kernel.Celebration.rank) + (Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour) + commemorations + +(* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan -- + NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single + Advent-anchored liturgical year, which straddles two civil years, so a + civil year's worth of output needs the tail of the liturgical year that + opened the PREVIOUS civil year (covers roughly 1 Jan - 28 Nov) plus the + liturgical year that opens within this one (roughly 29 Nov - 31 Dec). + Both are computed once each -- not once per day via [Calendar.day], which + would recompute the whole (~365-day) placement pass up to 365 times over + for the days sharing one liturgical year (calendar.mli's own "pays it + once" cost model assumes exactly this usage: call [year], not [day] in a + loop). *) +let day_report y = + match load_ef_layer () with + | Error msg -> + Printf.eprintf "colitur: %s\n" msg; + exit 2 + | Ok layer -> + let module Cal = Colitur_kernel.Calendar in + let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t = + Hashtbl.create 400 + in + let index days = + Array.iter + (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) -> + Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d) + days + in + index (Cal.year Rite_ef.context layer (y - 1)); + index (Cal.year Rite_ef.context layer y); + let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in + let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in + let d = ref jan1 in + while D.compare !d dec31 <= 0 do + (match Hashtbl.find_opt by_rata (D.to_rata !d) with + | Some day -> day_line day + | None -> + (* Unreachable for any [y] in 1583..9999: the two indexed + liturgical years jointly cover [year_start (y-1), year_start + (y+1)), which contains all of civil year [y] + (calendar.mli). Not a [failwith] -- an out-of-domain [d] + inside this loop is impossible by construction (jan1/dec31 + are themselves validated in range, and [add_days] only ever + advances within the same civil year here) -- but a silent + skip would violate the same "never silently dropped" + standard the kernel holds itself to, so a gap surfaces + loudly on stderr rather than as a quietly short year. *) + Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d)); + d := D.add_days !d 1 + done + let usage () = - prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year>"; + prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year>"; exit 2 let with_year ys f = @@ -57,4 +191,5 @@ let () = match Sys.argv with | [| _; "easter"; ys |] -> with_year ys easter_report | [| _; "temporal"; ys |] -> with_year ys temporal_report + | [| _; "day"; ys |] -> with_year ys day_report | _ -> usage () |
