Suppose I have this vector of maps:
[{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}]
And I wish to select the map with :id 18347125, how would I do this?
I've tried
(for [map maps
:when (= (:id map) id)]
map)
This feels a bit ugly and returns a sequence of length one, and I want to return just the map.
IMHO, there are several ways to solve your problem, and the definitely idiomatic way is in the realm of taste. This is my solution where I simply translated "to select maps whose :id is 1834715" into Clojure.
user> (def xs [{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}])
#'user/xs
user> (filter (comp #{18347125} :id) xs)
({:title "Title1", :id 18347125})
The :id keyword is a function that looks up itself in a collection passed to it. The set #{18347125} is also a function that tests if a value passed to it equals 18347125. Using a Clojure set as a predicate function allows for a succinct idiom.
I'm not sure if it's the simplest way to write it, but I think this is more clear about your intentions:
(->> maps
(filter #(= (:id %) id))
first)
This doesn't do what you asked for exactly, but might be useful nonetheless:
user=> (group-by :id [{:title "Title1" :id 18347125}
{:title "Title2" :id 18347123}
{:title "Title3" :id 18341121}])
{18347125 [{:title "Title1" :id 18347125}]
18347123 [{:title "Title2" :id 18347123}]
18341121 [{:title "Title3" :id 18341121}]}
Now you can simply look the map up by id. Read more about group-by on clojuredocs, its a very useful function.
Note that it puts the maps inside vectors. This is because group-by is designed to handle grouping (ie multiple items with the same key):
user=> (group-by :id [{:title "Title1" :id 123}
{:title "Title2" :id 123}
{:title "Title3" :id 18341121}])
{123 [{:title "Title1" :id 123} {:title "Title2" :id 123}]
18341121 [{:title "Title3" :id 18341121}]}
If you need to query not just once, but multiple times for maps with specific IDs, I would suggest to make your data types match your use case, i.e. to change the vector into a map:
(def maps-by-id (zipmap (map :id maps) maps))
So now your IDs are the keys in this new map of maps:
user=> (maps-by-id 18347125)
{:title "Title1", :id 18347125}
Related
I have the following map:
(def gigs {:gig-01 {:id :gig-01
:title "Macaron"
:artist "Baher Khairy"
:desc "Sweet meringue-based rhythms with smooth and sweet injections of soul"
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/baher-khairy-97645.jpg"
:price 1000
:sold-out false}
:gig-02 {:id :gig-02
:title "Stairs"
:artist "Brentr De Ranter"
:desc "Stairs to the highets peaks of music."
:img "https://res.cloudinary.com/schae/image/upload/f_auto,q_auto/v1519552695/giggin/brent-de-ranter-426248.jpg"
:price 2000
:sold-out false}})
I'd like to create a spec for it, but I'm not sure how to define the key e.g. ":gig-01" any pointers?
You could try:
(s/def ::gig-id
(s/and keyword?
(fn [x] (->> x name (re-matches #"gig-\d+")))))
This question already has answers here:
Custom equality in Clojure distinct
(3 answers)
Closed 5 years ago.
For the sake of example, let's assume I have two sets:
(def set-a #{{:id 1 :name "ABC" :zip 78759} {:id 2 :name "DEF" :zip 78759}})
(def set-b #{{:id 1 :name "ABC" :zip 78753} {:id 3 :name "XYZ" :zip 78704}})
I would like to find an union between the sets, using only :id and :name fields. However, with out using a custom comparator I get four elements in the set, because :zip field is different.
(clojure.set/union set-a set-b)
#{{:id 3, :name "XYZ", :zip 78704} {:id 1, :name "ABC", :zip 78753}
{:id 1, :name "ABC", :zip 78759} {:id 2, :name "DEF", :zip 78759}}
What is the idomatic way of finding union between two sets using a custom comparator or compare?
You could use group-by to do this:
(map first (vals (group-by (juxt :id :name) (concat set-a set-b))))
Or threaded:
(->> (concat set-a set-b)
(group-by (juxt :id :name))
(vals)
(map first))
This is grouping your elements by a combination of their key/values i.e. (juxt :id :name). Then it grabs the values of the produced map, then maps first over that to get the first item in each grouping.
Or use some code specifically built for this like distinct-by.
Note these approaches apply to any collection, not just sets.
If you don't mind throwing :zip away entirely, consider using clojure.set/project.
(clojure.set/union
(clojure.set/project set-a [:id :name])
(clojure.set/project set-b [:id :name]))
#{{:id 3, :name "XYZ"} {:id 2, :name "DEF"} {:id 1, :name "ABC"}}
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)
Say I have a list of maps that looks like the following:
(def my-map '({:some-key {:another-key "val"}
:id "123"}
{:some-key {:another-key "val"}
:id "456"}
{:some-other-key {:a-different-key "val2"}
:id "789"})
In my attempt to filter this map by :another-key, I tried this:
(filter #(= "val" ((% :some-key) :another-key)) my-map)))
However, this will throw a NullPointerException on the map entry that doesn't contain the key I'm filtering on. What would be the optimal way to filter this map, excluding entries that don't match the filtered schema entirely?
Your first lookup of the key :some-key will return nil if the map key is not in the map. Calling nil will result in the NPE you see.
The solution is easy, just make the keyword lookup itself in the map which work even if given a nil:
(def my-map '({:some-key {:another-key "val"}
:id "123"}
{:some-key {:another-key "val"}
:id "456"}
{:some-other-key {:a-different-key "val2"}
:id "789"}))
(filter #(= "val" (:another-key (% :some-key))) my-map)
You can also use get-in:
(filter #(= "val" (get-in % [:some-key :another-key])) my-map)
And if your list could potentially have nil items:
(filter #(= "val" (:another-key (:some-key %))) my-map)
Explanation:
(:k nil);; => nil
(nil :k);; => NPE
({:k 4} :k);; => 4
(:k {:k 4});; => 4
;; BTW, you can also specify the "not found" case:
(:k nil :not-there);; => :not-there
See also the clojure style guide.
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.