lib.bend source
lib.bend on the hub · documented module
# Csv — small, Base-only comma-separated values.## Rows are List<&2, String>; a document is List<&2, List<&2, String>>.# The parser is deliberately a small line-oriented CSV subset. See README.import Base# A field is quoted when it contains syntax which would otherwise be data.def Csv.is_quote(+c: Char) -> Bool: match c: case Chr{+x}: U32.is_eq(x, 34)def Csv.is_comma(+c: Char) -> Bool: match c: case Chr{+x}: U32.is_eq(x, 44)# Finish a field. fields is stored backwards while scanning.def Csv.parse_line.finish( +quoted: Bool, +field: String, +fields: List<&2, String>) -> List<&2, String>: List.reverse(&2, String, String.reverse(field) <> fields)# The scanner has an explicit state so doubled quotes need no mutual# recursion. AfterQuote re-processes the next non-quote character as plain.type Csv.Scan is Data: CPlain{} CQuoted{} CAfterQuote{}def Csv.parse_line.go( s: String, state: Csv.Scan, +field: String, +fields: List<&2, String>) -> List<&2, String>: match s state: case SNil{} _: Csv.parse_line.finish(False{}, field, fields) case SCon{+h, +t} CPlain{}: Bool.pick( List<&2, String>, Csv.is_quote(h), Bool.pick( List<&2, String>, String.is_empty(field), Csv.parse_line.go(t, CQuoted{}, field, fields), Csv.parse_line.go(t, CPlain{}, SCon{h, field}, fields) ), Bool.pick( List<&2, String>, Csv.is_comma(h), Csv.parse_line.go(t, CPlain{}, SNil{}, String.reverse(field) <> fields), Csv.parse_line.go(t, CPlain{}, SCon{h, field}, fields) ) ) case SCon{+h, +t} CQuoted{}: Bool.pick( List<&2, String>, Csv.is_quote(h), Csv.parse_line.go(t, CAfterQuote{}, field, fields), Csv.parse_line.go(t, CQuoted{}, SCon{h, field}, fields) ) case SCon{+h, +t} CAfterQuote{}: Bool.pick( List<&2, String>, Csv.is_quote(h), Csv.parse_line.go(t, CQuoted{}, SCon{h, field}, fields), Csv.parse_line.go(t, CPlain{}, SCon{h, field}, fields) )# Parse one physical line. Empty lines are one empty field.def Csv.parse_line(s: String) -> List<&2, String>: Csv.parse_line.go(s, CPlain{}, SNil{}, Nil{})def Csv.parse.go( lines: List<&2, String>) -> List<&2, List<&2, String>>: match lines: case Nil{}: Nil{} case h <> t: Csv.parse_line(h) <> Csv.parse.go(t)# Empty input is no document; a non-empty input is split on LF. CR is not# stripped, so callers wanting CRLF should trim it before parsing.def Csv.parse(s: String) -> List<&2, List<&2, String>>: match s: case SNil{}: Nil{} case SCon{h, t}: Csv.parse.go(String.lines(SCon{h, t}))# Double quotes in a quoted field are represented by two quotes.def Csv.encode_field.escaped(s: String) -> String: match s: case SNil{}: SNil{} case SCon{+h, +t}: Bool.pick( String, Csv.is_quote(h), String.append("\"\"", Csv.encode_field.escaped(t)), String.append(SCon{h, SNil{}}, Csv.encode_field.escaped(t)) )def Csv.encode_field(+s: String) -> String: Bool.pick( String, Bool.or(String.contains(s, ","), String.contains(s, "\"")), String.append("\"", String.append(Csv.encode_field.escaped(s), "\"")), s )def Csv.encode_row.go( cells: List<&2, String>, first: Bool) -> String: match cells: case Nil{}: SNil{} case +h <> +t: Bool.pick( String, first, String.append(Csv.encode_field(h), Csv.encode_row.go(t, False{})), String.append(",", String.append(Csv.encode_field(h), Csv.encode_row.go(t, False{}))) )def Csv.encode_row(cells: List<&2, String>) -> String: Csv.encode_row.go(cells, True{})def Csv.encode.go( rows: List<&2, List<&2, String>>, first: Bool) -> String: match rows: case Nil{}: SNil{} case +h <> +t: Bool.pick( String, first, String.append(Csv.encode_row(h), Csv.encode.go(t, False{})), String.append("\n", String.append(Csv.encode_row(h), Csv.encode.go(t, False{}))) )def Csv.encode(rows: List<&2, List<&2, String>>) -> String: Csv.encode.go(rows, True{})