Search code examples
clojureclojurescript

How to filter a collection of maps into group-by map by value?


Lets say I have a collection like:

(def xs 
  [{:name "Apple" :type "Fruit is a type"} 
  {:name "Tomato" :type "Vegetable are food"} 
  {:name "Pear" :type "the type can also be Fruit"} 
  {:name "Steak" :type "eat less Meat"}])

And I want to filter and group-by the collection into something like this:

{:Fruit [{:name "Apple" :type "Fruit is a type"} {:name "Pear" :type "the type can also be Fruit"}] :Vegetable [{:name "Tomato" :type "Vegetable are food"}]

I currently just filter the results but can't seem to figure out a good way to group-by. Here's what I have so far:

(defn filter-response [x query]
  (filter #(s/includes? (:type %) query) x))

(defn group-by-types [queries]
  (map #(filter-response xs %) queries))

(group-by-types ["Fruit" "Vegetable"])

How can I accomplish this?


Solution

  • Updated Answer

    You can use a list comprehension to check each item in the collection for each pattern.

    (defn- all-occurrences [xs patterns]
      (for [x xs
            pattern patterns
            :when (clojure.string/includes? (:type x) pattern)]
        [(keyword pattern) x]))
    

    Or using your filter-response function:

    (defn- all-occurrences [xs patterns]
      (for [pattern patterns
            x (filter-response xs pattern)]
        [(keyword pattern) x]))
    

    Then use reduce with update to merge the list of occurrences into a single map:

    (defn group-by-patterns [xs patterns]
      (reduce (fn [m [pattern text]] (update m pattern conj text))
              {}
              (all-occurrences xs patterns)))
    

    Calling it with the new input:

    (def xs
      [{:name "Apple" :type "Fruit is a type"}
       {:name "Tomato" :type "Vegetable are food"}
       {:name "Pear" :type "the type can also be Fruit"}
       {:name "Steak" :type "eat less Meat"}])
    
    (group-by-patterns xs ["Fruit" "Vegetable"])
    => {:Fruit ({:name "Pear", :type "the type can also be Fruit"} {:name "Apple", :type "Fruit is a type"}),
        :Vegetable ({:name "Tomato", :type "Vegetable are food"})}
    

    Original Answer

    First you can use group-by to group by values under specified keys:

    (def xs
       [{:name "Apple" :type "Fruit"}
        {:name "Tomato" :type "Vegetable"}
        {:name "Pear" :type "Fruit"}
        {:name "Steak" :type "Meat"}])
    
    erdos=> (group-by :type xs)
    {"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}], 
     "Vegetable" [{:name "Tomato", :type "Vegetable"}],
     "Meat" [{:name "Steak", :type "Meat"}]}
    

    Then use select-keys to filter the keys:

    erdos=> (select-keys (group-by :type xs) ["Fruit" "Vegetable"])
    {"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}], 
     "Vegetable" [{:name "Tomato", :type "Vegetable"}]}
    

    If you need keyword keys, you need an extra mapping step:

    erdos=> (into {}
                  (for [[k v] (select-keys (group-by :type xs) ["Fruit" "Vegetable"])]
                         [(keyword k) v]))
    {:Fruit [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}], 
     :Vegetable [{:name "Tomato", :type "Vegetable"}]}