Commits

Miki Tebeka committed f180309

Writing start to work

  • Participants
  • Parent commits 198dd12
  • Branches write

Comments (0)

Files changed (2)

File src/csvlib.clj

 (ns csvlib
   (:import (com.csvreader CsvReader CsvWriter)
-            java.nio.charset.Charset))
-
-(defn- make-converter
-  "Make a converter function from a conversion table"
-  [converison-map]
-  (let [convert (fn [key value] ((get converison-map key identity) value))]
-    (fn [record]
-      (zipmap (keys record) (map #(convert % (record %)) (keys record))))))
+            java.nio.charset.Charset)
+  (:use [clojure.set :only (subset?)]))
 
 ; Default delimiter
 (def *delimiter* \,)
 ; Default charset
 (def *charset* "UTF-8")
+; Flush every record write?
+(def *flush?* nil)
+
+(defn- make-converter
+  "Make a converter function from a conversion table."
+  [converison-map]
+  (let [convert (fn [key value] ((get converison-map key identity) value))]
+    (fn [record]
+      (zipmap (keys record) (map #(convert % (record %)) (keys record))))))
 
 (defn- record-seq 
   "Reutrn a lazy sequence of records from a CSV file"
                         (into [] (.getValues csv))))]
     (take-while (complement nil?) (repeatedly read-record))))
 
-; Default delimiter
-(def *delimiter* \,)
-; Default charset
-(def *charset* "UTF-8")
-
 (defn read-csv
   "Return a lazy sequence of records (maps) from CSV file.
 
-  With header map will be header->value, otherwise it'll be position->value.
-  `conversions` is an optional map from header to a function that convert the
-  value.
+  With headers? map will be header->value, otherwise it'll be position->value.
   
-  Additional keyword arguments are 'charset' for the file character set and
-  'delimiter' for record delimiter"
+  Options keyword arguments:
+    headers? - Use first line as headers
+    convert - A conversion map (field -> conversion function)
+    charset - Charset to use (defaults to *charset*)
+    delimiter - Record delimiter (defaults to *delimiter*)"
   [filename & {:keys [headers? convert charset delimiter]
                :or {charset *charset* delimiter *delimiter*}}]
    (let [records (record-seq filename delimiter charset)
      (map convert
           (map #(zipmap headers %) (if headers? (rest records) records)))))
 
-; (defn- indexes [coll]
-;   (range (count coll)))
-; 
-; (defn- gen-headers 
-;   "Generate headers for combinations of headers supplied by the user (which can
-;   be a nil, map or a vector, and the first record (which can also be nil, map or
-;   vector)."
-;   [headers record]
-;   (cond
-;     (and (nil? headers) (nil? record)) nil
-;     (and (nil? headers) (vector? record)) nil
-;     (vector? headers) (zipmap headers (indexes headers))
-;     (map? headers) headers
-;     (map? record) (zipmap (keys record) (indexes record))))
-; 
-; (defn sort-record [record headers]
-;   (if (vector? record)
-;     record
-;     (if headers
-;       (sort-by :record headers)
-;       (assert aaaa
-;       record)))
-; 
-; (defn write-csv
-;   [records filename & {:keys [delimiter charset headers]
-;                :or {delimiter *delimiter* charset *charset*}}]
-;   (let [writer (CsvWriter. filename delimiter (Charset/fromName charset))
-;         headers (gen-headers headers (first records))]
-;     (when headers (
+(defn vectorize-headers 
+  "Return a vector of headers keys sorted by values"
+  [headers]
+  (map first (sort-by headers headers)))
 
+(defn- gen-headers 
+  "Generate headers for combinations of headers supplied by the user (which can
+  be a nil, map or a vector, and the first record (which can also be nil, map or
+  vector)."
+  [headers record]
+  (cond
+    (and (nil? headers) (nil? record)) nil
+    (and (nil? headers) (vector? record)) nil
+    (vector? headers) headers
+    (map? headers) (vectorize-headers headers)
+    (map? record) (vectorize-headers headers)))
 
+(defn keyset
+  "Return a set of map keys."
+  [map]
+  (set (keys map)))
 
-; (defn write-csv
-;   ([filename] (write-csv filename records nil))
-;   [filename records opts]
-;   (let [delimiter (:delimiter opts \,)
-;         charset (Charset/forName (:charset opts "UTF-8"))
-;         headers (gen-headers (:headers opts) (first records))]
-;   (let [writer (CsvWriter. filename delimiter (Charset/fromName charset))
-;         headers? (not (nil? headers))
-;         header 
-;         headers+ (if headers headers (zipmap (range
+(defn unknowns? 
+  "Return true if there are any unknown fields in record."
+  [record headers]
+  (or (> (count record) (count headers))
+      (not (subset? (keyset record) (set headers)))))
 
+(defn write-values 
+  "Write values (a line) to a CSV, will flush if *flush?* is true."
+  [writer values]
+  (.writeRecord writer (into-array String values))
+  (when *flush?* (.flush writer)))
+
+(defn gen-formatter [format headers]
+  (let [index (if headers #(headers %) identity)]
+    (fn [record]
+      (keep-indexed #((get format (index %1) str) %2) record))))
+
+(defn sort-record 
+  "Sort record by headers. Return a sequence of values."
+  [record headers]
+  (when (unknowns? record headers) (throw (Exception. "unknown fields")))
+  (map #(get % record "") headers))
+
+(defn gen-values 
+  "Generate values seq from a record."
+  [record headers format]
+  (when (and (map? record) (nil? headers)) 
+    (throw (Exception. "map record with no headers")))
+  (let [record (if (vector? record) record (sort-record record headers))]
+    (format record)))
+
+(defn write-csv
+  "Write records to CSV.
+
+  Optional arguments are:
+    delimiter - Delimiter to use (defaults to *delimiter*)
+    charset - Charset to use (default to *charset*)
+    flush? - Flag to flush after every record (default to *flush?*)
+    format - A map key -> formatter"
+  [records filename & {:keys [delimiter charset headers flush? format]
+                       :or {delimiter *delimiter* charset *charset*}}]
+  (let [writer (CsvWriter. filename delimiter (Charset/forName charset))
+        headers (gen-headers headers (first records))
+        formatter (gen-formatter format headers)]
+    (binding [*flush?* flush]
+      (when headers (write-values writer headers))
+      (dorun 
+        (map #(write-values writer (gen-values % headers formatter)) records))
+      (.close writer))))

File test/csvlib_test.clj

 (ns csvlib-test
+  (:import java.io.File)
   (:use [csvlib] :reload-all)
   (:use [clojure.test]))
 
         records (read-csv "test/toons.csv" :headers? true :convert conv)]
     (is (= (count records) 3))
     (is (= ((first records) "Funny") true))))
+
+(defn tempfile []
+  (.getCanonicalPath (File/createTempFile "-csvlib-" nil)))
+
+(deftest test-write-simple
+  (let [tmp (tempfile)]
+    (write-csv [[1 2 3] [4 5 6]] tmp)
+    (is (= (slurp tmp) "1,2,3\n4,5,6\n"))))
+