(* The canonical flat output view (spec ยง2.4). Rite-parametric types stop here: every field is a string, so CSV, JSON and the template engine never see a type variable. *) type t = { date : string; rite : string; season : string; week : string; (** "" when the day is outside a numbered week *) weekday : string; slug : string; rank : string; colour : string; subject : string; names : (string * string) list; citations : (string * string) list; } let of_temporal ~rite vocab date (t : ('s, 'r) Temporal.t) = let cel = t.Temporal.office in { date = Date.to_iso8601 date; rite; season = vocab.Vocab.season_to_string t.Temporal.season; week = (match t.Temporal.week with Some n -> string_of_int n | None -> ""); weekday = Date.weekday_to_string t.Temporal.weekday; slug = Slug.to_string cel.Celebration.slug; rank = vocab.Vocab.rank_to_string cel.Celebration.rank; colour = Colour.to_string cel.Celebration.colour; subject = Subject.to_string cel.Celebration.subject; names = List.map (fun (l, n) -> (Lang.to_string l, n)) (Names.to_list cel.Celebration.names); citations = List.map (fun c -> (Citation.part_to_string c.Citation.part, c.Citation.reference)) cel.Celebration.citations; } (* Scalar columns, each with a (name, extractor) pair. Derived from this single source ensures headers and to_row cannot drift apart. Names and citations are excluded: they are variable-arity and belong to richer encoders in Plan 5. *) let columns = [ ("date", fun r -> r.date); ("rite", fun r -> r.rite); ("season", fun r -> r.season); ("week", fun r -> r.week); ("weekday", fun r -> r.weekday); ("slug", fun r -> r.slug); ("rank", fun r -> r.rank); ("colour", fun r -> r.colour); ("subject", fun r -> r.subject); ] let headers = List.map fst columns let to_row r = List.map (fun (_, get) -> get r) columns