Recently Written · git

CSV

Convert CSV (in a reader macro, or in a file) in to Lisp lists.

git clone https://github.com/equwal/CSV

Log | Files | Refs


commit ac0bf7108272763fa16a318d8cf7bb8f53c727cc
Spenser Truex <spensertruexonline@gmail.com>
2019-01-07 11:38:47 -0800

Initial

 .gitignore   | 18 ++++++++++++++++++
 README.md    | 34 ++++++++++++++++++++++++++++++++++
 csv.asd      |  9 +++++++++
 csv.lisp     | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.lisp |  4 ++++
 utils.lisp   | 20 ++++++++++++++++++++
 6 files changed, 140 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..25a2bf4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+*.FASL
+*.fasl
+*.lisp-temp
+*.dfsl
+*.pfsl
+*.d64fsl
+*.p64fsl
+*.lx64fsl
+*.lx32fsl
+*.dx64fsl
+*.dx32fsl
+*.fx64fsl
+*.fx32fsl
+*.sx64fsl
+*.sx32fsl
+*.wx64fsl
+*.wx32fsl
+README.html
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2fd1c77
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# CSV
+Defines a lisp reader to read is CSV:
+
+`#!field,field,field!#`
+
+which is used to read in CSV files with `slurp`:
+
+`(slurp #p"example.csv")`
+
+and the result is a list:
+
+``` common-lisp
+CL-USER> (csv:slurp #p"example.csv")
+("example" "csv" "list" "result" "like" " this")
+```
+
+This program invokes the lisp reader, making it:
+1) Potentially risky to use if the input it not trusted.
+2) Resource intensive for medium to large files (>5MB).
+
+use with care.
+
+# Features:
+Supports escaping quotes the CSV way, with quad quotes:
+csv   -> lisp
+`""""`->`"\""`
+
+# Installation
+Use ASDF:
+
+``` common-lisp
+CL-USER> (asdf:load-system :csv)
+T
+```
diff --git a/csv.asd b/csv.asd
new file mode 100644
index 0000000..35b1887
--- /dev/null
+++ b/csv.asd
@@ -0,0 +1,9 @@
+(asdf:defsystem :csv
+  :version "0.1.0"
+  :description  "Read CSV files into lists."
+  :author       "Spenser Truex spensertruex.com"
+  :serial       t
+  :license      "GNU GPL, version 3"
+  :components   ((:file "package")
+                 (:file "utils")
+                 (:file "csv")))
diff --git a/csv.lisp b/csv.lisp
new file mode 100644
index 0000000..6d5bbb0
--- /dev/null
+++ b/csv.lisp
@@ -0,0 +1,55 @@
+(in-package :csv)
+(defun make-escapep (prev curr)
+  (char= #\" prev curr))
+(defun end-tokenp (prev curr)
+  (declare (ignore curr))
+  (char= #\" prev))
+(defun openp (prev curr)
+  (declare (ignore curr))
+  (char= #\" prev))
+(let ((in nil)
+      (escape nil))
+  (setf (fdefinition 'switches)
+	(lambda (prev curr str)
+	  (let ((str (if (array-has-fill-pointer-p str)
+                         str
+                         (make-array 1 :fill-pointer 0
+                                       :adjustable t
+                                       :element-type 'character))))
+            (if (null curr)
+	        (progn (setf in nil escape nil) str)
+	        (if in
+		    (if escape
+		        (progn (toggle escape) (push-on prev str))
+		        (if (make-escapep prev curr)
+			    (progn (toggle escape) str)
+			    (if (end-tokenp prev curr)
+			        (progn (toggle in) (push-on #\Nul str))
+			        (push-on prev str))))
+		    (if (openp prev curr)
+		        (progn (toggle in) str)
+		        str)))))))
+(defun splice (str)
+  (labels ((inner (str acc!)
+             (if (string= "" str)
+                 (nreverse acc!)
+                 (let ((res (search (string #\Nul) str)))
+                   (inner (subseq str (1+ res)) (push (subseq str 0 res) acc!))))))
+    (inner str nil)))
+(make-reader my-csv (stream #\# #\!)
+  (macrolet ((once (&body body) ``',,@body))
+    (do ((prev #1= (read-char stream nil nil) curr)
+	 (curr #1# #1#)
+	 (str (make-array 0 :adjustable t :element-type 'character)
+	      (switches prev curr str)))
+	((and (char= curr #\#)
+              (char= prev #\!)) (once (splice (switches nil nil str)))))))
+(defun read-to-string (stream)
+  (with-output-to-string (s)
+    (dolines (line stream)
+      (princ line s)
+      (format s "~%"))))
+(defun slurp (path)
+  "Read in a CSV file. Resource intensive, invokes the lisp reader."
+  (with-open-file (s path)
+    (concatenate 'string "#!" (read-to-string s) "!#")))
diff --git a/package.lisp b/package.lisp
new file mode 100644
index 0000000..c0b5a5c
--- /dev/null
+++ b/package.lisp
@@ -0,0 +1,4 @@
+(in-package :cl-user)
+(defpackage #:csv
+  (:use #:cl)
+  (:export :slurp))
diff --git a/utils.lisp b/utils.lisp
new file mode 100644
index 0000000..e26831a
--- /dev/null
+++ b/utils.lisp
@@ -0,0 +1,20 @@
+(in-package :csv)
+(defmacro dolines ((var stream) &body body)
+  `(do ((,var #1=(read-line ,stream nil nil) #1#))
+       ((null ,var))
+     ,@body))
+(defmacro with-gensyms (symbols &body body)
+  "Create gensyms for those symbols."
+  `(let (,@(mapcar #'(lambda (sym)
+		       `(,sym ',(gensym))) symbols))
+     ,@body))
+(defmacro make-reader (name (stream-var dispatch-1 dispatch-2) &body body)
+  "Simplify the reader macro creation process in most cases."
+  (with-gensyms (sub-char numarg)
+    `(progn (defun ,name (,stream-var ,sub-char ,numarg)
+	      (declare (ignore ,sub-char ,numarg))
+	      ,@body)
+	    (set-dispatch-macro-character ,dispatch-1 ,dispatch-2 #',name))))
+(defun push-on (elt stack)
+  (vector-push-extend elt stack) stack)
+(define-modify-macro toggle () not "Flipping vars.")