2

There are a few SO posts related to this topic, however I could not find anything that works for what I am looking to accomplish.

I have a vector of maps. I am going to use the example from another related SO post:

(def data [{:id 1 :first-name "John1" :last-name "Dow1" :age "14"} {:id 2 :first-name "John2" :last-name "Dow2" :age "54"} {:id 3 :first-name "John3" :last-name "Dow3" :age "34"} {:id 4 :first-name "John4" :last-name "Dow4" :age "12"} {:id 5 :first-name "John5" :last-name "Dow5" :age "24"}])) 

I would like to convert this into a map with the values of each entry be a vector of the associated values (maintaining the order of data).

Here is what I would like to have as the output:

{:id [1 2 3 4 5] :first-name ["John1" "John2" "John3" "John4" "John5"] :last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"] :age ["14" "54" "34" "12" "24"]} 

Is there an elegant and efficient way to do this in Clojure?

2
  • You can use reduce to do this, but not sure how elegant that would be. :D Commented Jan 25, 2019 at 14:44
  • Yeah. I thought Clojure would provide a more elegant solution than a straight reduce. Commented Jan 25, 2019 at 14:56

5 Answers 5

7

Can be made more efficient, but this is a nice start:

(def ks (keys (first data))) (zipmap ks (apply map vector (map (apply juxt ks) data))) ;;=> {:id [1 2 3 4 5] :first-name ["John1" "John2" "John3" "John4" "John5"] :last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"] :age ["14" "54" "34" "12" "24"]} 

Another one that comes close:

(group-by key (into [] cat data)) ;;=> {:id [[:id 1] [:id 2] [:id 3] [:id 4] [:id 5]], :first-name [[:first-name "John1"] [:first-name "John2"] [:first-name "John3"] [:first-name "John4"] [:first-name "John5"]], :last-name [[:last-name "Dow1"] [:last-name "Dow2"] [:last-name "Dow3"] [:last-name "Dow4"] [:last-name "Dow5"]], :age [[:age "14"] [:age "54"] [:age "34"] [:age "12"] [:age "24"]]} 
Sign up to request clarification or add additional context in comments.

1 Comment

I was suspecting group-by could play a big role but couldn't make it work. You were way faster, good job :)
3

Well, I worked out a solution and then before I could post, Michiel posted a more concise solution, but I'll go ahead and post it anyway =).

(defn map-coll->key-vector-map [coll] (reduce (fn [new-map key] (assoc new-map key (vec (map key coll)))) {} (keys (first coll)))) 

Comments

1

For me, the clearest approach here is the following:

(defn ->coll [x] (cond-> x (not (coll? x)) vector)) (apply merge-with #(conj (->coll %1) %2) data) 

Basically, the task here is to merge all maps (merge-with), and gather up all the values at the same key by conjoining (conj) onto the vector at key – ensuring that values are actually conjoined onto a vector (->coll).

1 Comment

Won't work if the keys are collections. The trouble is that merge-with doesn't let you specify a starting value. These days, I think we'd expect that to be the value returned by the zero arity of the merging function.
0

If we concatenate the maps into a single sequence of pairs, we have an edge-list representation of a graph. What we have to do is convert it into an adjacency-list (here a vector, not a list) representation.

(defn collect [maps] (reduce (fn [acc [k v]] (assoc acc k (conj (get acc k []) v))) {} (apply concat maps))) 

For example,

=> (collect data) {:id [1 2 3 4 5] :first-name ["John1" "John2" "John3" "John4" "John5"] :last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"] :age ["14" "54" "34" "12" "24"]} 

The advantage of this method over some of the others is that the maps in the given sequence can be different shapes.

Comments

0

Please consider the reader when writing code! There are no prizes for playing "code golf". There are, however, considerable costs to others when you force them to decipher code that is overly condensed.

I always try to be explicit about what code is doing. This is easiest if you break down a problem into simple steps and use good names. In particular, it is almost impossible to accomplish this using juxt or any other cryptic function.

Here is how I would implement the solution:

(def data [{:id 1 :first-name "John1" :last-name "Dow1" :age "14"} {:id 2 :first-name "John2" :last-name "Dow2" :age "54"} {:id 3 :first-name "John3" :last-name "Dow3" :age "34"} {:id 4 :first-name "John4" :last-name "Dow4" :age "12"} {:id 5 :first-name "John5" :last-name "Dow5" :age "24"}]) (def data-keys (keys (first data))) (defn create-empty-result "init result map with an empty vec for each key" [data] (zipmap data-keys (repeat []))) (defn append-map-to-result [cum-map item-map] (reduce (fn [result map-entry] (let [[curr-key curr-val] map-entry] (update-in result [curr-key] conj curr-val))) cum-map item-map)) (defn transform-data [data] (reduce (fn [cum-result curr-map] (append-map-to-result cum-result curr-map)) (create-empty-result data) data)) 

with results:

(dotest (is= (create-empty-result data) {:id [], :first-name [], :last-name [], :age []}) (is= (append-map-to-result (create-empty-result data) {:id 1 :first-name "John1" :last-name "Dow1" :age "14"}) {:id [1], :first-name ["John1"], :last-name ["Dow1"], :age ["14"]}) (is= (transform-data data) {:id [1 2 3 4 5], :first-name ["John1" "John2" "John3" "John4" "John5"], :last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"], :age ["14" "54" "34" "12" "24"]})) 

Note that I included unit tests for the helper functions as a way of both documenting what they are intended to do as well as demonstrating to the reader that they actually work as advertised.

This template project can be used to run the above code.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.