summaryrefslogtreecommitdiff
path: root/lib/render/emit_csv.ml
blob: e04211f9c1825d7f4c91a9ff9d257ba82369c4d0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(* emit_csv.ml *)
module T = Template

let escape_field s =
  let needs =
    String.exists (fun c -> c = ',' || c = '"' || c = '\n' || c = '\r') s
  in
  if not needs then s
  else begin
    let b = Buffer.create (String.length s + 8) in
    Buffer.add_char b '"';
    String.iter (fun c -> if c = '"' then Buffer.add_string b "\"\"" else Buffer.add_char b c) s;
    Buffer.add_char b '"';
    Buffer.contents b
  end

let get v k = match v with T.Obj kvs -> List.assoc_opt k kvs | _ -> None
let s v k = match get v k with Some (T.Str x) -> x | _ -> ""

(* [name] is now the single RESOLVED display string (view.ml), not a
   lang-keyed object -- the [name_la]/[name_en] pair this replaces carried
   the kernel's own [Celebration.names], a different, unlocalised source.
   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
   [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
   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" ]

let row ~rite d =
  let comms =
    match get d "comms" with
    | 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 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_char b '\n';
  List.iter (fun d -> Buffer.add_string b (row ~rite d); Buffer.add_char b '\n') days;
  Buffer.contents b