csv #
Typed CSV codecs for OCaml, with streaming I/O and located errors.
Reading a CSV file into records usually means splitting lines by hand,
looking columns up by index, and reporting a bad value as an exception with no
row or column attached; writing the records back repeats the work in reverse.
nox-csv describes a row once, as a codec in the style of
jsont, and uses that description both
to decode a file into records and to encode records into a file. Columns are
matched by name against the header once per file, so each row is decoded
without a lookup.
Streaming I/O is built on
bytesrw: of_reader reads from a
Bytes.Reader.t and to_writer writes to a Bytes.Writer.t, so CSV
processing composes with other bytesrw codecs. of_file and of_string
return the decoded rows, and fold_file and fold_reader process rows
without keeping the list.
Decode errors carry a source location from loc and a path naming the failing row, by index, and column, by name.
Install #
$ opam install nox-csv
If opam cannot find the package, add the overlay repository first:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-csv
Usage #
type point = { x : float; y : float; label : string }
let point_codec =
Csv.(Row.(
obj (fun x y label -> { x; y; label })
|> col "x" Field.float ~enc:(fun p -> p.x)
|> col "y" Field.float ~enc:(fun p -> p.y)
|> col "label" Field.string ~enc:(fun p -> p.label)
|> finish
))
(* Decode a string. Errors carry source locations and row/column context. *)
let csv = "x,y,label\n1.0,2.0,A\n3.5,4.5,B\n"
let points =
match Csv.of_string point_codec csv with
| Ok points -> points
| Error e -> failwith (Csv.Error.to_string e)
let () =
assert (points =
[ { x = 1.0; y = 2.0; label = "A" };
{ x = 3.5; y = 4.5; label = "B" } ])
(* Streaming decode from a bytesrw reader. *)
let sum =
let r = Bytesrw.Bytes.Reader.of_string csv in
match Csv.of_reader point_codec r with
| Ok rows -> List.fold_left (fun acc p -> acc +. p.x) 0.0 rows
| Error e -> failwith (Csv.Error.to_string e)
let () = assert (sum = 4.5)
(* Encode to a bytesrw writer. Raises [Csv.Invalid_utf8_encode] if any
field string contains malformed UTF-8. *)
let encoded =
let buf = Buffer.create 256 in
let w = Bytesrw.Bytes.Writer.of_buffer buf in
Csv.to_writer point_codec points w;
Buffer.contents buf
let () = assert (encoded = "x,y,label\n1,2,A\n3.5,4.5,B\n")
For programmatic inspection of errors (IDE diagnostics, error
aggregation), decoding functions return structured Csv.Error.t values.
Use Csv.Error.kind to inspect the error kind and Csv.Error.to_string
to format the complete diagnostic. Reading functions with an _exn
suffix raise Csv.Error instead:
let error_kind =
match Csv.of_string (Csv.col "x" Csv.Field.float) "x\nbad\n" with
| Ok _ -> failwith "expected an invalid float"
| Error e -> Csv.Error.kind e
let () =
assert (Csv.Error.string_of_kind error_kind =
"invalid value \"bad\": not a float")
API overview #
- Field codecs:
Field.string,int,int64,float,bool,option,nullable_floatandnullable_int.Field.vbuilds a custom codec andField.maptransforms one;Field.decode,Field.decode_exnandField.encoderun one on a single field. - The
Rowbuilder:obj,col,col_at,col_blockandfinish. Named and positional columns take a?dec_absentdefault, andcol_blockreads strided groups of columns. - Reading:
of_file,of_stringandof_reader, with_exnvariants. Writing:to_stringandto_writer. Folding:fold_fileandfold_reader. decode,decode_exnandencodework on rows already split into fields, header first, andresolve,decode_row,encode_rowandencode_headeron one row at a time against a header.col,update_colanddelete_colquery and update single columns, andcol_namesandcol_countdescribe a codec.Csv.Loc,Csv.Meta,Csv.PathandCsv.Errorare the location and error modules ofloc, and theSortmodule names the three sorts of value (Row,Field,Header) that errors refer to.- The encoder raises
Invalid_utf8_encodewhen a field is not well-formed UTF-8.
The byte-reading entry points and folds accept ?max_rows and
?max_cols to bound data-row count and header width on untrusted input.
Both limits are unlimited by default. Byte readers and writers accept
?sep for a separator other than a comma.
A decode reads the columns its codec names and skips the rest, so a wide file costs only the columns the codec uses. UTF-8 well-formedness is checked on every header name, which resolving the codec reads, and in a data row only on the columns the codec reads.
Full signatures in csv.mli.
References #
- RFC 4180, the CSV format.
- jsont, the JSON codec library whose design this follows.
- bytesrw, composable byte stream readers and writers.
- loc, source locations and errors.
License #
ISC. See LICENSE.md.