Create new map or update existing one - clojure

I have the following data:
({:seriesId "series 0", :episodeId "0"}
{:seriesId "series 1", :episodeId "1"}
{:seriesId "series 1", :episodeId "2"}
{:seriesId "series 2", :episodeId "3"}
{:seriesId "series 2", :episodeId "4"}
{:seriesId "series 2", :episodeId "5"})
And would like to associate each episode to its series, like this:
[{:put-request
{:item {:seriesId "series 0", :episodeCount 1, :episodeIds #{"0"}}}}
{:put-request
{:item {:seriesId "series 1", :episodeCount 2, :episodeIds #{"1" "2"}}}}
{:put-request
{:item {:seriesId "series 2", :episodeCount 3, :episodeIds #{"3" "4" "5"}}}}]
Currently I am stuck with the following:
[{:put-request
{:item {:seriesId "series 0", :episodeCount 1, :episodeIds #{"0"}}}}
{:put-request
{:item {:seriesId "series 1", :episodeCount 1, :episodeIds #{"1"}}}}
{:put-request
{:item {:seriesId "series 1", :episodeCount 1, :episodeIds #{"2"}}}}
{:put-request
{:item {:seriesId "series 2", :episodeCount 1, :episodeIds #{"3"}}}}
{:put-request
{:item {:seriesId "series 2", :episodeCount 1, :episodeIds #{"4"}}}}
{:put-request
{:item {:seriesId "series 2", :episodeCount 1, :episodeIds #{"5"}}}}]
I am using the create-or-update-series function. I don't know how to find/get a previously added series (if added!) using the seriesId. I tried many things but these were dead-end tracks.
(ns clojure-sscce.core
(:gen-class)
(:require clojure.pprint))
(defn create-or-update-series
([episodes]
(create-or-update-series episodes []))
([episodes result]
(if (zero? (count episodes))
result
(create-or-update-series (rest episodes)
(conj result {
:put-request {
:item {
:seriesId (:seriesId (first episodes))
:episodeCount 1
:episodeIds #{(:episodeId (first episodes))}}}})))))
;; Tests
(defn -main [& args]
(let
[series0 (mapv (fn [episode-id] {
:seriesId "series 0"
:episodeId (str episode-id)}) (range 0 1))
series1 (mapv (fn [episode-id] {
:seriesId "series 1"
:episodeId (str episode-id)}) (range 1 3))
series2 (mapv (fn [episode-id] {
:seriesId "series 2"
:episodeId (str episode-id)}) (range 3 6))]
(clojure.pprint/pprint
(concat series0 series1 series2))
(clojure.pprint/pprint
(create-or-update-series (concat series0 series1 series2)))))
Note that {:put-request {:item { ... is needed because the new maps are expected to be PUT to DynamoDB.
Would love your help!

group-by is pretty good for things like this. Here's one try in combination with a for comprehension:
(defn group-by-series [episodes]
(let [grouped (group-by :seriesId episodes)]
(for [[series eps-in-series] grouped]
{:seriesId series
:episodeCount (count eps-in-series)
:episodeIds (into #{} (map :episodeId eps-in-series))})))
(group-by-series example-data)
;=> ({:seriesId "series 0", :episodeCount 1, :episodeIds #{"0"}}
; {:seriesId "series 1", :episodeCount 2, :episodeIds #{"1" "2"}}
; {:seriesId "series 2", :episodeCount 3, :episodeIds #{"3" "4" "5"}})
You can add the DynamoDB stuff right in the for comprehension if you want, or make a wrapping function and map it across them.

So if we want to look at the "create-or-update" problem as such, there's a couple ways we can go about implementing that. Like your attempt we're going to need to recursively make a collection of series, but like group-by it's probably better to make it a map, keyed on the series ID. This way when we find a new episode in the input we can easily and efficiently find the series it belongs to in the collection.
First, let's make a little convenience function to update such a map for just one episode. It should:
Take a series map and an episode.
Look up the right series, if it's there, or else create one.
Add the episode to the series and the series to the series map.
Here's my approach:
(defn- update-series-map [series-map {:keys [seriesId episodeId] :as episode}]
(let[current-series (get series-map seriesId
{:seriesId seriesId :episodeIds #{} :episodeCount 0})
updated-series (-> current-series
(update-in [:episodeCount] inc)
(update-in [:episodeIds] conj episodeId))]
(assoc series-map seriesId updated-series)))
Here we can use the if-not-found parameter of get to create an appropriate empty series if the series doesn't have an entry yet, otherwise we get the entry that's there. In either case we then have to update the entry to add the episode - we have to conj the episode ID into the episode set and inc the episode count. I used update-in to do both of these, but if you're on Clojure 1.7+ update is better for cases like this where we don't go down a deeper key sequence than 1 key.
With that building block we can make something to loop through several episodes. We can do it with a multi-arity recursive approach like in create-or-update-series:
(defn group-by-series-multiarity
([episodes]
(group-by-series-multiarity {} episodes))
([series-map
[ep & more]]
(if (seq more)
(recur (update-series-map series-map ep) more)
(vals (update-series-map series-map ep)))))
In structure this is basically the same. I use recur rather than recurring by name mainly as an optimization. Explicit calls use up call stack space, while recur can avoid that. Checking for emptiness with seq is another small optimization, since we don't have to loop through the remaining episodes in order to count them.
At the end it needs a little cleanup, because we don't want the whole map we've created, only the values. That's why I do vals at the end.
Alternatively we could use loop as the target for our recur. This can be nice if our "public API" doesn't fit with the way we do our recursion:
(defn group-by-series-looping[episodes]
(loop[series-map {}
[ep & more] episodes]
(if (seq more)
(recur (update-series-map series-map ep) more)
(vals (update-series-map series-map ep)))))
loop basically works like creating a local helper function (in this case with arity 2) and using recur in that.
We could also notice that these recursive functions follow a well-known pattern called 'left fold' or 'reduction' and abstract that pattern using higher-order functions:
(defn group-by-series-reducing [episodes]
(vals (reduce update-series-map {} episodes)))
Note how reduce basically takes care of the whole loop from group-by-series-looping if we just give it the reducing function it should use (update-series-map) and the initial value {}.

Related

Clojure convert vector of maps into map of vector values

There are a few SO posts related to this topic, however I could not find anything that works for what I am looking to accomplish.
I have a vector of maps. I am going to use the example from another related SO post:
(def data
[{:id 1 :first-name "John1" :last-name "Dow1" :age "14"}
{:id 2 :first-name "John2" :last-name "Dow2" :age "54"}
{:id 3 :first-name "John3" :last-name "Dow3" :age "34"}
{:id 4 :first-name "John4" :last-name "Dow4" :age "12"}
{:id 5 :first-name "John5" :last-name "Dow5" :age "24"}]))
I would like to convert this into a map with the values of each entry be a vector of the associated values (maintaining the order of data).
Here is what I would like to have as the output:
{:id [1 2 3 4 5]
:first-name ["John1" "John2" "John3" "John4" "John5"]
:last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"]
:age ["14" "54" "34" "12" "24"]}
Is there an elegant and efficient way to do this in Clojure?
Can be made more efficient, but this is a nice start:
(def ks (keys (first data)))
(zipmap ks (apply map vector (map (apply juxt ks) data))) ;;=>
{:id [1 2 3 4 5]
:first-name ["John1" "John2" "John3" "John4" "John5"]
:last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"]
:age ["14" "54" "34" "12" "24"]}
Another one that comes close:
(group-by key (into [] cat data))
;;=>
{:id [[:id 1] [:id 2] [:id 3] [:id 4] [:id 5]],
:first-name [[:first-name "John1"] [:first-name "John2"] [:first-name "John3"] [:first-name "John4"] [:first-name "John5"]],
:last-name [[:last-name "Dow1"] [:last-name "Dow2"] [:last-name "Dow3"] [:last-name "Dow4"] [:last-name "Dow5"]],
:age [[:age "14"] [:age "54"] [:age "34"] [:age "12"] [:age "24"]]}
Well, I worked out a solution and then before I could post, Michiel posted a more concise solution, but I'll go ahead and post it anyway =).
(defn map-coll->key-vector-map
[coll]
(reduce (fn [new-map key]
(assoc new-map key (vec (map key coll))))
{}
(keys (first coll))))
For me, the clearest approach here is the following:
(defn ->coll [x]
(cond-> x (not (coll? x)) vector))
(apply merge-with #(conj (->coll %1) %2) data)
Basically, the task here is to merge all maps (merge-with), and gather up all the values at the same key by conjoining (conj) onto the vector at key – ensuring that values are actually conjoined onto a vector (->coll).
If we concatenate the maps into a single sequence of pairs, we have an edge-list representation of a graph. What we have to do is convert it into an adjacency-list (here a vector, not a list) representation.
(defn collect [maps]
(reduce
(fn [acc [k v]] (assoc acc k (conj (get acc k []) v)))
{}
(apply concat maps)))
For example,
=> (collect data)
{:id [1 2 3 4 5]
:first-name ["John1" "John2" "John3" "John4" "John5"]
:last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"]
:age ["14" "54" "34" "12" "24"]}
The advantage of this method over some of the others is that the maps in the given sequence can be different shapes.
Please consider the reader when writing code! There are no prizes for playing "code golf". There are, however, considerable costs to others when you force them to decipher code that is overly condensed.
I always try to be explicit about what code is doing. This is easiest if you break down a problem into simple steps and use good names. In particular, it is almost impossible to accomplish this using juxt or any other cryptic function.
Here is how I would implement the solution:
(def data
[{:id 1 :first-name "John1" :last-name "Dow1" :age "14"}
{:id 2 :first-name "John2" :last-name "Dow2" :age "54"}
{:id 3 :first-name "John3" :last-name "Dow3" :age "34"}
{:id 4 :first-name "John4" :last-name "Dow4" :age "12"}
{:id 5 :first-name "John5" :last-name "Dow5" :age "24"}])
(def data-keys (keys (first data)))
(defn create-empty-result
"init result map with an empty vec for each key"
[data]
(zipmap data-keys (repeat [])))
(defn append-map-to-result
[cum-map item-map]
(reduce (fn [result map-entry]
(let [[curr-key curr-val] map-entry]
(update-in result [curr-key] conj curr-val)))
cum-map
item-map))
(defn transform-data
[data]
(reduce
(fn [cum-result curr-map]
(append-map-to-result cum-result curr-map))
(create-empty-result data)
data))
with results:
(dotest
(is= (create-empty-result data)
{:id [], :first-name [], :last-name [], :age []})
(is= (append-map-to-result (create-empty-result data)
{:id 1 :first-name "John1" :last-name "Dow1" :age "14"})
{:id [1], :first-name ["John1"], :last-name ["Dow1"], :age ["14"]})
(is= (transform-data data)
{:id [1 2 3 4 5],
:first-name ["John1" "John2" "John3" "John4" "John5"],
:last-name ["Dow1" "Dow2" "Dow3" "Dow4" "Dow5"],
:age ["14" "54" "34" "12" "24"]}))
Note that I included unit tests for the helper functions as a way of both documenting what they are intended to do as well as demonstrating to the reader that they actually work as advertised.
This template project can be used to run the above code.

Clojure - update data values in array of hashes

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.

Clojure - Recursively Semi-Flatten Nested Map

In clojure, how can I turn a nested map like this:
(def parent {:id "parent-1"
:value "Hi dude!"
:children [{:id "child-11"
:value "How is life?"
:children [{:id "child-111"
:value "Some value"
:children []}]}
{:id "child-12"
:value "Does it work?"
:children []}]})
Into this:
[
[{:id "parent-1", :value "Hi dude!"}]
[{:id "parent-1", :value "Hi dude!"} {:id "child-11", :value "How is life?"}]
[{:id "parent-1", :value "Hi dude!"} {:id "child-11", :value "How is life?"} {:id "child-111", :value "Some value"}]
[{:id "parent-1", :value "Hi dude!"} {:id "child-12", :value "Does it work?"}]
]
I'm stumbling through very hacky recursive attempts and now my brain is burnt out.
What I've got so far is below. It does get the data right, however it puts the data in some extra undesired nested vectors.
How can this be fixed?
Is there a nice idiomatic way to do this in Clojure?
Thanks.
(defn do-flatten [node parent-tree]
(let [node-res (conj parent-tree (dissoc node :children))
child-res (mapv #(do-flatten % node-res) (:children node))
end-res (if (empty? child-res) [node-res] [node-res child-res])]
end-res))
(do-flatten parent [])
Which produces:
[
[{:id "parent-1", :value "Hi dude!"}]
[[
[{:id "parent-1", :value "Hi dude!"} {:id "child-11", :value "How is life?"}]
[[
[{:id "parent-1", :value "Hi dude!"} {:id "child-11", :value "How is life?"} {:id "child-111", :value "Some value"}]
]]]
[
[{:id "parent-1", :value "Hi dude!"} {:id "child-12", :value "Does it work?"}]
]]
]
I don't know if this is idiomatic, but it seems to work.
(defn do-flatten
([node]
(do-flatten node []))
([node parents]
(let [path (conj parents (dissoc node :children))]
(vec (concat [path] (mapcat #(do-flatten % path)
(:children node)))))))
You can leave off the [] when you call it.
another option is to use zippers:
(require '[clojure.zip :as z])
(defn paths [p]
(loop [curr (z/zipper map? :children nil p)
res []]
(cond (z/end? curr) res
(z/branch? curr) (recur (z/next curr)
(conj res
(mapv #(select-keys % [:id :value])
(conj (z/path curr) (z/node curr)))))
:else (recur (z/next curr) res))))
I'd be inclined to use a bit of local state to simplify the logic:
(defn do-flatten
([node]
(let [acc (atom [])]
(do-flatten node [] acc)
#acc))
([node base acc]
(let [new-base (into base (self node))]
(swap! acc conj new-base)
(doall
(map #(do-flatten % new-base acc) (:children node))))))
Maybe some functional purists would dislike it, and of course you can do the whole thing in a purely functional way. My feeling is that it's a temporary and entirely local piece of state (and hence isn't going to cause the kinds of problems that state is notorious for), so if it makes for greater readability (which I think it does), I'm happy to use it.

Extract value from list of hash-maps Clojure

I'm currently trying to get a value from a list of hash-maps.
(def cards
(hash-map
:card1 {:name "Wisp" :damage 1 :health 1 :cost 0}
:card2 {:name "Spider Tank" :damage 3 :health 4 :cost 3}
)
I have a hash-map with my "cards".
(def deck1
(list (get cards :card1) (get cards :card2) (get cards :card1))
And a deck which is a list of these cards. (I've shortened the two structures)
What I'm trying to do is search this deck structure for a card by passing it a card name. This card is kept as a var and passed elsewhere. I will then reconstruct the list without this card.
So, I'm trying to draw a specific card from anywhere in the deck.
Currently I'm just trying to get the card but I've hit a dead-end with the code.
(defn playCard [card]
(let [c first deck1 (get-in deck1 [card :name]))]
(println "LOOK AT ME" c)))
Any help would be appreciated.
Here is an example of how to retrieve your cards. I also refactored the declarations :
(def cards {:card1 {:health 1, :name "Wisp", :damage 1, :cost 0},
:card2 {:health 4, :name "Spider Tank", :damage 3, :cost 3}})
(def deck [(:card1 cards)
(:card2 cards)
(:card1 cards)])
(defn find-first
[f coll]
(first (filter f coll)))
(find-first #(= (:name %) "Wisp") deck) ;; => {:health 1, :name "Wisp", :damage 1, :cost 0}
This is assuming you want to find a card by name (it doesn't make sense to look for a card you have already).

What does this clojure expression do (taken from O'Reilly book)

I'm new in Clojure Programming. Have been playing with it for a week now. I read the book "Clojure Programming" and after reading again and again and try to dissect this function in REPL but don't know how exactly this function works:
(defn reduce-by [key-fn f init coll]
(reduce (fn [summaries x]
(let [k (key-fn x)]
(assoc summaries k (f (summaries k init) x))))
{} coll))
I am still cannot understand the assoc part:
(assoc summaries k (f (summaries k init) x))))
Especially in the (summaries k init). It doesn't look like a function because summaries is defined as a map.
The function is intended to be used as follows
(def orders
[{:product "Clock", :customer "Wile Coyote", :qty 6, :total 300}
{:product "Dynamite", :customer "Wile Coyote", :qty 20, :total 5000}
{:product "Shotgun", :customer "Elmer Fudd", :qty 2, :total 800}
{:product "Shells", :customer "Elmer Fudd", :qty 4, :total 100}
{:product "Hole", :customer "Wile Coyote", :qty 1, :total 1000}
{:product "Anvil", :customer "Elmer Fudd", :qty 2, :total 300}
{:product "Anvil", :customer "Wile Coyote", :qty 6, :total 900}])
(reduce-by :customer #(+ %1 (:total %2)) 0 orders)
and it will yield a seq like below
;= {"Elmer Fudd" 1200, "Wile Coyote" 7200}
I appreciate to anyone who can explain it to me.
Thanks
Okay, looks like I figured it out.
Because maps are also functions, (summaries k init) will return the value of init if the map summaries doesn't contain key k.
Silly me to skim and forget.