src/main.lisp @ a4abd0534b7d

Initial commit
author Steve Losh <steve@stevelosh.com>
date Tue, 25 Dec 2018 14:32:50 -0500
parents (none)
children 9322171b2cc4
(in-package :trivial-csv)

;;;; Configuration ------------------------------------------------------------
(defparameter *delimiter* #\,)

(defun check-delimiter ()
  (check-type *delimiter* (and character (not (member #\newline #\")))))


;;;; Reading ------------------------------------------------------------------
(defun read-char-if (char stream &optional (eof-error-p t) eof-value)
  "If the next character in `stream` is `char`, read and return it.  Otherwise, return `nil`."
  (cond
    ((eql char (peek-char nil stream eof-error-p nil)) (read-char stream))
    ((eql char eof-value) char)
    (t nil)))

(defun read-unquoted-field (stream delimiter)
  "Read an unquoted field (but not the ending field delimiter) from `stream`."
  (with-output-to-string (result)
    (loop :for next = (peek-char nil stream nil delimiter)
          :until (cond
                   ((char= next delimiter) t)
                   ((char= next #\newline) t)
                   ((char= next #\") (error "Unquoted field contains quote."))
                   (t nil))
          :do (write-char (read-char stream) result))))

(defun read-quoted-field (stream delimiter)
  "Read a quoted field (but not the ending field delimiter) from `stream`."
  (declare (ignore delimiter))
  (read-char stream) ; chomp initial quote
  (with-output-to-string (result)
    (loop :for char = (read-char stream)
          :until (and (char= #\" char)
                      (not (read-char-if #\" stream nil)))
          :do (write-char char result))))

(defun read-field (stream delimiter)
  "Read and return a single field from `stream`."
  (let* ((field (case (peek-char nil stream nil :eof)
                  (#\" (read-quoted-field stream delimiter))
                  (#\newline (read-char stream) nil)
                  (:eof nil)
                  (t (read-unquoted-field stream delimiter))))
         (done (cond
                 ((null field) t) ; empty field at the end
                 ((read-char-if delimiter stream nil) nil) ; normal field
                 ((read-char-if #\newline stream nil #\newline) t) ; last field
                 (t (error "Bad data after field ~S: ~S"
                           field (peek-char nil stream))))))
    (values (or field "") done)))

(defun read-row% (stream delimiter eof-error-p eof-value)
  (if (eql :eof (peek-char nil stream eof-error-p :eof))
    eof-value
    (loop
      :with field :with done
      :do (setf (values field done) (read-field stream delimiter))
      :collect field
      :until done)))


;;;; Writing ------------------------------------------------------------------
(defun field-needs-quoting-p (field delimiter)
  (find-if (lambda (char)
             (or (char= char #\newline)
                 (char= char #\")
                 (char= char delimiter)))
           field))

(defun write-quoted-field (field stream)
  (write-char #\" stream)
  (loop :for char :across field :do
        (write-char char stream)
        (when (char= #\" char)
          (write-char char stream)))
  (write-char #\" stream))

(defun write-unquoted-field (field stream)
  (write-string field stream))

(defun write-field (field stream delimiter)
  (if (field-needs-quoting-p field delimiter)
    (write-quoted-field field stream)
    (write-unquoted-field field stream)))

(defun write-row% (row stream delimiter)
  (check-type row (not null))
  (loop :for (field . more) :on row :do
        (write-field field stream delimiter)
        (when more
          (write-char delimiter stream)))
  (write-char #\newline stream))


;;;; API ----------------------------------------------------------------------
(defun read-row (&optional (stream *standard-input*) (eof-error-p t) eof-value)
  "Read and return a row of fields from `stream`."
  (check-delimiter)
  (read-row% stream *delimiter* eof-error-p eof-value))

(defun read-rows (&optional (stream *standard-input*))
  "Read and return all rows from `stream`."
  (check-delimiter)
  (loop :with delimiter = *delimiter*
        :for row = (read-row% stream delimiter nil :eof)
        :until (eql row :eof)
        :collect row))

(defun write-row (row &optional (stream *standard-output*))
  (check-delimiter)
  (write-row% row stream *delimiter*)
  (values))

(defun write-rows (rows &optional (stream *standard-output*))
  (check-delimiter)
  (loop :with delimiter = *delimiter*
        :for row :in rows
        :do (write-row% row stream delimiter)))