--- a/Makefile Tue Jan 14 20:06:56 2020 -0500
+++ b/Makefile Mon Apr 13 15:20:35 2026 -0400
@@ -10,31 +10,31 @@
test-sbcl:
$(heading_printer) computer 'SBCL'
- sbcl --load test/run.lisp
+ time sbcl --load test/run.lisp
test-ccl:
$(heading_printer) slant 'CCL'
- ccl --load test/run.lisp
+ time ccl --load test/run.lisp
test-ecl:
$(heading_printer) roman 'ECL'
- ecl --load test/run.lisp
+ time ecl --load test/run.lisp
test-abcl:
$(heading_printer) broadway 'ABCL'
- abcl --load test/run.lisp
+ time abcl --load test/run.lisp
# Documentation ---------------------------------------------------------------
-# $(apidocs): $(sourcefiles)
-# sbcl --noinform --load docs/api.lisp --eval '(quit)'
+$(apidocs): $(sourcefiles)
+ sbcl --noinform --load docs/api.lisp --eval '(quit)'
-# docs/build/index.html: $(docfiles) $(apidocs) docs/title
-# cd docs && ~/.virtualenvs/d/bin/d
+docs/build/index.html: $(docfiles) $(apidocs) docs/title
+ cd docs && ~/bin/venvs/tools/bin/d
-# docs: docs/build/index.html
+docs: docs/build/index.html
-# pubdocs: docs
-# hg -R ~/src/docs.stevelosh.com pull -u
-# rsync --delete -a ./docs/build/ ~/src/docs.stevelosh.com/conserve
-# hg -R ~/src/docs.stevelosh.com commit -Am 'conserve: Update site.'
-# hg -R ~/src/docs.stevelosh.com push
+pubdocs: docs
+ hg -R ~/src/docs.stevelosh.com pull -u
+ rsync --delete -a ./docs/build/ ~/src/docs.stevelosh.com/conserve
+ hg -R ~/src/docs.stevelosh.com commit -Am 'conserve: Update site.'
+ hg -R ~/src/docs.stevelosh.com push
--- a/README.markdown Tue Jan 14 20:06:56 2020 -0500
+++ b/README.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -1,10 +1,7 @@
-Conserve is a Common Lisp library for reading and writing [RFC
+Conserve is a small Common Lisp library for reading and writing [RFC
4180](https://tools.ietf.org/html/rfc4180) CSV data.
-**Not ready yet, clone at your own risk.**
-
-The test suite passes in SBCL, CCL, ECL, ABCL, Allegro, and LispWorks on Ubuntu
-18.04.
+It is about 200 lines of code and has no dependencies.
* **License:** MIT/X11
* **Documentation:** <https://docs.stevelosh.com/conserve/>
--- a/conserve.asd Tue Jan 14 20:06:56 2020 -0500
+++ b/conserve.asd Mon Apr 13 15:20:35 2026 -0400
@@ -27,7 +27,7 @@
:serial t
:components ((:module "test" :serial t :components
- ((:file "package.test")
+ ((:file "package")
(:file "tests"))))
:perform (asdf:test-op (op system)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/01-usage.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,142 @@
+Usage
+=====
+
+Conserve is a small Common Lisp library for reading and writing [RFC
+4180](https://tools.ietf.org/html/rfc4180) CSV data.
+
+It was made because I was tired of dealing with complicated libraries with many
+dependencies for parsing simple CSV data files.
+
+[TOC]
+
+## Example Data
+
+In the following documentation we'll read from the following `example.csv`:
+
+ id,name,score
+ 1,foo,88.8
+ 2,bar,100
+ 3,baz,77
+
+## Reading
+
+CSV files can be read row-by-row or all at once, from a stream or a string. If
+you want to read from a file it's up to you to open the file (with the
+appropriate external format) yourself.
+
+To read all rows from a stream, use `conserve:read-rows`:
+
+ (with-open-file (f "example.csv" :direction :input)
+ (conserve:read-rows f))
+ ; =>
+ ; (("id" "name" "score")
+ ; ("1" "foo" "88.8")
+ ; ("2" "bar" "100")
+ ; ("3" "baz" "77"))
+
+Conserve does not process headers in any special way, nor does it parse values
+at all. Rows are returned as lists of strings — what you do with that is up to
+you.
+
+ (defun parse-row (row)
+ (destructuring-bind (id name score) row
+ (list (parse-integer id)
+ name
+ (parse-float:parse-float score))))
+
+ (with-open-file (f "example.csv" :direction :input)
+ (destructuring-bind (header . rows)
+ (conserve:read-rows f)
+ (values header (map-into rows #'parse-row rows))))
+
+ ; =>
+ ; ("id" "name" "score")
+ ; ((1 "foo" 88.8)
+ ; (2 "bar" 100.0)
+ ; (3 "baz" 77.0))
+
+Use `conserve:read-row` to read a single row at a time:
+
+ (with-open-file (f "example.csv" :direction :input)
+ (conserve:read-row f))
+ ; =>
+ ; ("id" "name" "score")
+
+Note that `conserve:read-row` has the same interface as most Common Lisp
+`read-*` style functions, so it can often be used in places that expect that
+interface:
+
+ (iterate
+ (for (id name nil) :in-file "example.csv" :using #'conserve:read-row)
+ (finding (parse-integer id) :such-that (string= name "bar")))
+ ; => 2
+
+Both reading functions support reading from a string instead of a stream:
+
+ (conserve:read-row "foo,\"a,b,c\",bar")
+ ; => ("foo" "a,b,c" "bar")
+
+## Writing
+
+Much like reading, Conserve supports writing one or many rows at a time.
+
+Use `conserve:write-rows` to write a list of rows:
+
+ (with-open-file (f "out1.csv" :direction :output)
+ (conserve:write-rows '(("id" "name" "score")
+ ("1" "foo" "88.8")
+ ("2" "bar" "100.0")
+ ("3" "baz" "77.0"))
+ f))
+
+Use `conserve:write-row` to write a single row at a time:
+
+ (with-open-file (f "out2.csv" :direction :output)
+ (conserve:write-row '("id" "name" "score") f)
+ (conserve:write-row '("1" "foo" "88.8") f)
+ (conserve:write-row '("2" "bar" "100.0") f)
+ (conserve:write-row '("3" "baz" "77.0") f))
+
+Rows must be a list of *strings* — Conserve does not attempt to guess how you
+would like to serialize other objects to strings.
+
+If `nil` is passed as a stream, Conserve will return the resulting CSV as
+a string:
+
+ (conserve:write-row '("foo"
+ "some \"quoted\" field"
+ "comma,field")
+ nil)
+ ; =>
+ "foo,\"some \"\"quoted\"\" field\",\"comma,field\"
+ "
+
+## Delimiter
+
+Conserve allows one single piece of customization: the choice of delimiter to
+use. You can change the delimiter by binding `conserve:*delimiter*`:
+
+ (let ((conserve:*delimiter* #\,))
+ (conserve:write-row '("a" "b") nil))
+ ; =>
+ "a,b
+ "
+
+ (let ((conserve:*delimiter* #\|))
+ (conserve:write-row '("a" "b") nil))
+ ; =>
+ "a|b
+ "
+
+ (let ((conserve:*delimiter* #\tab))
+ (conserve:write-row '("foo,bar" "foo|bar") nil))
+ ; =>
+ "foo,bar foo|bar
+ "
+
+## Test Suite
+
+The test suite include both hardcoded tests against particular edge cases, as
+well as round-trip fuzz testing against `cl-csv` and `fare-csv` to make sure it
+produces similar results. You will need to Quickload those other CSV parsers to
+run the test suite (but not to use Conserve itself, of course).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/02-reference.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,64 @@
+# API Reference
+
+The following is a list of all user-facing parts of Conserve.
+
+If there are backwards-incompatible changes to anything listed here, they will
+be noted in the changelog and the author will feel bad.
+
+Anything not listed here is subject to change at any time with no warning, so
+don't touch it.
+
+[TOC]
+
+## Package `CONSERVE`
+
+### `*DELIMITER*` (variable)
+
+### `READ-ROW` (function)
+
+ (READ-ROW &OPTIONAL (STREAM-OR-STRING *STANDARD-INPUT*) (EOF-ERROR-P T) EOF-VALUE)
+
+Read and return a row of fields from the CSV data in `stream-or-string`.
+
+ The result will be a fresh list.
+
+ If the end of file for the stream is encountered immediately, an error is
+ signaled unless `eof-error-p` is false, in which case `eof-value` is returned.
+
+
+
+### `READ-ROWS` (function)
+
+ (READ-ROWS &OPTIONAL (STREAM-OR-STRING *STANDARD-INPUT*))
+
+Read and return all CSV rows from the CSV data in `stream-or-string`.
+
+ The result will be a completely fresh list of lists.
+
+
+
+### `WRITE-ROW` (function)
+
+ (WRITE-ROW ROW &OPTIONAL (STREAM *STANDARD-OUTPUT*))
+
+Write `row` to `stream` as CSV data.
+
+ `row` must be a list of strings.
+
+ If `stream` is `nil`, the data will be returned as a fresh string instead.
+
+
+
+### `WRITE-ROWS` (function)
+
+ (WRITE-ROWS ROWS &OPTIONAL (STREAM *STANDARD-OUTPUT*))
+
+Write `rows` to `stream` as CSV data.
+
+ `rows` must be a list of lists of strings. The consequences are undefined if
+ all the rows do not have the same number of fields.
+
+ If `stream` is `nil`, the data will be returned as a fresh string instead.
+
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/03-changelog.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,12 @@
+Changelog
+=========
+
+Here's the list of changes in each released version.
+
+[TOC]
+
+1.0.0
+-----
+
+Initial version.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/api.lisp Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,20 @@
+(ql:quickload "cl-d-api")
+
+(defparameter *header*
+ "The following is a list of all user-facing parts of Conserve.
+
+If there are backwards-incompatible changes to anything listed here, they will
+be noted in the changelog and the author will feel bad.
+
+Anything not listed here is subject to change at any time with no warning, so
+don't touch it.
+
+")
+
+(d-api:generate-documentation
+ :conserve
+ #p"docs/02-reference.markdown"
+ (list "CONSERVE")
+ *header*
+ :title "API Reference")
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/footer.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,3 @@
+<i>Made with Lisp and love by [Steve Losh][].</i>
+
+[Steve Losh]: http://stevelosh.com/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/index.markdown Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,9 @@
+Conserve is a small Common Lisp library for reading and writing [RFC
+4180](https://tools.ietf.org/html/rfc4180) CSV data.
+
+It is about 200 lines of code and has no dependencies.
+
+* **License:** MIT/X11
+* **Documentation:** <https://docs.stevelosh.com/conserve/>
+* **Mercurial:** <https://hg.stevelosh.com/conserve/>
+* **Git:** <https://github.com/sjl/conserve/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/title Mon Apr 13 15:20:35 2026 -0400
@@ -0,0 +1,1 @@
+Conserve
--- a/test/tests.lisp Tue Jan 14 20:06:56 2020 -0500
+++ b/test/tests.lisp Mon Apr 13 15:20:35 2026 -0400
@@ -271,16 +271,17 @@
(defun read-file/conserve ()
- (with-open-file (s "test/data/large-conserve.csv")
- (if *verify-large-file-reads*
- (loop
- :for original :in *data*
- :for row = (conserve:read-row s nil :eof)
- :until (eql :eof row)
- :do (assert (equal original row)))
- (loop
- :for row = (conserve:read-row s nil :eof)
- :until (eql :eof row)))))
+ (losh:profile
+ (with-open-file (s "test/data/large-conserve.csv")
+ (if *verify-large-file-reads*
+ (loop
+ :for original :in *data*
+ :for row = (conserve:read-row s nil :eof)
+ :until (eql :eof row)
+ :do (assert (equal original row)))
+ (loop
+ :for row = (conserve:read-row s nil :eof)
+ :until (eql :eof row))))))
(defun read-file/fare-csv ()
(with-open-file (s "test/data/large-fare-csv.csv")