Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging maps without overriding keys

Tags:

clojure

I have a clojure function that returns a sequence of 1-key maps. I want to merge these maps into one map; however, if there are maps with the same key, I don't want to overwrite the values, only to combine them into a vector. merge seems to overwrite, and merge-with seems to seriously distort the type.

I have:

({:foo "hello"}
 {:bar "world"} 
 {:baz "!!!"}
 {:ball {:a "abc", :b "123"}}
 {:ball {:a "def", :b "456"}}
 {:ball {:a "ghi", :b "789"}})

I'd like:

{:foo "hello"
 :bar "world"
 :baz "!!!"
 :ball [{:a "abc", :b "123"} {:a "def", :b "456"} {:a "ghi", :b "789"}]}

Thanks.

like image 760
Cameron Cook Avatar asked Oct 29 '14 19:10

Cameron Cook


2 Answers

(def data ...) ;; your list of maps

(apply merge-with (comp flatten vector) data)
;; => {:baz "!!!", :ball ({:b "123", :a "abc"} {:b "456", :a "def"} {:b "789", :a "ghi"}), :bar "world", :foo "hello"}

Note: the use of flatten works in OP's case but is NOT a general way to merge maps while creating vectors of values belonging to colliding keys.

like image 152
Kyle Avatar answered Nov 13 '22 02:11

Kyle


The "vector-safe" variant I could come up with has to iterate over all key-value-pairs twice:

(->> (for [[k vs] (group-by key (apply concat data))]
       (if (next vs)
         [k (mapv val vs)]
         (first vs)))
     (into {}))
;; => {:foo "hello",
;;     :bar "world",
;;     :baz "!!!",
;;     :ball [{:a "abc", :b "123"} ...]}

Basically, this groups all values by key and only removes the seq around them if it contains exactly one element.


The fully threaded version (for readability):

(->> (apply concat data)
     (group-by key)
     (map
       (fn [[k vs]]
         (if (next vs)
           [k (mapv val vs)]
           (first vs))))
     (into {}))
like image 36
xsc Avatar answered Nov 13 '22 02:11

xsc