Get list of values from vector of hashmap using filter or for - clojure

I'm quite new to clojure and have been struggling to understand how things work exactly. I have a vector of hashmaps as such, titled authors:
------ Authors -----------
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}]
I want to write a function that takes the id, and returns a list of the corresponding author names. I have two options for doing so: using filter or using for loop.
When using filter, I have a predicate function already that returns true if the author has matching id:
(defn check-by-id [author id]
(if (= id (:id author)) true false))
But I'm not sure how to use this in order to get the list of author names when passing the id.

Three other ways via keep, for and reduce:
(keep (fn [{:keys [id name]}] (when (= id 100) name)) authors)
;; => ("Albert Einstein")
(for [{:keys [id name]} authors
:when (= id 100)]
name)
;; => ("Albert Einstein")
(reduce (fn [v {:keys [id name]}]
(if (= id 100) (conj v name) v))
[]
authors)
;; => ["Albert Einstein"]
I prefer for (with :when) since it's shortest and in my eyes most clear. reduce I find best when you want to build a specific type of collection, this case a vector.

Filter will filter the list of maps. But the result is still a sequence of maps. You can map or reduce the result to get the list of authors.
(def authors [{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 100, :name "Richard Fynmann", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(defn check-by-id [author id] (= id (:id author)))
(defn filter-ids [id col] (filter #(check-by-id % id) col))
(filter-ids 100 authors)
;; ↪ ({:id 100, :name "Albert Einstein", :interest "Physics"}
;; {:id 100, :name "Richard Fynmann", :interest "Physics"})
(map :name (filter-ids 100 authors))
;; ↪ ("Albert Einstein" "Richard Fynmann")

You can also use group-by for this task:
(def list-of-maps
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(map :name (get (group-by :id list-of-maps) 100))
;; => ("Albert Einstein")

Related

Clojure: Find missing records in a collection based on another collection

I have 2 vectors: employ and emp-income. I want to loop thru emp-income based on employ to find what all the missing records. In this case, it's missing id = 2. And i want to create the missing record in emp-income and set the income as the previous record's income value. What is the best way to do it in clojure?
(def employ
[{:id 1 :name "Aaron"}
{:id 2 :name "Ben"}
{:id 3 :name "Carry"}])
from:
(def emp-income
[{:emp-id 1 :income 1000}
{:emp-id 3 :income 2000}])
to:
(def emp-income
[{:emp-id 1 :income 1000}
{:emp-id 2 :income 1000}
{:emp-id 3 :income 2000}])
You could use:
(let [emp-id->income (into {} (map (fn [rec] [(:emp-id rec) rec]) emp-income))]
(reduce (fn [acc {:keys [id]}]
(let [{:keys [income]} (or (get emp-id->income id) (peek acc))]
(conj acc {:emp-id id :income income})))
[]
employ))
Note this will create a record of {:emp-id id :income nil} if the first record is not found in emp-income. It will also use the last :emp-id encountered if duplicate :emp-id values are found within emp-income.

Clojure Macro using filter returns an object reference. Do not know how to interpret this reference

I am defining this macro
seminar.core=> (defmacro select
#_=> [vara _ coll _ wherearg _ orderarg]
#_=> `(filter ~wherearg))
#'seminar.core/select
And then defining a table
(def persons '({:id 1 :name "olle"} {:id 2 :name "anna"} {:id 3 :name
"isak"} {:id 4 :name "beatrice"}))
When I try to run my macro, so that I get the columns from the table where the id is greater than 2 (i.e {:id 3 :name "isak"} {:id 4 :name "beatrice"})
seminar.core=> (select [:id :name] from persons where [> :id 2] orderby :name)
I receive the following message and do not know quite how to interpret it
#object[clojure.core$filter$fn__4808 0x18e53c53 "clojure.core$filter$fn__4808#18e53c53"]
Update
I added a second argument to filter
seminar.core=> (defmacro select
#_=> [vara _ coll _ wherearg _ orderarg]
#_=> `(filter ~wherearg ~coll))
and receive IllegalArgumentException Key must be integer clojure.lang.APersistentVector.invoke (APersistentVector.java:292) as my return value now. I do not know how to interpret this error
When you use macroexpand-1 function to see the expanded form of macro it may give you a clue:
(macroexpand-1 '(select [:id :name] from persons where (> :id 2) orderby :name))
;;=> (clojure.core/filter [> :id 2] persons)
The form [> :id 2] isn't a valid function definition in Clojure. You have to pass proper function to filter, e.g. using anonymous function:
(select [:id :name] from persons where #(> (:id %) 2) orderby :name)
;;=> ({:id 3, :name "isak"} {:id 4, :name "beatrice"})

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.

Add items from collection 1 to collection 2, if collection 2 doesn't contain item from collection 1

I've got two maps:
(def people {:1 "John" :2 "Paul" :3 "Ringo" :4 "George"})
(def band
{:data
{:members
{:1 {:id 1 :name "John"}
:2 {:id 2 :name "Paul"}}}})
I want to loop over people and add any members that don't exist in [:data :members] to band, resulting in:
(def band
{:data
{:members
{:1 {:id 1 :name "John"}
:2 {:id 2 :name "Paul"}
:3 {:id 3 :name "Ringo"}
:4 {:id 4 :name "George"}}}})
Here's what I've tried:
(for [[id name] people]
(when-not
(contains? (get-in band [:data :members]) id)
(assoc-in band [:data :members id] {:id id :name name})))
Which yields:
({:data
{:members
{:4 {:id :4, :name "George"},
:1 {:name "John", :id 1},
:2 {:name "Paul", :id 2}}}}
nil
nil
{:data
{:members
{:1 {:name "John", :id 1},
:2 {:name "Paul", :id 2},
:3 {:id :3, :name "Ringo"}}}})
I'm not sure why I'm getting back what looks to be a list of each mutation of band. What am I doing wrong here? How can I add the missing members of people to band [:data :members]?
To be pedantic you aren't getting back any mutation of band. In fact, one of the most important features of Clojure is that the standard types are immutible, and the primary collection operations return a modified copy without changing the original.
Also, for in Clojure is not a loop, it is a list comprehension. This is why it always returns a sequence of each step. So instead of altering an input one step at a time, you made a new variation on the input for each step, each derived from the immutable original.
The standard construct for making a series of updated copies of an input based on a sequence of values is reduce, which passes a new version of the accumulator and each element of the list to your function.
Finally, you are misunderstanding the role of :keyword syntax - prefixing an item with a : is not needed in order to construct map keys - just about any clojure value is a valid key for a map, and keywords are just a convenient idiom.
user=> (def band
{:data
{:members
{1 {:id 1 :name "John"}
2 {:id 2 :name "Paul"}}}})
#'user/band
user=> (def people {1 "John" 2 "Paul" 3 "Ringo" 4 "George"})
#'user/people
user=> (pprint
(reduce (fn [band [id name :as person]]
(if-not (contains? (get-in band [:data :members]) id)
(assoc-in band [:data :members id] {:id id :name name})
band))
band
people))
{:data
{:members
{3 {:id 3, :name "Ringo"},
4 {:id 4, :name "George"},
1 {:name "John", :id 1},
2 {:name "Paul", :id 2}}}}
nil
You may notice the body of the fn passed to reduce is essentially the same as the body of your for comprehension. The difference is that instead of when-not which returns nil on the alternate case, I use if-not, which allows us to propagate the accumulator (here called band, same as the input) regardless of whether any new version of it is made.