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

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

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"]}})}

(apply merge-with into {}
(for [tool tools
class (get-in tool [:config :classes])]
{class [tool]}))

Related

How do I modify maps nested in vectors based on a series of values in Clojure?

Supposing I has a data structure like this:
[[{:name "bob" :favorite-color "green"}{:name "tim" :favorite-color "blue"}]
[{:name "eric" :favorite-color "orange"}{:name "jim" :favorite-color "purple"}]
[{:name "andy" :favorite-color "green"}{:name "tom" :favorite-color "blue"}]]
and an array like this:
["green" "purple"]
How would I pass over my data structure and augment all maps for folks who liked the colors in my array with a new key value pair of :likes-my-colors "yes" ?
The result would be:
[[{:name "bob" :favorite-color "green" :likes-my-colors "yes"}{:name "tim" :favorite-color "blue"}]
[{:name "eric" :favorite-color "orange"}{:name "jim" :favorite-color "purple" :likes-my-colors "yes"}]
[{:name "andy" :favorite-color "green" :likes-my-colors "yes"}{:name "tom" :favorite-color "blue"}]]
(I intentionally made the value a string of yes as opposed to true because that's closer to what I am trying to figure out).
I tried loop and recur with postwalk but couldn't figure out how to mutate the map with subsequent recursions. I won't paste my horrid attempt here because I am guessing there's a better way to do it then with recur. However, postwalk would have the advantage of being able to handle more an increasingly nested data structure, which will likely be the case. So maybe recur with postwalk is the way to go.
I'm using ClojureScript and Reagent to store app state in an atom... as things occur I need to keep updating the app state in that atom. The app state gets reset repeatedly in a single user session... it gets built up and modified after each reset. As in this example, the app state gets modified based on arrays. My code needs to work through the elements of the array and modify all the maps that meet a condition. Eventually, this structure is used to add classes to a Hiccup data structure. The UI changes accordingly; people in a list would have borders appear around them if they liked my colors, for example, by having a class added.
I had awesome help in learning how to look through a data structure like this and update all maps given a specific key/value pair... but I've run into trouble doing it with a series of values. In other words, 'build up' a map in a sense... but it's more 'modify with multiple passes'. That phrasing will hopefully improve as my understanding does.
I am wondering, as a side note, how Clojure users go about accessing and mutating elements buried deeply in nested data structures. I'd rather have more complicated data structures but I avoid them because it seems hard to modify deep elements. I'm suspecting they might use libraries. It seems like there may be an easier way of getting at and modifying complex structures than writing brain teaser (for me) code. But then again, I may be wrong. There are a lot of examples online but they are often about modifying simple structures.
i would start with an item updater, like this for example:
(defn handle-fav-colors [color-set data]
(if (color-set (:favorite-color data))
(assoc data :likes-my-color "yes")
data))
then you would be free to update your data any way you like. Like mapping:
user> (mapv (partial mapv (partial handle-fav-colors #{"green" "purple"})) data)
;;=> [[{:name "bob", :favorite-color "green", :likes-my-color "yes"}
;; {:name "tim", :favorite-color "blue"}]
;; [{:name "eric", :favorite-color "orange"}
;; {:name "jim", :favorite-color "purple", :likes-my-color "yes"}]
;; [{:name "andy", :favorite-color "green", :likes-my-color "yes"}
;; {:name "tom", :favorite-color "blue"}]]
i won't personally recommend using walkers for this, since this one has a regular structure, while walkers go indefinitely deep, leading to a non-zero possibility to mess up some deeply nested maps. The rule of thumb (works for me): when you know the exact level in data structure you need to operate at, you should not use tools possibly operating above or below this level.
Also, as clojure (and FP in general) is all about small composable and reusable utils, you could approach with first making up the proper general functions like nested collections mapping:
(defn mapv-deep [level f data]
(if (pos? level)
(mapv (partial mapv-deep (dec level) f) data)
(mapv f data)))
user> (mapv-deep 0 inc [1 2 3])
;; [2 3 4]
user> (mapv-deep 1 inc [[1 2] [3 4]])
;; [[2 3] [4 5]]
user> (mapv-deep 2 inc [[[1 2] [3 4]] [[5 6] [7 8]]])
;; [[[2 3] [4 5]] [[6 7] [8 9]]]
and conditional analog of assoc
(defn assoc-when [data pred k v]
(if (pred data)
(assoc data k v)
data))
user> (assoc-when {:a 10 :b 20} #(-> % :a even?) :even-a? true)
;;=> {:a 10, :b 20, :even-a? true}
user> (assoc-when {:a 11 :b 20} #(-> % :a even?) :even-a? true)
;;=> {:a 11, :b 20}
so now the task can be solved this way:
(defn handle-fav-colors [color-set data]
(assoc-when data (comp color-set :favorite-color) :likes-my-color "yes"))
user> (mapv-deep 1 (partial handle-fav-colors #{"green" "purple"}) data)
;;=> [[{:name "bob", :favorite-color "green", :likes-my-color "yes"}
;; {:name "tim", :favorite-color "blue"}]
;; [{:name "eric", :favorite-color "orange"}
;; {:name "jim", :favorite-color "purple", :likes-my-color "yes"}]
;; [{:name "andy", :favorite-color "green", :likes-my-color "yes"}
;; {:name "tom", :favorite-color "blue"}]]
Lets write an utility function first for handling a single map. It will check if the value under :favorite-color is present in favorite-colors. Since favorite-colors is a vector, we need to convert it to a set so we can use contains? on it.
(defn handle-map [m]
(if (contains? (set favorite-colors) (:favorite-color m))
(assoc m :likes-my-colors "yes")
m))
Now we can use postwalk to call it on all map nodes:
(clojure.walk/postwalk
(fn [m]
(if (map? m)
(handle-map m)
m))
data)

Clojure parse nested vectors

I am looking to transform a clojure tree structure into a map with its dependencies
For example, an input like:
[{:value "A"}
[{:value "B"}
[{:value "C"} {:value "D"}]
[{:value "E"} [{:value "F"}]]]]
equivalent to:
:A
:B
:C
:D
:E
:F
output:
{:A [:B :E] :B [:C :D] :C [] :D [] :E [:F] :F}
I have taken a look at tree-seq and zippers but can't figure it out!
Here's a way to build up the desired map while using a zipper to traverse the tree. First let's simplify the input tree to match your output format (maps of :value strings → keywords):
(def tree
[{:value "A"}
[{:value "B"} [{:value "C"} {:value "D"}]
{:value "E"} [{:value "F"}]]])
(def simpler-tree
(clojure.walk/postwalk
#(if (map? %) (keyword (:value %)) %)
tree))
;; [:A [:B [:C :D] :E [:F]]]
Then you can traverse the tree with loop/recur and clojure.zip/next, using two loop bindings: the current position in tree, and the map being built.
(loop [loc (z/vector-zip simpler-tree)
deps {}]
(if (z/end? loc)
deps ;; return map when end is reached
(recur
(z/next loc) ;; advance through tree
(if (z/branch? loc)
;; for (non-root) branches, add top-level key with direct descendants
(if-let [parent (some-> (z/prev loc) z/node)]
(assoc deps parent (filterv keyword? (z/children loc)))
deps)
;; otherwise add top-level key with no direct descendants
(assoc deps (z/node loc) [])))))
=> {:A [:B :E], :B [:C :D], :C [], :D [], :E [:F], :F []}
This is easy to do using the tupelo.forest library. I reformatted your source data to make it fit into the Hiccup syntax:
(dotest
(let [relationhip-data-hiccup [:A
[:B
[:C]
[:D]]
[:E
[:F]]]
expected-result {:A [:B :E]
:B [:C :D]
:C []
:D []
:E [:F]
:F []} ]
(with-debug-hid
(with-forest (new-forest)
(let [root-hid (tf/add-tree-hiccup relationhip-data-hiccup)
result (apply glue (sorted-map)
(forv [hid (all-hids)]
(let [parent-tag (grab :tag (hid->node hid))
kid-tags (forv [kid-hid (hid->kids hid)]
(let [kid-tag (grab :tag (hid->node kid-hid))]
kid-tag))]
{parent-tag kid-tags})))]
(is= (format-paths (find-paths root-hid [:A]))
[[{:tag :A}
[{:tag :B} [{:tag :C}] [{:tag :D}]]
[{:tag :E} [{:tag :F}]]]])
(is= result expected-result ))))))
API docs are here. The project README (in progress) is here. A video from the 2017 Clojure Conj is here.
You can see the above live code in the project repo.

Get two different keywords from map

I just started learning Clojure and I'd like to get two keywords from a vector of maps.
Let's say there's a vector
(def a [{:id 1, :description "bla", :amount 12, :type "A", :other "x"} {:id 2, :description "blabla", :amount 10, :type "B", :other "y"}])
And I'd like to get a new vector
[{"bla" 12} {"blabla" 10}]
How can I do that??
Thanks!
Assuming you want the :description and :amount separately, not maps that map one to the other, you can use juxt to retrieve both at the same time:
(mapv (juxt :description :amount) a)
;; => [["bla" 12] ["blabla" 10]]
If you actually did mean to make maps, you can use for instance apply and hash-map to do that:
(mapv #(apply hash-map ((juxt :description :amount) %)) a)
;; => [{"bla" 12} {"blabla" 10}]
You can use mapv to map over the source vector. Within the transform function you can destructure each map to extract the keys you want and construct the result:
(mapv (fn [{:keys [description amount]}] {description amount}) a)
(mapv #(hash-map (:description %) (:amount %)) a)

How best to update this tree?

I've got the following tree:
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}
]
}
}
I want to update the people and projects subtrees by adding a :name key-value pair.
Assuming I have these maps to perform the lookup:
(def people {1 "Susan" 2 "John")
(def projects {1 "Foo" 2 "Bar" 3 "Qux")
How could I update the original tree so that I end up with the following?
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:name "Susan"
:projects [{:id 1 :name "Foo"} {:id 2 :name "Bar"}]}
{:id 2
:name "John"
:projects [{:id 1 :name "Foo"} {:id 3 :name "Qux"}]}
]
}
}
I've tried multiple combinations of assoc-in, update-in, get-in and map calls, but haven't been able to figure this out.
I have used letfn to break down the update into easier to understand units.
user> (def tree {:start_date "2014-12-07"
:data {:people [{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}]}})
#'user/tree
user> (def people {1 "Susan" 2 "John"})
#'user/people
user> (def projects {1 "Foo" 2 "Bar" 3 "Qux"})
#'user/projects
user>
(defn integrate-tree
[tree people projects]
;; letfn is like let, but it creates fn, and allows forward references
(letfn [(update-person [person]
;; -> is the "thread first" macro, the result of each expression
;; becomes the first arg to the next
(-> person
(assoc :name (people (:id person)))
(update-in [:projects] update-projects)))
(update-projects [all-projects]
(mapv
#(assoc % :name (projects (:id %)))
all-projects))]
(update-in tree [:data :people] #(mapv update-person %))))
#'user/integrate-tree
user> (pprint (integrate-tree tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
nil
Not sure if entirely the best approach:
(defn update-names
[tree people projects]
(reduce
(fn [t [id name]]
(let [person-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (:people (:data t)))))
temp (assoc-in t [:data :people person-idx :name] name)]
(reduce
(fn [t [id name]]
(let [project-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (get-in t [:data :people person-idx :projects]))))]
(if project-idx
(assoc-in t [:data :people person-idx :projects project-idx :name] name)
t)))
temp
projects)))
tree
people))
Just call it with your parameters:
(clojure.pprint/pprint (update-names tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
With nested reduces
Reduce over the people to update corresponding names
For each people, reduce over projects to update corresponding names
The noisesmith solution looks better since doesn't need to find person index or project index for each step.
Naturally you tried to assoc-in or update-in but the problem lies in your tree structure, since the key path to update John name is [:data :people 1 :name], so your assoc-in code would look like:
(assoc-in tree [:data :people 1 :name] "John")
But you need to find John's index in the people vector before you can update it, same things happens with projects inside.

What is the idiomatic way to assoc several keys/values in a nested map in Clojure?

Imagine you have a map like this:
(def person {
:name {
:first-name "John"
:middle-name "Michael"
:last-name "Smith" }})
What is the idiomatic way to change values associated with both :first-name and :last-name in one expression?
(Clarification: Let's say you want to set :first-name to "Bob" and :last-name to "Doe". Let's also say that this map has some other values in it that we want to preserve, so constructing it from scratch is not an option)
Here are a couple of ways.
user> (update-in person [:name] assoc :first-name "Bob" :last-name "Doe")
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}
user> (update-in person [:name] merge {:first-name "Bob" :last-name "Doe"})
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}
user> (update-in person [:name] into {:first-name "Bob" :last-name "Doe"})
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}
user> (-> person
(assoc-in [:name :first-name] "Bob")
(assoc-in [:name :last-name] "Doe"))
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}
Edit
update-in does recursive assocs on your map. In this case it's roughly equivalent to:
user> (assoc person :name
(assoc (:name person)
:first-name "Bob"
:last-name "Doe"))
The repetition of keys becomes more and more tedious as you go deeper into a series of nested maps. update-in's recursion lets you avoid repeating keys (e.g. :name) over and over; intermediary results are stored on the stack between recursive calls. Take a look at the source for update-in to see how it's done.
user> (def foo {:bar {:baz {:quux 123}}})
#'user/foo
user> (assoc foo :bar
(assoc (:bar foo) :baz
(assoc (:baz (:bar foo)) :quux
(inc (:quux (:baz (:bar foo)))))))
{:bar {:baz {:quux 124}}}
user> (update-in foo [:bar :baz :quux] inc)
{:bar {:baz {:quux 124}}}
assoc is dynamic (as are update-in, assoc-in, and most other Clojure functions that operate on Clojure data structures). If assoc onto a map, it returns a map. If you assoc onto a vector, it returns a vector. Look at the source for assoc and take a look in in RT.java in the Clojure source for details.
I'm not sure if my case is quite the same but I had list of changes to apply:
(def updates [[[:name :first-name] "Bob"]
[[:name :last-name] "Doe"]])
In that case you can reduce over that list with assoc-in like this:
(reduce #(assoc-in %1 (first %2) (second %2)) person updates)