viebel
viebel

Reputation: 20670

In clojure, how to merge several maps combining mappings with same key into a list?

In Clojure, I would like to combine several maps into a single map where mappings with same key are combined into a list.

For example:

{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}

should lead to:

{:weather :sunny, :humor (:happy :sad :happy)}

I thought about:

(merge-with (comp flatten list) data)

But it is not efficient because flatten has O(n) complexity.

Then I came up with:

(defn agg[x y] (if (coll? x) (cons y x) (list y x)))
(merge-with agg data)

But it feels not idiomatic. Any other idea?

Upvotes: 21

Views: 7396

Answers (7)

amalloy
amalloy

Reputation: 91897

One approach would be

(defn merge-lists [& maps]
  (reduce (fn [m1 m2]
            (reduce (fn [m [k v]]
                      (update-in m [k] (fnil conj []) v))
                    m1, m2))
          {}
          maps))

It's a bit ugly, but that's only because your values aren't already lists. It also forces everything to be a list (so you'd get :weather [:sunny] rather than :weather :sunny). Frankly, this is likely to be a million times easier for you to work with anyway.

If you had each value as a vector already, you could simply do (apply merge-with into maps). So, another thing you could try would be to convert to vectors first, and then do the simple merge-with. This probably performs a bit worse than the first approach because of the intermediate data structures.

(defn merge-lists [& maps]
  (apply merge-with into
         (for [m maps, [k v] m]
           {k [v]})))

Upvotes: 14

sFritsch09
sFritsch09

Reputation: 442

If you have the same amount of keys in your collection:

(defn fn-format [maps]
  (reduce (fn [r k] ; result = {}, keys = [:humor, :weather]
           (assoc r k (mapv #(get % k) maps)))
           {}
           [:humor, :weather]))

(fn-format [{:humor :happy :weather :cloudy}
            {:humor :sad :weather :sunny}
            {:humor :happy :weather :rainy}
            {:weather :sunny :humor :happy}])

=> {:humor [:happy :sad :happy :happy], :weather [:cloudy :sunny :rainy :sunny]}

Upvotes: 0

DanLebrero
DanLebrero

Reputation: 8593

What about using group-by? It doesn't return exactly what you ask for but it is very similar:

user=> (group-by first (concat {:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny :humor :whooot}))
{:humor [[:humor :happy] [:humor :sad] [:humor :happy] [:humor :whooot]], :weather [[:weather :sunny]]}

Or with a small modification to the group-by function:

(defn group-by-v2
 [f vf coll]
  (persistent!
    (reduce
     (fn [ret x]
       (let [k (f x)]
         (assoc! ret k (conj (get ret k []) (vf x)))))
     (transient {}) coll)))

becomes:

user=> (group-by-v2 key val (concat {:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny :humor :whooot}))
{:humor [:happy :sad :happy :whooot], :weather [:sunny]}

Upvotes: 1

Aleksei Averchenko
Aleksei Averchenko

Reputation: 1776

Merge with this function:

(defn acc-list [x y]
  (let [xs (if (seq? x) x (cons x nil))]
    (cons y xs)))

Upvotes: 1

user845492
user845492

Reputation: 1

Here's a solution where every value is represented as lists, even if singletons:

(->> [{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}]
     (map first)
     (reduce (fn [m [k v]] (update-in m [k] #(cons v %))) {}))

=> {:weather (:sunny), :humor (:happy :sad :happy)}

If you don't want to wrap singletons in a list then I thought your original solution was just fine. The only way to make it more idiomatic is to use core.match.

(->> [{:humor :happy} {:humor :sad} {:humor :happy} {:weather :sunny}]
     (apply merge-with #(match %1
                               [& _] (conj %1 %2)
                               :else [%1 %2])))

=> {:weather :sunny, :humor [:happy :sad :happy]}

Upvotes: 0

kotarak
kotarak

Reputation: 17299

@amalloy's answer can flattened a little bit by using for.

(reduce (fn [m [k v]] (update-in m [k] (fnil conj []) v)) {} (for [m data entry m] entry))

Source for this technique: http://clj-me.cgrand.net/2010/01/19/clojure-refactoring-flattening-reduces/

Upvotes: 2

mikera
mikera

Reputation: 106351

You could try the following, I think it's pretty efficient

(reduce 
  (fn [m pair] (let [[[k v]] (seq pair)]
                 (assoc m k (cons v (m k))))) 
  {} 
  data)

=> {:weather (:sunny), :humor (:happy :sad :happy)}

Upvotes: 2

Related Questions