I have a list of maps:
(def mylist
[{:id 1 :sub [{:subid 1} {:subid 2}]}
{:id 2 :sub [{:subid 3}]}])
I want to add a new key/value pair to each map element of the list that contains the count of items in :sub:
[{:id 1 :sub [{:subid 1} {:subid 2}] :subcount 2}
{:id 2 :sub [{:subid 3}] :subcount 1}]
How can I do this?
In clojure, "adding to a map" is done with assoc, which returns a new map with the specified value(s) added, and usually if you want to do the same operation on a collection of things, you use the map function.
(defn subcount
"return the number of items in the :sub of m"
[m]
(count (:sub m)))
(defn add-count
"add subcount to the given map"
[m]
(assoc m :subcount (subcount m)))
(defn add-counts
"add subcount to all the objects"
[objects]
(map add-count objects))
(def mylist
[{:id 1 :sub [{:subid 1} {:subid 2}]}
{:id 2 :sub [{:subid 3}]}])
(add-counts mylist)
=> ({:sub [{:subid 1} {:subid 2}], :subcount 2, :id 1} {:sub [{:subid 3}], :subcount 1, :id 2})
Related
I have a list filled with many maps (all of them have the same key), like this:
({:a 1} {:a 1} {:a 2} {:a 2} {:a 3} {:a 2})
I would like to convert it to a map that stores the occurrence of the value of each map. For exemple, the list above should return the following map:
{:1 2, :2 3, :3 1}
Any ideas on how can i do that?
(def m '({:a 1} {:a 1} {:a 2} {:a 2} {:a 3} {:a 2}))
(frequencies (map :a m)) ;; => {1 2, 2 3, 3 1}
Note the keys of the result are not keywords, as that would be an odd thing to do.
I would solve it like this:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(defn maps->freqs
[maps]
(frequencies
(for [m maps]
(second (first m)))))
(dotest
(let [data (quote
({:a 1} {:a 1} {:a 2} {:a 2} {:a 3} {:a 2}))]
(is= (maps->freqs data)
{1 2, 2 3, 3 1})))
The above uses my favorite template project. The best technique is to build it up slowely:
(defn maps->freqs
[maps]
(for [m maps]
(first m)))
then (spyx-pretty (maps->freqs data)) produces
(maps->freqs data) =>
[[:a 1] [:a 1] [:a 2] [:a 2] [:a 3] [:a 2]]
modify it:
(defn maps->freqs
[maps]
(for [m maps]
(second (first m))))
with result
(maps->freqs data) =>
[1 1 2 2 3 2]
Then use frequencies to get the final result.
Please be sure to read the list of documentation, especially the Clojure CheatSheet!
I want to know how to convert
vector [1 2 3 :a :b :c :A :B :C]
to
[ {:index 1 :lower :a :upper :A} {:index 2 :lower :b :upper :B} {:index 3 :lower :c :upper :C} ] ?
the vector may be [1 2 3 4 :a :b :c :d :A :B :C :D]
or if there is not an easy way,is there a way to convert
[{:index 1} {:index 2} {:index 3}] [{:lower :a} {:lower :b} {:lower :c}] [{:upper :A} {:upper :B} {:upper :C}]
to
[{:index 1 :lower :a :upper :A} {:index 2 :lower :b :upper :B} {:index 3 :lower :c :upper :C}]
Thanks!
So in general, when faced with a problem like this, I would go upstream and fix the input format. A vector that is a concatenation of arbitrary parts doesn't make any sense. For the sake of an answer, let us assume this isn't possible.
First we define a helper function to create the result maps:
(defn make-result [i l u]
{:index i :lower l :upper u})
Then we just need to map this function over the three subsequences:
(defn input->output [i]
(apply map make-result (partition (/ (count i) 3) i)))
We need to use apply as we generate a sequence of subsequences that we want to use as the parameters for map (recall that the function arity should match the number of sequences you pass to map - which conveniently our helper does).
This will work for both of the vectors given above.
(input->output [1 2 3 :a :b :c :A :B :C])
({:index 1, :lower :a, :upper :A} {:index 2, :lower :b, :upper :B} {:index 3, :lower :c, :upper :C})
(input->output [1 2 3 4 :a :b :c :d :A :B :C :D])
({:index 1, :lower :a, :upper :A} {:index 2, :lower :b, :upper :B} {:index 3, :lower :c, :upper :C} {:index 4, :lower :d, :upper :D})
Behaviour if the vector is in a different format may surprise or disappoint - perhaps some input validation is in order.
(let [ks [:index :lower :upper]
xs [1 2 3 :a :b :c :A :B :C]]
(->> xs
(partition (/ (count xs) (count ks)))
(apply map vector)
(mapv zipmap (repeat ks))))
How it works:
We first partition the vector by count:
(partition (/ (count xs) (count ks)) xs)=> ((1 2 3) (:a :b :c) (:A :B :C))
Then transpose the matrix:
(apply map vector *1)=> ([1 :a :A] [2 :b :B] [3 :c :C])
Finally zipmap with the provided keys for each row:
(mapv zipmap (repeat ks) *1)=> [{:index 1, :lower :a, :upper :A} {:index 2, :lower :b, :upper :B} {:index 3, :lower :c, :upper :C}]
If you can provide a list of the key-values, like the following (formatted to improve readability):
(def items [[{:index 1} {:index 2} {:index 3}]
[{:lower :a} {:lower :b} {:lower :c}]
[{:upper :A} {:upper :B} {:upper :C}]])
then you can use the following:
(apply map merge items)
;; returns ({:index 1, :lower :a, :upper :A} {:index 2, :lower :b, :upper :B} {:index 3, :lower :c, :upper :C})
This works by using the map function to merge the individual hash-maps in the 3 collections. First, the first elements of each collection are merged together, resulting in the element {:index 1, :lower :a, :upper :A}, then the second elements of each collection are merged, and so on.
Since the arguments to map merge are a collection, you need to use apply to provide the arguments to map.
I am not a clojure expert but maybe this helps:
; your data
(def x [1 2 3 :a :b :c :A :B :C])
; resolve symbols and numbers to strings
(def xa (map (fn [e] (if (keyword? e) (name e) (str e))) x))
; split into three sequences and zip this lists together
(let [xan (filter (fn [e] (not (empty? (re-matches #"[0-9]" e)))) xa)
xaa (filter (fn [e] (not (empty? (re-matches #"[a-z]" e)))) xa)
xaA (filter (fn [e] (not (empty? (re-matches #"[A-Z]" e)))) xa)]
(map-indexed (fn [i e] {:index e :lower (nth xaa i) :upper (nth xaA i)}) xan ))
You just build three sequences and iterate over any of them and use the index to access the corresponding elements from the other.
I would like to update values in hashes, but I'm not sure how this can be done efficiently
I tried using a loop approach, but keeping the previous record's value also in account seems like a big challenge.
This is what I am trying to do,
Considering the records are sorted based on created_at in descending order, For example,
[{:id 1, :created_at "2016-08-30 11:07:00"}{:id 2, :created_at "2016-08-30 11:05:00"}...]
]
; Basically in humanised form.
Could anyone share some ideas to achieve this? Thanks in advance.
Simplified example:
(def data [{:value 10} {:value 8} {:value 3}])
(conj
(mapv
(fn [[m1 m2]] (assoc m1 :difference (- (:value m1) (:value m2))))
(partition 2 1 data))
(last data))
;;=> [{:value 10, :difference 2} {:value 8, :difference 5} {:value 3}]
what you need, is to iterate over all the pairs of consecutive records, keeping the first of them, adding the difference to it.
first some utility functions for dates handling:
(defn parse-date [date-str]
(.parse (java.text.SimpleDateFormat. "yyyy-MM-dd HH:mm:ss") date-str))
(defn dates-diff [date-str1 date-str2]
(- (.getTime (parse-date date-str1))
(.getTime (parse-date date-str2))))
then the mapping part:
user> (def data [{:id 1, :created_at "2016-08-30 11:07:00"}
{:id 2, :created_at "2016-08-30 11:05:00"}
{:id 3, :created_at "2016-08-30 10:25:00"}])
user> (map (fn [[rec1 rec2]]
(assoc rec1 :difference
(dates-diff (:created_at rec1)
(:created_at rec2))))
(partition 2 1 data))
({:id 1, :created_at "2016-08-30 11:07:00", :difference 120000}
{:id2, :created_at "2016-08-30 11:05:00", :difference 2400000})
notice that it doesn't contain the last item, since it was never the first item of a pair. So you would have to add it manually:
user> (conj (mapv (fn [[rec1 rec2]]
(assoc rec1 :difference
(dates-diff (:created_at rec1)
(:created_at rec2))))
(partition 2 1 data))
(assoc (last data) :difference ""))
[{:id 1, :created_at "2016-08-30 11:07:00", :difference 120000}
{:id 2, :created_at "2016-08-30 11:05:00", :difference 2400000}
{:id 3, :created_at "2016-08-30 10:25:00", :difference ""}]
now it's ok. The only difference with your desired variant, is that the diff is in millis, rather than formatted string. To do that you can add the formatting to the dates-diff function.
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
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?