b5c392b79f449c6b838ff5185acee78622c2c3c9
[ocaml-csv.git] / csv.ml
1 (* csv.ml - comma separated values parser
2  *
3  * $Id: csv.ml,v 1.1 2003-12-17 16:05:08 rich Exp $
4  *)
5
6 (* The format of CSV files:
7  * 
8  * Each field starts with either a double quote char or some other
9  * char. For the some other char case things are simple: just read up
10  * to the next comma (,) which marks the end of the field.
11  * 
12  * In the case where a field begins with a double quote char the
13  * parsing rules are different. Any double quotes are doubled ("") and
14  * we finish reading when we reach an undoubled quote. eg: "The
15  * following is a quote: "", and that's all" is the CSV equivalent of
16  * the following literal field: The following is a quote: ", and that's
17  * all
18  * 
19  * CSV fields can also contain literal carriage return characters, if
20  * they are quoted, eg: "This field is split over lines" represents a
21  * single field containing a \n.
22  * 
23  * Excel will only use the quoting format if a field contains a double
24  * quote or comma, although there's no reason why Excel couldn't always
25  * use the quoted format.
26  * 
27  * The practical upshot of this is that you can't split a line in a CSV
28  * file just by looking at the commas. You need to parse each field
29  * separately.
30  * 
31  * How we represent CSV files:
32  * 
33  * We load in the whole CSV file at once, and store it internally as a
34  * 'string list list' type (note that each line in the CSV file can,
35  * and often will, have different lengths). We then provide simple
36  * functions to read the CSV file line-by-line, copy it out, or copy a
37  * subset of it into a matrix.
38  * 
39  * For future work: According to the Text::CSV_XS manual page, "0 is a
40  * valid encoding, within quoted fields, of the ASCII NUL character. In
41  * Unix this character could, of course, be encoded directly in the
42  * file.
43  *)
44
45 type t = string list list
46
47 exception Bad_CSV_file of string
48
49 let lines = List.length
50
51 let columns csv =
52   List.fold_left max 0 (List.map List.length csv)
53
54 type state_t = StartField
55                | InUnquotedField
56                | InQuotedField
57                | InQuotedFieldAfterQuote
58
59 let load_rows f chan =
60   let row = ref [] in                   (* Current row. *)
61   let field = ref [] in                 (* Current field. *)
62   let state = ref StartField in         (* Current state. *)
63   let end_of_field () =
64     let field_list = List.rev !field in
65     let field_len = List.length field_list in
66     let field_str = String.create field_len in
67     let rec loop i = function
68         [] -> ()
69       | x :: xs ->
70           field_str.[i] <- x;
71           loop (i+1) xs
72     in
73     loop 0 field_list;
74     row := field_str :: !row;
75     field := [];
76     state := StartField
77   in
78   let empty_field () =
79     row := "" :: !row;
80     field := [];
81     state := StartField
82   in
83   let end_of_row () =
84     let row_list = List.rev !row in
85     f row_list;
86     row := [];
87     state := StartField
88   in
89   let rec loop () =
90     let c = input_char chan in
91     if c != '\r' then (                 (* Always ignore \r characters. *)
92       match !state with
93           StartField ->                 (* Expecting quote or other char. *)
94             if c = '\"' then (
95               state := InQuotedField;
96               field := []
97             ) else if c = ',' then      (* Empty field. *)
98               empty_field ()
99             else if c = '\n' then (     (* Empty field, end of row. *)
100               empty_field ();
101               end_of_row ()
102             ) else (
103               state := InUnquotedField;
104               field := [c]
105             )
106         | InUnquotedField ->            (* Reading chars to end of field. *)
107             if c = ',' then             (* End of field. *)
108               end_of_field ()
109             else if c = '\n' then (     (* End of field and end of row. *)
110               end_of_field ();
111               end_of_row ()
112             ) else
113               field := c :: !field
114         | InQuotedField ->              (* Reading chars to end of field. *)
115             if c = '\"' then
116               state := InQuotedFieldAfterQuote
117             else
118               field := c :: !field
119         | InQuotedFieldAfterQuote ->
120             if c = '\"' then (          (* Doubled quote. *)
121               field := c :: !field;
122               state := InQuotedField
123             ) else if c = '0' then (    (* Quote-0 is ASCII NUL. *)
124               field := '\000' :: !field;
125               state := InQuotedField
126             ) else if c = ',' then      (* End of field. *)
127               end_of_field ()
128             else if c = '\n' then (     (* End of field and end of row. *)
129               end_of_field ();
130               end_of_row ()
131             )
132     ); (* end of match *)
133     loop ()
134   in
135   try
136     loop ()
137   with
138       End_of_file ->
139         (* Any part left to write out? *)
140         (match !state with
141              StartField ->
142                if !row <> [] then
143                  ( empty_field (); end_of_row () )
144            | InUnquotedField | InQuotedFieldAfterQuote ->
145                end_of_field (); end_of_row ()
146            | InQuotedField ->
147                raise (Bad_CSV_file "Missing end quote after quoted field.")
148         )
149
150 let load_in chan =
151   let csv = ref [] in
152   let f row =
153     csv := row :: !csv
154   in
155   load_rows f chan;
156   List.rev !csv
157
158 let load filename =
159   let chan = open_in filename in
160   let csv = load_in chan in
161   close_in chan;
162   csv 
163
164 (* Quote a single CSV field. *)
165 let quote_field field =
166   if String.contains field ',' ||
167     String.contains field '\"' ||
168     String.contains field '\n'
169   then (
170     let buffer = Buffer.create 100 in
171     Buffer.add_char buffer '\"';
172     for i = 0 to (String.length field) - 1 do
173       match field.[i] with
174           '\"' -> Buffer.add_string buffer "\"\""
175         | c    -> Buffer.add_char buffer c
176     done;
177     Buffer.add_char buffer '\"';
178     Buffer.contents buffer
179    )
180   else
181     field
182
183 let save_out chan csv =
184   List.iter (fun line ->
185                output_string chan (String.concat ","
186                                      (List.map quote_field line));
187                output_char chan '\n') csv
188
189 let print csv =
190   save_out stdout csv
191
192 let save file csv =
193   let chan = open_out file in
194   save_out chan csv;
195   close_out chan