diff --git a/src/semantic_csv/core.cljc b/src/semantic_csv/core.cljc index 25c4ddf..cba3a93 100644 --- a/src/semantic_csv/core.cljc +++ b/src/semantic_csv/core.cljc @@ -75,11 +75,12 @@ If `:transform-header` is present, this option will be ignored. * `:transform-header` - A function that transforms the header/column names for each column. This takes precedence over `keyify` and should be a function that takes a string. + * `:preserve-header` - every row will have the full header even if values are missing from the CSV (default: `false`) * `:header` - specify the header to use for map keys, preventing first row of data from being consumed as header. * `:structs` - bool; use structs instead of hash-maps or array-maps, for performance boost (default: `false`)." ([rows] (mappify {} rows)) - ([{:keys [keyify transform-header header structs] :or {keyify true} :as opts} + ([{:keys [keyify preserve-header transform-header header structs] :or {keyify true preserve-header false} :as opts} rows] (let [xform #?(:clj (if structs (td/structify opts) diff --git a/src/semantic_csv/impl/core.cljc b/src/semantic_csv/impl/core.cljc index 8f09866..61b3203 100644 --- a/src/semantic_csv/impl/core.cljc +++ b/src/semantic_csv/impl/core.cljc @@ -4,10 +4,25 @@ (:require [clojure.string :as s])) +(defn zipmap-keys + "Returns a map with the keys mapped to the corresponding vals. + Will add nil values if vals is smaller than keys." + [keys vals] + (loop [map {} + ks (seq keys) + vs (seq vals)] + (if ks + (recur (assoc map (first ks) (first vs)) + (next ks) + (next vs)) + map))) + (defn mappify-row "Translates a single row of values into a map of `colname -> val`, given colnames in `header`." - [header row] - (into {} (map vector header row))) + [preserve-header header row] + (if preserve-header + (zipmap-keys header row) ; custom zipmap that will put nil elements + (zipmap header row))) ; default zipmap will not put nil row elements (defn apply-kwargs diff --git a/src/semantic_csv/transducers.cljc b/src/semantic_csv/transducers.cljc index fea1fd3..8feb51b 100644 --- a/src/semantic_csv/transducers.cljc +++ b/src/semantic_csv/transducers.cljc @@ -24,9 +24,10 @@ * `:keyify` - bool; specify whether header/column names should be turned into keywords (default: `true`). * `:header` - specify the header to use for map keys, preventing first row of data from being consumed as header. + * `:preserve-header` - every row will have the full header even if values are missing from the CSV (default: `false`) * `:transform-header` - specify a transformation function for each header key (ignored if `:header` or `:keyify` is specified)." ([] (mappify {})) - ([{:as opts :keys [keyify transform-header header] :or {keyify true}}] + ([{:as opts :keys [keyify preserve-header transform-header header] :or {keyify true preserve-header false}}] (fn [rf] (let [hdr (volatile! (if keyify (mapv keyword header) @@ -41,7 +42,7 @@ keyify (mapv keyword input) :else input)) results) - (rf results (impl/mappify-row @hdr input))))))))) + (rf results (impl/mappify-row preserve-header @hdr input))))))))) ;; Here's an example using the mappify transducers. ;; diff --git a/test/semantic_csv/core_test.clj b/test/semantic_csv/core_test.clj index 583cd2e..b6003e7 100644 --- a/test/semantic_csv/core_test.clj +++ b/test/semantic_csv/core_test.clj @@ -17,6 +17,9 @@ (testing "mappify should not regard comments" (is (= (last (mappify data)) {:this "# some comment"}))) + (testing ":preserve-header will put nil values" + (is (= (last (mappify {:preserve-header true} data)) + {:this "# some comment" :that nil}))) (testing "mappify should not consume header if :header is specified" (is (= (first (mappify {:header ["foo" "bar"]} data)) {:foo "this" :bar "that"}))) diff --git a/test/semantic_csv/transducers_test.clj b/test/semantic_csv/transducers_test.clj index 310b3d8..55a512c 100644 --- a/test/semantic_csv/transducers_test.clj +++ b/test/semantic_csv/transducers_test.clj @@ -17,6 +17,9 @@ (testing "mappify should not regard comments" (is (= (last (into [] (mappify) data)) {:this "# some comment"}))) + (testing ":preserve-header will put nil values" + (is (= (last (into [] (mappify {:preserve-header true}) data)) + {:this "# some comment" :that nil}))) (testing "mappify should not consume header if :header is specified" (is (= (first (into [] (mappify {:header ["foo" "bar"]}) data)) {:foo "this" :bar "that"})))