What is the clojure way to update a map inside a vector e.g. if I have something like this, assuming each map has unique :name
(def some-vec
[{:name "foo"
....}
{:name "bar"
....}
{:name "baz"
....}])
I want to update the map in someway if it has :name equal to foo. Currently I'm using map, like this
(map (fn [{:keys [name] :as value}]
(if-not (= name "foo")
value
(do-something .....))) some-vec)
But this will loop through the entire vector even though I only update one item.
Keep the data as a map instead of a vector of map-records, keyed by :name.
(def some-data
{"foo" {:name "foo" :other :stuff}
"bar" {:name "bar" :other :stuff}
"baz" {:name "baz" :other :stuff}})
Then
(assoc-in some-data ["bar" :other] :things)
produces
{"foo" {:other :stuff, :name "foo"},
"bar" {:other :things, :name "bar"},
"baz" {:other :stuff, :name "baz"}}
in one go.
You can capture the basic manipulation in
(defn assoc-by-fn [data keyfn datum]
(assoc data (keyfn datum) datum))
When, for example,
(assoc-by-fn some-data :name {:name "zip" :other :fassner})
produces
{"zip" {:other :fassner, :name "zip"},
"foo" {:other :stuff, :name "foo"},
"bar" {:other :stuff, :name "bar"},
"baz" {:other :stuff, :name "baz"}}
Given that you have a vector of maps, your code looks fine to me. Your concern about "looping through the entire vector" is a natural consequence of the fact that you're doing a linear search for the :name and the fact that vectors are immutable.
I wonder whether what you really want is a vector of maps? Why not a map of maps?
(def some-map
{"foo" {...}
"bar" (...}
"baz" {...}}
Which you could then update with update-in?
Given this shape of the input data and unless you have an index that can tell you which indices the maps with a given value of :name reside at, you will have to loop over the entire vector. You can, however, minimize the amount of work involved in producing the updated vector by only "updating" the matching maps, rather than rebuilding the entire vector:
(defn update-values-if
"Assumes xs is a vector. Will update the values for which
pred returns true."
[xs pred f]
(let [lim (count xs)]
(loop [xs xs i 0]
(if (< i lim)
(let [x (nth xs i)]
(recur (if (pred x)
(assoc xs i (f x))
xs)
(inc i)))
xs))))
This will perform as many assoc operations as there are values in xs for which pred returns a truthy value.
Example:
(def some-vec [{:name "foo" :x 0} {:name "bar" :x 0} {:name "baz" :x 0}])
(update-values-if some-vec #(= "foo" (:name %)) #(update-in % [:x] inc))
;= [{:name "foo", :x 1} {:name "bar", :x 0} {:name "baz", :x 0}]
Of course if you're planning to transform the vector in this way with some regularity, then Thumbnail's and Paul's suggestion to use a map of maps will be a much more significant improvement. That remains the case if :name doesn't uniquely identify the maps – in that case, you could simply transform your original vector using frequencies and deal with a map of vectors (of maps with a given :name).
If you're working with vector, you should know index of element that you want to change, otherwise you have to traverse it in some way.
I can propose this solution:
(defn my-update [coll val fnc & args]
(let [index (->> (map-indexed vector coll)
(filter (fn [[_ {x :name}]] (= x val)))
ffirst)]
(when index
(apply update-in coll [index] fnc args))))
Where:
coll - given collection of maps;
val - value of field :name;
fnc - updating function;
args - arguments of the updating function.
Let's try it:
user> (def some-vec
[{:name "foo"}
{:name "bar"}
{:name "baz"}])
;; => #'user/some-vec
user> (my-update some-vec "foo" assoc :boo 12)
;; => [{:name "foo", :boo 12} {:name "bar"} {:name "baz"}]
user> (my-update some-vec "bar" assoc :wow "wow!")
;; => [{:name "foo"} {:name "bar", :wow "wow!"} {:name "baz"}]
I think that Thumbnail's answer may be quite useful for you. If you can keep your data as a map, these manipulations become much easier. Here is how you can transform your vector into a map:
user> (apply hash-map (interleave (map :name some-vec) some-vec))
;; => {"foo" {:name "foo"}, "bar" {:name "bar"}, "baz" {:name "baz"}}
Related
I've been trying to map the nested values of a map within a vector into a vector of vectors without success.
The data I have is like this:
[{:country {:name "chile", :id 1},
:subcountries [{:name "talca", :id 2}
{:name "concepcion", :id 3}
{:name "puerto montt", :id 4}]}
{:country {:name "united states", :id 5},
:subcountries [{:name "boston", :id 6}
{:name "texas", :id 7}]}]
While the code I've been playing with vaguely returns an approximation of what I'm trying to get as a result:
(map
(fn [x]
(let [{{id :id name :name} :country
subcountries :subcountries} x]
[id
name
(map (fn [y] (let [{yid :id yname :yname} y] [yid yname])))]))
data)
The result I'm receiving with that is something pretty odd, since the vector I'd want to have is just a function:
([1 "chile" #function[clojure.core/map/fn--5862]]
[5 "united states" #function[clojure.core/map/fn--5862]])
What am I doing wrong?
Expected output should be something like:
[[["chile" 1] ["talca" 2] ["concepcion" 3] ["puerto montt" 4]]
[["united states" 5] ["boston" 6] ["texas" 7]]]
The reason you're seeing the function in your vector output is that your inner map wasn't applying the function to any data structure, so it was returning a transducer.
Here I've updated the inner map to map the function to the subcountries, which I assume was your intent. (There was also a tiny typo, you had yname :yname instead of yname :name)
(defn f [data]
(mapv
(fn [x]
(let [{{id :id name :name} :country
subcountries :subcountries} x]
[id
name
(mapv (fn [y] (let [{yid :id yname :name} y] [yid yname])) subcountries)]))
data))
Not sure if this is exactly your desired output, since you said "something like...". If not, let us know if you need more help getting it the rest of the way there.
> (f data)
[[1 "chile" [[2 "talca"] [3 "concepcion"] [4 "puerto montt"]]]
[5 "united states" [[6 "boston"] [7 "texas"]]]]
This might be a more "Clojurey" way to do it:
(defn countries->vecs [data]
(let [->pair (juxt :name :id)
map->pairs (fn [{:keys [country subcountries]}]
(apply vector (->pair country)
(map ->pair subcountries)))]
(mapv map->pairs data)))
May be, it is a stupid question, but it may help many of newbies. How do I add a key-value pair to the map?
I mean something like:
(defn init-item [v item]
(let [{:keys [id value]} item]
(-> v
(assoc :{ID_AS_A_KEY} value))))
And I get:
(init-item {} {:id "123456789" :value [:name "King" :surname "Leonid"]})
user=> {:123456789 [:name "King" :surname "Leonid"]}
Just don't do it. Use the string itself as your map key. There's no reason to make it a keyword. It's much easier to work with if you leave it alone.
(defn init-item [v item]
(assoc v (:id item) (:value item)))
I think this is what you meant to do:
(defn init-item
[dest-map item]
(let [item-id-str (:id item)
item-val (:value item)
item-id-kw (keyword item-id-str)]
(assoc dest-map item-id-kw item-val)))
(let [all-items {:a 1 :b 2 :c 3}
item-1 {:id "123456789"
:value [:name "King" :surname "Leonid"]}]
(init-item all-items item-1)
;=> {:a 1, :b 2, :c 3, :123456789 [:name "King" :surname "Leonid"]}
Clojure has functions name, symbol, and keyword to convert between strings and symbols/keywords. Since you already have the ID as a string, you just need to call keyword to convert it.
Be sure to always keep a browser tab open to The Clojure CheatSheet.
I have a collection of maps
(def a '({:id 9345 :value 3 :type "orange"}
{:id 2945 :value 2 :type "orange"}
{:id 145 :value 3 :type "orange"}
{:id 2745 :value 6 :type "apple"}
{:id 2345 :value 6 :type "apple"}))
I want to group this first by value, followed by type.
My output should look like:
{
:orange [{
:value 3,
:id [9345, 145]
}, {
:value 2,
:id [2935]
}],
:apple [{
:value 6,
:id [2745, 2345]
}]
}
How would I do this in Clojure? Appreciate your answers.
Thanks!
Edit:
Here is what I had so far:
(defn by-type-key [data]
(group-by #(get % "type") data))
(reduce-kv
(fn [m k v] (assoc m k (reduce-kv
(fn [sm sk sv] (assoc sm sk (into [] (map #(:id %) sv))))
{}
(group-by :value (map #(dissoc % :type) v)))))
{}
(by-type-key a))
Output:
=> {"orange" {3 [9345 145], 2 [2945]}, "apple" {6 [2745 2345], 3 [125]}}
I just couldnt figure out how to proceed next...
Your requirements are a bit inconsistent (or rather irregular) - you use :type values as keywords in the result, but the rest of the keywords are carried through. Maybe that's what you must do to satisfy some external formats - otherwise you need to either use the same approach as with :type through, or add a new keyword to the result, like :group or :rows and keep the original keywords intact. I will assume the former approach for the moment (but see below, I will get to the shape as you want it,) so the final shape of data is like
{:orange
{:3 [9345 145],
:2 [2945]},
:apple
{:6 [2745 2345]}
}
There is more than one way of getting there, here's the gist of one:
(group-by (juxt :type :value) a)
The result:
{["orange" 3] [{:id 9345, :value 3, :type "orange"} {:id 145, :value 3, :type "orange"}],
["orange" 2] [{:id 2945, :value 2, :type "orange"}],
["apple" 6] [{:id 2745, :value 6, :type "apple"} {:id 2345, :value 6, :type "apple"}]}
Now all rows in your collection are grouped by the keys you need. From this, you can go and get the shape you want, say to get to the shape above you can do
(reduce
(fn [m [k v]]
(let [ks (map (comp keyword str) k)]
(assoc-in m ks
(map :id v))))
{}
(group-by (juxt :type :value) a))
The basic idea is to get the rows grouped by the key sequence (and that's what group-by and juxt do,) and then combine reduce and assoc-in or update-in to beat the result into place.
To get exactly the shape you described:
(reduce
(fn [m [k v]]
(let [type (keyword (first k))
value (second k)
ids (map :id v)]
(update-in m [type]
#(conj % {:value value :id ids}))))
{}
(group-by (juxt :type :value) a))
It's a bit noisy, and it might be harder to see the forest for the trees - that's why I simplified the shape, to highlight the main idea. The more regular your shapes are, the shorter and more regular your functions become - so if you have control over it, try to make it simpler for you.
I would do the transform in two stages (using reduce):
the first to collect the values
the second for formating
The following code solves your problem:
(def a '({:id 9345 :value 3 :type "orange"}
{:id 2945 :value 2 :type "orange"}
{:id 145 :value 3 :type "orange"}
{:id 2745 :value 6 :type "apple"}
{:id 2345 :value 6 :type "apple"}))
(defn standardise [m]
(->> m
;; first stage
(reduce (fn [out {:keys [type value id]}]
(update-in out [type value] (fnil #(conj % id) [])))
{})
;; second stage
(reduce-kv (fn [out k v]
(assoc out (keyword k)
(reduce-kv (fn [out value id]
(conj out {:value value
:id id}))
[]
v)))
{})))
(standardise a)
;; => {:orange [{:value 3, :id [9345 145]}
;; {:value 2, :id [2945]}],
;; :apple [{:value 6, :id [2745 2345]}]}
the output of the first stage is:
(reduce (fn [out {:keys [type value id]}]
(update-in out [type value] (fnil #(conj % id) [])))
{}
a)
;;=> {"orange" {3 [9345 145], 2 [2945]}, "apple" {6 [2745 2345]}}
You may wish to use the built-in function group-by. See http://clojuredocs.org/clojure.core/group-by
I'm having trouble finding solution to the following problem:
Lets say I have a map:
(def defaults {
:name "John"
:surname "Doe"
:info {:date-of-birth "01-01-1980"
:registered [{:type "newsletter" :name "breaking news" }]}
})
And then I pass a similar structured map but I want to conjoin the vectors and overwrite the rest of the keys:
(def new {
:name "Peter"
:info {:date-of-birth "11-01-1986"
:registered [{:type "alert" :name "mobile-alert" }]}
})
And I want this result:
{:name "Peter"
:surname "Doe"
:info {:date-of-birth "11-01-1986"
:registered [{:type "newsletter" :name "breaking news" }
{:type "alert" :name "mobile-alert" }]}}
Now I can do this easily by using static syntax like:
(reduce conj (get-in defaults [:info :registered]) (get-in new [:info :registered]))
(There is probably a better way...) But I was hoping more of a dynamic function with the following properties:
Keep all keys from both maps, without knowing the structure
Update any keys with the values from the right map
if the val of a key is a vector, then conj the vector with the vector of the right map (if the appropriate key exists of course)
Thanks for the help in advance :)
You should definitely look at merge-with function. This is possible implementation:
(defn deep-merge [a b]
(merge-with (fn [x y]
(cond (map? y) (deep-merge x y)
(vector? y) (concat x y)
:else y))
a b))
Here's a possible implementation for this kind of functionality. It is at least a starting point, you may need some extra validations depending on the possible structure of your data (e.g. what if the overriding map's value is a vector but the value in the default map is not even a collection?).
(declare merge-maps)
(defn merge-map [x [k v]]
(cond (vector? v)
(assoc x k (vec (reduce conj (x k) v)))
(map? v)
(assoc x k (merge-maps (x k) v))
:esle
(assoc x k v)))
(defn merge-maps [x y]
(reduce merge-map x y))
(merge-maps defaults new)
;= {:info {:date-of-birth "11-01-1986",
;= :registered [{:name "breaking news", :type "newsletter"}
;= {:name "mobile-alert", :type "alert"}]},
;= :name "Peter",
;= :surname "Doe"}
I am having a bit of difficulty with Lists in Clojure
I have a quick question concerning the filter function
Let's say I have a List composed of Maps
My code is:
(def Person {:name Bob } )
(def Person2 {:name Eric } )
(def Person3 {:name Tim } )
(def mylist (list Person Person2 Person3))
How would i go about filtering my list so that , for example: I want the list minus Person2 (meaning minus any map that has :name Eric)
Thank you very much to everybody helping me out. This is my last question I promise
For this purpose, it's better to use the 'remove' function. It takes a sequence, and removes elements on which it's predicate returns 'true'. It's basically the opposite of filter. Here is an example of it, and filter's usage for the same purposes, that I worked up via the REPL.
user> (def m1 {:name "eric" :age 32})
#'user/m1
user> (def m2 {:name "Rayne" :age 15})
#'user/m2
user> (def m3 {:name "connie" :age 44})
#'user/m3
user> (def mylist (list m1 m2 m3))
#'user/mylist
user> (filter #(not= (:name %) "eric") mylist)
({:name "eric", :age 32})
user> (remove #(= (:name %) "eric") mylist)
({:name "Rayne", :age 15} {:name "connie", :age 44})
As you can see, remove is a little bit cleaner, because you don't have to use not=. Also, when working with maps, you don't have to use the 'get' function unless you want it to return something special if a key isn't in the map. If you know the key you're looking for will be in the map, there is no reason to use 'get'. Good luck!
Suppose you have something like this:
(def Person {:name "Bob" } )
(def Person2 {:name "Eric" } )
(def Person3 {:name "Tim" } )
(def mylist (list Person Person2 Person3))
This would work:
(filter #(not= "Eric" (get % :name)) mylist)
user=> (filter (fn [person] (not= (person :name) "Eric")) mylist)
({:name "Bob"} {:name "Tim"})
or using a more compact syntax:
user=> (filter #(not= (% :name) "Eric") mylist)
({:name "Bob"} {:name "Tim"})