diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/render/emit_csv.ml | 63 | ||||
| -rw-r--r-- | lib/render/emit_csv.mli | 4 |
2 files changed, 45 insertions, 22 deletions
diff --git a/lib/render/emit_csv.ml b/lib/render/emit_csv.ml index e04211f..ff11a81 100644 --- a/lib/render/emit_csv.ml +++ b/lib/render/emit_csv.ml @@ -23,26 +23,42 @@ let s v k = match get v k with Some (T.Str x) -> x | _ -> "" Machine formats still carry [slug] alongside it, so a script keeps the stable key and a human reads the name. - W5: deliberately NOT widened with a "second" column, unlike + W5 left this deliberately NOT widened with a "second" column, unlike [Emit_xml]/[Emit_ics] (each a repeated-element/free-text format that - grows for free). A CSV's header row is a CONTRACT with every row under + grows for free): a CSV's header row is a CONTRACT with every row under it (RFC 4180 sec. 2: "each field in the header... should contain the same number of fields as the records"), so there is no way to add a column only on the days that carry a [Second] reading -- a real - three-citation day would either need an ALWAYS-present, mostly-empty - "second" column (widening every EF row forever for a case EF can never - hit) or genuinely variable-width rows (not CSV). Neither is a change to - make speculatively: [bin/main.ml]'s [reject_rite_for] refuses [--rite] - on `emit` entirely for any rite but EF, so no [Second] reading can reach - this module today, and EF's own [citations] is always exactly - [[First; Gospel]] ({!Rite_ef}'s own [citation_shapes]) -- there is - nothing to widen FOR yet. When OF is admitted here, this is the one - emitter that needs a deliberate, disclosed breaking change (a new - column, a version bump, or a documented "third reading silently - dropped" trade-off), not a silent one. *) -let columns = - [ "date"; "rite"; "season"; "season_name"; "week"; "slug"; "name"; "weekday"; - "rank"; "rank_name"; "colour"; "colour_name"; "subject"; "first"; "gospel"; "comms" ] + three-citation day needs either an ALWAYS-present, mostly-empty + "second" column or genuinely variable-width rows (not CSV). At the + time, [bin/main.ml]'s [reject_rite_for] refused [--rite] on `emit` + entirely for any rite but EF, so no [Second] reading could reach this + module, and this was recorded as the deliberate, disclosed breaking + change the day OF was admitted here would need -- "a new column, a + version bump, or a documented trade-off, not a silent one". + + Fix 1 (cli-flags-report, 2026-08-27): that day is this one. The + "always-present, mostly-empty column" option is taken, gated on + [rite] (read from [v] itself, exactly as [row]'s own [~rite] already + is) rather than threaded as a new parameter: EF's [rite = "ef"] keeps + the ORIGINAL 16-column header and rows, byte for byte -- no [Second] + reading has ever reached EF's own [citations] + ({!Rite_ef}'s own [citation_shapes] is still exactly + [[First; Gospel]]), so [rite = "ef"]'s branch below is provably + unreachable-different, not merely untested-different. Any OTHER rite + (OF today) gets a 17th column, "second", spliced between "first" and + "gospel" -- the same position [Emit_xml]/[Emit_ics] already use for + it -- reading [s d "second"], which [get]'s own [None -> ""] fallback + already resolves to the empty string on every day that has no [Second] + part (a feria, feast or memorial; OLM 1981 Praenotanda n. 66.1/n. 69.1), + so nothing here needs to know in advance which OF days do carry one. *) +let columns ~rite = + let base = + [ "date"; "rite"; "season"; "season_name"; "week"; "slug"; "name"; "weekday"; "rank"; "rank_name"; + "colour"; "colour_name"; "subject"; "first" ] + in + let tail = [ "gospel"; "comms" ] in + if rite = "ef" then base @ tail else base @ [ "second" ] @ tail let row ~rite d = let comms = @@ -50,17 +66,20 @@ let row ~rite d = | Some (T.List l) -> String.concat " " (List.map (fun c -> s c "slug") l) | _ -> "" in - String.concat "," - (List.map escape_field - [ s d "iso"; rite; s d "season"; s d "season_name"; s d "week"; s d "slug"; - s d "name"; s d "weekday"; s d "rank"; s d "rank_name"; s d "colour"; - s d "colour_name"; s d "subject"; s d "first"; s d "gospel"; comms ]) + let base = + [ s d "iso"; rite; s d "season"; s d "season_name"; s d "week"; s d "slug"; s d "name"; + s d "weekday"; s d "rank"; s d "rank_name"; s d "colour"; s d "colour_name"; s d "subject"; + s d "first" ] + in + let tail = [ s d "gospel"; comms ] in + let fields = if rite = "ef" then base @ tail else base @ [ s d "second" ] @ tail in + String.concat "," (List.map escape_field fields) let year v = let rite = s v "rite" in let days = match get v "days" with Some (T.List l) -> l | _ -> [] in let b = Buffer.create (64 * 400) in - Buffer.add_string b (String.concat "," columns); + Buffer.add_string b (String.concat "," (columns ~rite)); Buffer.add_char b '\n'; List.iter (fun d -> Buffer.add_string b (row ~rite d); Buffer.add_char b '\n') days; Buffer.contents b diff --git a/lib/render/emit_csv.mli b/lib/render/emit_csv.mli index 262f660..cd7fd5a 100644 --- a/lib/render/emit_csv.mli +++ b/lib/render/emit_csv.mli @@ -6,4 +6,8 @@ double an embedded quote. Exposed for testing. *) val escape_field : string -> string +(** The header is [rite]-dependent (read from [v] itself, its own "rite" + key): EF's 16 columns are unchanged from before this field existed; + any other rite (OF) gets a 17th, "second", between "first" and + "gospel" -- see emit_csv.ml's own [columns] for the full argument. *) val year : Template.value -> string |
