Search code examples
clojureclojurescript

Clojure / Clojurescript: group-by a map on multiple values


Given a data structure, I'd like to re-structure it to be grouped by one of the nested values. These values are vectors, and whenever I encounter more than one value, I'm getting stuck.

Given a vector of maps like this:

(def tools 
 [{:name "A",
   :config
   {:accepts ["id"],
    :classes ["X"]}}
  {:name "B",
   :config
   {:accepts ["id"],
    :classes ["X", "Y"]
    }}])

I can almost get what I want - the values sorted by "classes" as a key, with values repeated if need be - by running group-by:

(group-by #(get-in % [:config :classes]) tools)

But it takes the whole vector in :classes as the key.

 {["X"] [{:name "A", 
          :config {:accepts ["id"], 
                   :classes ["X"]}}], 
  ["X" "Y"] [{:name "B", 
              :config {:accepts ["id"], 
                       :classes ["X" "Y"]}}]}

What I really want is to copy the values once per class, to look like this:

 {"X" [{:name "A"
        :config {:accepts ["id"]
                 :classes ["X"]}}
       {:name "B"
        :config {:accepts ["id"]
                 :classes ["X" "Y"]}}]
  "Y" [{:name "B"
        :config {:accepts ["id"]
                 :classes ["X" "Y"]}}]}

I'm not quite sure how to handle this given that I have multiple values in classes.

Working repl demo: https://repl.it/@YoYehudi/FamiliarDisguisedXiphiasgladius


Solution

  • Here's a way to do it using a nested reduce:

    (defn aggregate-classes [m tool]
      (->> (get-in tool [:config :classes])
           (reduce (fn [acc elem]
                     (update acc elem conj tool))
                   m)))
    
    (reduce aggregate-classes {} tools)
    =>
    {"X" ({:name "B", :config {:accepts ["id"], :classes ["X" "Y"]}} {:name "A", :config {:accepts ["id"], :classes ["X"]}}),
     "Y" ({:name "B", :config {:accepts ["id"], :classes ["X" "Y"]}})}