Using specter to transform values that match a key - clojure

I'm sorry if this has been answered elsewhere, but I can't seem to find an example that matches the pattern of what I'm looking for. I also may not yet understand recursive specter paths fully.
If I have the data (explicitly with the nested vector):
{:a "1" :b "2" :c [ {:a "3" :b "4"} {:a "5" :b "6"} ]}
And I'd like to apply the keyword function to all values with the key :a to result in:
{:a :1 :b "2" :c [ {:a :3 :b "4"} {:a :5 :b "6"} ]}
Finally, I'd like it to be recursive to an arbitrary depth, and handle the vector case as well.
I've read https://github.com/nathanmarz/specter/wiki/Using-Specter-Recursively , but I must be missing something critical.
Thanks to anyone pointing me in the right direction!

(use '[com.rpl.specter])
(let [input {:a "1" :b "2" :c [{:a "3" :b "4"} {:a "5" :b "6"}]}
desired-output {:a :1 :b "2" :c [{:a :3 :b "4"} {:a :5 :b "6"}]}
FIND-KEYS (recursive-path [] p (cond-path map? (continue-then-stay [MAP-VALS p])
vector? [ALL p]
STAY))]
(clojure.test/is
(= (transform [FIND-KEYS (must :a)] keyword input)
desired-output)))

Not a Specter solution, but it is easily done via clojure.walk/postwalk:
(ns demo.core
(:require
[clojure.walk :as walk] ))
(def data {:a "1" :b "2" :c [{:a "3" :b "4"} {:a #{7 8 9} :b "6"}]})
(def desired {:a :1 :b "2" :c [{:a :3 :b "4"} {:a #{7 8 9} :b "6"}]})
(defn transform
[form]
(if (map-entry? form)
(let [[key val] form]
(if (and
(= :a key)
(string? val))
[key (keyword val)] ; can return either a 2-vector
{key val})) ; or a map here
form))
(walk/postwalk transform data) =>
{:a :1, :b "2", :c [{:a :3, :b "4"} {:a #{7 9 8}, :b "6"}]}
I even put in a non-string for one of the :a values to make it trickier.

Related

Clojure - storing maps into a list of maps on keys

Say I have two maps in clojure.
(def map1 {:a 1 :b 1 :c nil :d 1})
(def map2 {:a 1 :b 2 :c 3 :d nil})
(def listofmaps '({:a 1 :b 1 :c nil :d 1} {:a 2 :b 2 :c 2 :d nil}))
If :a value matches with any map in listofmaps and map1, then if map1 :d is not null, put :d value from map1 into the matching map in listofmaps.
Like, first we compare map1 and listofmaps - now if map1 (:a 1) matches with any maps in listofmaps (:a 1), if map1 (:d not null) replace (matching map in listofmaps with map1 :d value) and if map1 (:c not null) replace (matching map in listofmaps with map1 :c value)
(def map1 {:a 1 :b 1 :c nil :d 1})
(def listofmaps '({:a 1 :b 1 :c nil :d 1} {:a 2 :b 2 :c 2 :d nil}))
Then map2 (:a 1) matches with a map in listofmaps (:a 1) and :
(def map2 {:a 1 :b 2 :c 3 :d nil})
(def listofmaps '({:a 1 :b 1 :c nil :d 1} {:a 2 :b 2 :c 2 :d nil}))
if map2 (:d not null) replace (matching map in listofmaps with map2 :d value) and if map2 (:c not null) replace (matching map in listofmaps with map2 :c value)
output=> '({:a 1 :b 1 :c 3 :d 1} {:a 2 :b 2 :c 2 :d nil})
it's not clear what is meant by in list of maps and map2, here is a reasonably common pattern of adding in missing values in priority order.
(let [map1 {:a 1 :b 1 :c nil :d 1}
map2 {:a 1 :b 2 :c 3 :d nil}
list-of-maps [{:a 1 :b 1 :c nil :d 1} {:a 2 :b 2 :c 2 :d nil}]
or-fn (fn [a b] (or a b))]
(->>
list-of-maps
(map #(merge-with or-fn % map1))
(map #(merge-with or-fn % map2))))
({:a 1, :b 1, :c 3, :d 1} {:a 2, :b 2, :c 2, :d 1})
I understood your question to be
If the value of the :a key in the new map matches the value of the :a key in any map in listofmaps, then, for each such matched map, replace the values of the keys :c and :d in that matched map in listofmaps by a new value only if the corresponding new value is not null.
Assuming that, here is an answer.
If I did not understand the question correctly, please clarify and show your desired output.
user> (def map1 {:a 1 :b 1 :c nil :d 1})
#'user/map1
user> (def map2 {:a 1 :b 2 :c 3 :d nil})
#'user/map2
user> (def listofmaps [{:a 1 :b 1 :c nil :d 1} {:a 2 :b 2 :c 2 :d nil}])
#'user/listofmaps
user> (defn mapper [m ms]
(mapv (fn [elem]
(if (= (:a elem) (:a m))
(merge-with #(or %1 %2) (select-keys m [:c :d]) elem)
elem))
maps))
#'user/mapper
user> (mapper map1 listofmaps)
[{:c nil, :d 1, :a 1, :b 1} {:a 2, :b 2, :c 2, :d nil}]
user> (mapper map2 listofmaps)
[{:c 3, :d 1, :a 1, :b 1} {:a 2, :b 2, :c 2, :d nil}]
user>

aggregate map values into a vector

I'm wondering if anyone can help me find the right function to use with merge-with to get the desired merging of map values as a single vector.
Thanks!
; works great -single vector
(merge-with vector {:a "b"} {:a "d"} {:a "c"})
; {:a ["b" "d"]}
; uh-oh... now we are beginning to nest each set
(merge-with vector {:a "b"} {:a "d"} {:a "c"})
;{:a [["b" "d"] "c"]}
; what I want:
; {:a ["b" "d" "c"]}
though the approach with flatten solves your concrete problem, it is not universal. Based on your question i would guess that you need a map of keyword to vector as a result. And it works, when all the maps contain exactly same keys. But guess the following corner cases:
user> (merge-with (comp flatten vector) {:a "b"})
;;=> {:a "b"} oops! you following processing probably wants {:a ["b"]}
user> (merge-with (comp flatten vector) {:a "b"} {:c "d"})
;;=> {:a "b", :c "d"} once again!
user> (merge-with (comp flatten vector) {:a ["b"]} {:a ["c" ["d"]]})
;;=> {:a ("b" "c" "d")}
;; here i can see some inconsistent behavior, breaking the initial data form: would't you rather want {:a [["b"] ["c" ["d"]]]} ?
so, given that you are doing something for production, rather then learning,
i would advice the following approach: you can make the function, merging maps, but also handling the single (or first) key appearing in the result the special way:
(defn smart-merge-with [first-val-fn merge-fn & args]
(when (seq args)
(reduce (fn [acc items-map]
(reduce (fn [acc [k v]]
(if (contains? acc k)
(update acc k merge-fn v)
(assoc acc k (first-val-fn v))))
acc items-map))
{} args)))
now you can just wrap the first value into a vector, and then, when there is another value with the same key appears just add it to that vector:
user> (smart-merge-with vector conj {:a 10 :b 30} {:a 20 :c 30} {:c 1} {:d 100})
;;=> {:a [10 20], :b [30], :c [30 1], :d [100]}
user> (smart-merge-with vector conj {:a [10] :b 30} {:a 20 :c 30} {:c 1} {:d 100})
{:a [[10] 20], :b [30], :c [30 1], :d [100]}
in addition, now you can add more sophisticated logic to the maps' merging, like for example some accumulation:
user> (smart-merge-with (fn [x] {:items [x] :sum x})
(fn [x y] (-> x
(update :items conj y)
(update :sum + y)))
{:a 10 :b 20} {:b 30 :c 40} {:c 1 :d 2})
;;=> {:a {:items [10], :sum 10},
;; :b {:items [20 30], :sum 50},
;; :c {:items [40 1], :sum 41},
;; :d {:items [2], :sum 2}}
From this answer we can use the same principle:
(merge-with (comp #(into [] % ) flatten vector) {:a "b"} {:a "d"} {:a "c"})
{:a ["b" "d" "c"]}
Or roll you own function:
(merge-with #(if (vector? %1) (conj %1 %2) (vector %1 %2)) {:a "b"} {:a "d"} {:a "c"})

merge two lists by some map key

I'd like to merge two lists by some map key as follow:
(def list1 '({:a 2 :b 2} {:a 1 :b 1}))
(def list2 '({:a 1 :c 1} {:a 2 :c 2}))
As result I'd like something like, using sort by :a for example:
'({:a 1 :b 1 :c 1} {:a 2 :b 2 :c 2})
Any ideas?
You can use join and sort-by:
(:require '[clojure.set :as s])
(sort-by :a (s/join list1 list2 {:a :a}))
Does this do it?
(def list1 '({:a 1 :b 1} {:a 2 :b 2}))
(def list2 '({:a 1 :c 1} {:a 2 :c 2}))
(println
(map merge list1 list2)
)
;=> ({:a 1, :b 1, :c 1} {:a 2, :b 2, :c 2})
UPDATE
(def list1 [ {:a 1 :b 1} {:a 2 :b 2} ] )
(def list2 [ {:a 2 :c 2} {:a 1 :c 1} ] )
(defn sort-merge [lista listb]
(map merge (sort-by :a lista) (sort-by :a listb)))
(println
(sort-merge list1 list2))
;=> ({:a 1, :b 1, :c 1} {:a 2, :b 2, :c 2})
another way is to use list comprehension:
user> (for [x list1
y list2
:when (= (:a x) (:a y))]
(merge x y))
({:a 2, :b 2, :c 2} {:a 1, :b 1, :c 1})

How to add fields to a map in Clojure?

I have a map like this:
{:a 1 :b 20}
: and I want to make sure that certain fields are not missing from the map:
(:a :b :c :d )
: Is there a function to merge the two, something like :
(merge-missing-keys {:a 1 :b 20} (:a :b :c :d ))
: which can produce :
{:a 1 :b 20 :c nil :d nil}
?
Update:
With some pointers from the answers I found that this can be done like this:
(defn merge-missing-keys [
a-set
some-keys
]
(merge-with
#(or %1 %2)
a-set
(into {} (map (fn[x] {x nil}) some-keys))))
(merge-missing-keys {:a 1 :b 20} '(:a :b :c :d :e ))
You should use merge-with:
Returns a map that consists of the rest of the maps conj-ed onto
the first. If a key occurs in more than one map, the mapping(s)
from the latter (left-to-right) will be combined with the mapping in
the result by calling (f val-in-result val-in-latter).
So the following will merge all maps with one actual value selected from the maps or nil.
(merge-with #(or %1 %2)
{:a 1 :b 2}
{:a nil :b nil :c nil :d nil})
; -> {:d nil :c nil :b 2 :a 1}
This will probably be enough for you to build your implementation.
You can always just merge into your default array as follows:
(merge
{:a nil :b nil :c nil :d nil} ; defaults
{:a 1 :b 20}) ; current values
=> {:a 1, :b 20, :c nil, :d nil}
A riff on #mikera's answer, to make it work when you don't have the keys available as literals:
(let [keys [:a :b :c :d]]
(merge (zipmap keys (repeat nil))
{:a 1 :b 20}))

How can I update a vector item in Clojure?

Given:
(def my-vec [{:id 0 :a "foo" :b "bar"} {:id 1 :a "baz" :b "spam"}
{:id 2 :a "qux" :b "fred"}])
How can I idiomatically update * the item in my-vec with :id=1 to have values :a="baz2" and :b="spam2"?
*: I recognize that I wouldn't actually be updating my-vec, but really returning a new vector that is identical to my-vec except for the replacement values.
Do you know ahead of time that the map with id == 1 is the second map in your vector? If so:
user> (-> my-vec
(assoc-in [1 :a] "baz2")
(assoc-in [1 :b] "spam2"))
[{:id 0, :a "foo", :b "bar"} {:id 1, :a "baz2", :b "spam2"} {:id 2, :a "qux", :b "fred"}]
If you need to access your data by id a lot, another idea is to replace your vector of hash-maps with a hash-map of hash-maps keyed on :id. Then you can more easily assoc-in no matter the order of things.
user> (def new-my-vec (zipmap (map :id my-vec) my-vec))
#'user/new-my-vec
user> new-my-vec
{2 {:id 2, :a "qux", :b "fred"}, 1 {:id 1, :a "baz", :b "spam"}, 0 {:id 0, :a "foo", :b "bar"}}
user> (-> new-my-vec
(assoc-in [1 :a] "baz2")
(assoc-in [1 :b] "spam2"))
{2 {:id 2, :a "qux", :b "fred"}, 1 {:id 1, :a "baz2", :b "spam2"}, 0 {:id 0, :a "foo", :b "bar"}}
map a function over the vector of maps that either creates a modified map if the key matches or uses the original if the keys don't match then turn the result back into a vector
(vec (map #(if (= (:id %) 1)
(assoc % :a "baz2" :b "spam2")
%)))
It is possible to do this more succinctly though this one really shows where the structural sharing occurs.
Might want to take a look at array-map which creates a map backed by an array and keyed by the index instead of using :id?