Reputation: 20670
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
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
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
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
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
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
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
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