fmt/csv.bend

cswap-bend-cli@0.1.0.0 · 182 lines · 30 definitions

1import Base
2
3# CSV, RFC 4180 quoting. A field is quoted when it holds a comma, a double
4# quote, CR or LF; inside quotes a double quote is written twice. Records end
5# in LF (the parser takes LF or CRLF). Pure: no effects.
6
7# ---- emitter -----------------------------------------------------------------
8
9def special(+c: U32) -> Bool:
10  U32.is_eq(c, 44) || U32.is_eq(c, 34) || U32.is_eq(c, 10) || U32.is_eq(c, 13)
11
12def needs(s: String) -> Bool:
13  match s:
14    case SNil{}:
15      False{}
16    case SCon{Chr{c}, t}:
17      special(c) || needs(t)
18
19def dq(s: String) -> String:
20  match s:
21    case SNil{}:
22      ""
23    case SCon{Chr{+c}, t}:
24      Bool.pick(String, U32.is_eq(c, 34), "\"\"", SCon{Chr{c}, SNil{}}) ++ dq(t)
25
26def field.q(c: Bool, +s: String) -> String:
27  Bool.pick(String, c, "\"" ++ dq(s) ++ "\"", s)
28
29# One field, quoted when it must be.
30def field(+s: String) -> String:
31  field.q(needs(s), s)
32
33# One record, without the line end.
34def row(fs: List<&2, String>) -> String:
35  match fs:
36    case Nil{}:
37      ""
38    case Con{h, Nil{}}:
39      field(h)
40    case Con{h, Con{h2, t2}}:
41      field(h) ++ "," ++ row(h2 <> t2)
42
43# Records, each ending in LF.
44def table(rs: List<&2, List<&2, String>>) -> String:
45  match rs:
46    case Nil{}:
47      ""
48    case Con{r, t}:
49      row(r) ++ "\n" ++ table(t)
50
51# ---- parser ------------------------------------------------------------------
52
53type Mode is Data:
54  MStart{}
55  MPlain{}
56  MQuoted{}
57  MClosed{}
58
59# rows and row are reversed; cur is the reversed text of the field in progress.
60type St is Data:
61  St{rows: List<&2, List<&2, String>>, row: List<&2, String>, cur: String, mode: Mode, err: String}
62
63def start() -> St:
64  St{[], [], "", MStart{}, ""}
65
66def with_mode(+st: St, m: Mode) -> St:
67  match st:
68    case St{rows, row, cur, mode, err}:
69      St{rows, row, cur, m, err}
70
71def push(+c: U32, +st: St) -> St:
72  match st:
73    case St{rows, row, cur, mode, err}:
74      St{rows, row, SCon{Chr{c}, cur}, mode, err}
75
76# The field in progress ends; the next field starts in `m`.
77def end_field(+st: St, m: Mode) -> St:
78  match st:
79    case St{rows, row, cur, mode, err}:
80      St{rows, String.reverse(cur) <> row, "", m, err}
81
82def end_row(+st: St) -> St:
83  match st:
84    case St{rows, row, cur, mode, err}:
85      St{List.reverse(&2, String, row) <> rows, [], cur, MStart{}, err}
86
87def end_both(+st: St) -> St:
88  end_row(end_field(st, MStart{}))
89
90def fail(+st: St, msg: String) -> St:
91  match st:
92    case St{rows, row, cur, mode, err}:
93      St{rows, row, cur, mode, Bool.pick(String, String.is_empty(err), msg, err)}
94
95def row_empty(+st: St) -> Bool:
96  match st:
97    case St{rows, row, cur, mode, err}:
98      List.is_empty(&2, String, row)
99
100def blank.pick(empty: Bool, +st: St) -> St:
101  match empty:
102    case True{}:
103      st
104    case False{}:
105      end_both(st)
106
107def sel.st(c: Bool, +a: St, +b: St) -> St:
108  match c:
109    case True{}:
110      a
111    case False{}:
112      b
113
114def at_start(+c: U32, +st: St) -> St:
115  sel.st(U32.is_eq(c, 34), with_mode(st, MQuoted{}),
116  sel.st(U32.is_eq(c, 44), end_field(st, MStart{}),
117  sel.st(U32.is_eq(c, 10), blank.pick(row_empty(st), st),
118  sel.st(U32.is_eq(c, 13), st,
119  with_mode(push(c, st), MPlain{})))))
120
121def in_plain(+c: U32, +st: St) -> St:
122  sel.st(U32.is_eq(c, 44), end_field(st, MStart{}),
123  sel.st(U32.is_eq(c, 10), end_both(st),
124  sel.st(U32.is_eq(c, 13), st,
125  push(c, st))))
126
127def in_quoted(+c: U32, +st: St) -> St:
128  sel.st(U32.is_eq(c, 34), with_mode(st, MClosed{}), push(c, st))
129
130def after_quote(+c: U32, +st: St) -> St:
131  sel.st(U32.is_eq(c, 34), with_mode(push(34, st), MQuoted{}),
132  sel.st(U32.is_eq(c, 44), end_field(st, MStart{}),
133  sel.st(U32.is_eq(c, 10), end_both(st),
134  sel.st(U32.is_eq(c, 13), st,
135  fail(st, "text after a closing quote")))))
136
137def step(m: Mode, +c: U32, +st: St) -> St:
138  match m:
139    case MStart{}:
140      at_start(c, st)
141    case MPlain{}:
142      in_plain(c, st)
143    case MQuoted{}:
144      in_quoted(c, st)
145    case MClosed{}:
146      after_quote(c, st)
147
148def mode_of(+st: St) -> Mode:
149  match st:
150    case St{rows, row, cur, mode, err}:
151      mode
152
153def go(s: String, +st: St) -> St:
154  match s:
155    case SNil{}:
156      st
157    case SCon{Chr{c}, t}:
158      go(t, step(mode_of(st), c, st))
159
160# The input ended: close the record in progress.
161def close(m: Mode, +st: St) -> St:
162  match m:
163    case MStart{}:
164      blank.pick(row_empty(st), st)
165    case MQuoted{}:
166      fail(st, "unterminated quoted field")
167    case MPlain{}:
168      end_both(st)
169    case MClosed{}:
170      end_both(st)
171
172def result(+st: St) -> Result<&2, &2, String, List<&2, List<&2, String>>>:
173  match st:
174    case St{rows, row, cur, mode, err}:
175      Bool.pick(Result<&2, &2, String, List<&2, List<&2, String>>>, String.is_empty(err), Done{List.reverse(&2, List<&2, String>, rows)}, Fail{err})
176
177def parse.fin(+st: St) -> Result<&2, &2, String, List<&2, List<&2, String>>>:
178  result(close(mode_of(st), st))
179
180# Records of fields. Blank lines are skipped; `Fail` names a quoting error.
181def parse(s: String) -> Result<&2, &2, String, List<&2, List<&2, String>>>:
182  parse.fin(go(s, start()))