summaryrefslogtreecommitdiffstats
path: root/src/csv.lisp
blob: 4c6297b0e8b000669f5ae749effc22d4ca2223a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
(in-package #:cepis)

(defun parse-csv (text)
  "Parse TEXT as RFC 4180 CSV and return a list of rows, each a list of strings.

Values may be double-quoted, in which case they may contain commas, newlines
and doubled quotes; that is why the batch endpoint cannot be read by splitting
on commas. A byte order mark and CRLF line endings are both tolerated."
  (let ((rows '())
        (row '())
        (buffer (make-string-output-stream))
        (in-quotes nil)
        (dirty nil)
        (index 0)
        (end (length text)))
    (labels ((flush-field ()
               (push (get-output-stream-string buffer) row))
             (flush-row ()
               (flush-field)
               (push (nreverse row) rows)
               (setf row '()
                     dirty nil)))
      (when (and (plusp end) (char= (char text 0) (code-char #xFEFF)))
        (incf index))
      (loop while (< index end)
            do (let ((character (char text index)))
                 (cond (in-quotes
                        (cond ((not (char= character #\"))
                               (write-char character buffer))
                              ((and (< (1+ index) end) (char= (char text (1+ index)) #\"))
                               (write-char #\" buffer)
                               (incf index))
                              (t (setf in-quotes nil))))
                       ((char= character #\")
                        (setf in-quotes t
                              dirty t))
                       ((char= character #\,)
                        (flush-field)
                        (setf dirty t))
                       ((char= character #\Newline)
                        (flush-row))
                       ((char= character #\Return))
                       (t
                        (write-char character buffer)
                        (setf dirty t))))
               (incf index))
      (when (or dirty row)
        (flush-row))
      (nreverse rows))))

(defun csv-row-table (header row)
  "Zip HEADER and ROW into a hash table, so a CSV row reads like a JSON object.

Columns the row does not reach are filled with the empty string, which is what
the API sends for a field it does not have."
  (let ((table (make-hash-table :test #'equal)))
    (loop for column in header
          for rest = row then (cdr rest)
          do (setf (gethash column table) (or (car rest) "")))
    table))