change struct-object state - clojure

I would like to change the state of the objects andre and blastoise, adding a new property(attribute/state) to the object... the name of this new property I want to add is "tax". I tryed the code below but didnt work out... help me plz:
(def andre {:owner "Andre" :type "car" :cur-speed 101 :license-plate "ABC"})
(def blastoise {:owner "Blastoise" :type "truck" :cur-speed 120 :license-plate "XYZ"})
(def car-tax[andre blastoise])
(defn calculate-car-tax [v]
(for [element v]
(if (> (element :cur-speed) 100) (dosync (alter element tax :20)))
)
)
(calculate-car-tax car-tax)

try
(assoc andre :tax 20)
From the docs:
user=> (doc assoc)
-------------------------
clojure.core/assoc
([map key val] [map key val & kvs])
assoc[iate]. When applied to a map, returns a new map of the
same (hashed/sorted) type, that contains the mapping of key(s) to
val(s). When applied to a vector, returns a new vector that
contains val at index. Note - index must be <= (count vector).
nil
user=>
Edit to include function:
(defn calculate-car-tax [v]
(for [element v]
(if (> (element :cur-speed) 100)
(dosync (assoc element :tax 20)))))

Related

How do I conditionally update keys and values in a Clojure map based on the key name?

I have a map and the keys are strings. If the key contains the word "kg" I want to multiply the value by 2.2 and then replace "kg" with "lb" in the key. I can't figure out how to iterate over the map in a way that I can conditionally update it.
Example map:
{"id" ("7215" "74777" "7219"),
"weight-kg" ("150" "220" "530"),
"time-seconds" ("1900" "2" "770")}
Desired output
{"id" ("7215" "74777" "7219"),
"weight-lb" ("330" "485" "1168"),
"time-seconds" ("1900" "2" "770")}
I've tried update, for map and reduce-kv. Project requirement is to not use the string library, which is why there is re-find. These are only attempts at changing the values. Since I can't change the values, I haven't attempted changing the keys.
(defn kg->lb [m k]
(if (re-find #"kg" k)
(map #(update m % * 2.2))))
(defn kg2->lb2 [m]
(reduce-kv #(if (re-find #"kg" %)
(update % * 2.2)) {} m)
(map #(if (re-find #"kg" %)
(update % * 2.2)) m)
(for [k (keys m)]
(if (re-find #"kg" k)
(update m k #(* % 2.2))))
Data:
(def data {"id" ["7215" "74777" "7219"],
"weight-kg" ["150" "220" "530"],
"time-seconds" ["1900" "2" "770"]})
Helper function to convert a string (kg amount) to string (lb amount):
(defn kg->lb [kg-string]
(-> kg-string
parse-long
(* 2.2)
int
str))
The most important function is reduce-kv.
If you find "kg" in key, you will replace that with "lb" and map helper function over all values.
If you don't find "kg" in key, you will just assoc that entry without change.
(reduce-kv (fn [m k v]
(if (re-find #"kg" k)
(assoc m (str/replace k #"kg" "lb")
(map kg->lb v))
(assoc m k v)))
{}
data)
I think I passed Project requirement is to not use the string library, except for (str/replace k #"kg" "lb"), which you can replace with String/replace interop: (.replace k "kg" "lb").
EDIT: Solution with map and into:
(defn update-entry [[k v]]
(if (re-find #"kg" k)
[(.replace k "kg" "lb") (map kg->lb v)]
[k v]))
(->> data
(map update-entry)
(into {}))
Transducer version:
(into {} (map update-entry) data)
I would do it a bit differently. First, I'd convert the strings to integers using the Java function Long/parseLong:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[tupelo.core :as t]
))
(defn parse-vec-longs
[v]
(mapv #(Long/parseLong %) v))
(verify
(is= (parse-vec-longs ["150" "220" "530"])
[150 220 530]))
Then I would write the code to convert the kg vals to lb vals. At the end, just use dissoc to get rid of the kg data, then assoc to add in the new lb data:
(defn convert-kg-lb
[data]
(let-spy-pretty [kg-vals (get data "weight-kg")
lb-vals (mapv #(Math/round (* 2.2 %)) kg-vals)
result (t/it-> data
(dissoc it "weight-kg")
(assoc it "weight-lb" lb-vals))]
result))
(verify
(let [data-str {"id" ["7215" "74777" "7219"]
"weight-kg" ["150" "220" "530"]
"time-seconds" ["1900" "2" "770"]}
data-parsed (t/map-vals data-str #(parse-vec-longs %))
expected-parsed {"id" [7215 74777 7219]
"weight-kg" [150 220 530]
"time-seconds" [1900 2 770]}
expected-out {"id" [7215 74777 7219]
"weight-lb" [330 484 1166]
"time-seconds" [1900 2 770]}
result (convert-kg-lb data-parsed)]
(is= data-parsed expected-parsed)
(is= (spyx-pretty result) (spyx-pretty expected-out))))
Normally, you'd also replace all the string keys with keywords as well, so "lb" => :lb.

Get key by first element in value list in Clojure

This is similar to Clojure get map key by value
However, there is one difference. How would you do the same thing if hm is like
{1 ["bar" "choco"]}
The idea being to get 1 (the key) where the first element if the value list is "bar"? Please feel free to close/merge this question if some other question answers it.
I tried something like this, but it doesn't work.
(def hm {:foo ["bar", "choco"]})
(keep #(when (= ((nth val 0) %) "bar")
(key %))
hm)
You can filter the map and return the first element of the first item in the resulting sequence:
(ffirst (filter (fn [[k [v & _]]] (= "bar" v)) hm))
you can destructure the vector value to access the second and/or third elements e.g.
(ffirst (filter (fn [[k [f s t & _]]] (= "choco" s))
{:foo ["bar", "choco"]}))
past the first few elements you will probably find nth more readable.
Another way to do it using some:
(some (fn [[k [v & _]]] (when (= "bar" v) k)) hm)
Your example was pretty close to working, with some minor changes:
(keep #(when (= (nth (val %) 0) "bar")
(key %))
hm)
keep and some are similar, but some only returns one result.
in addition to all the above (correct) answers, you could also want to reindex your map to desired form, especially if the search operation is called quite frequently and the the initial map is rather big, this would allow you to decrease the search complexity from linear to constant:
(defn map-invert+ [kfn vfn data]
(reduce (fn [acc entry] (assoc acc (kfn entry) (vfn entry)))
{} data))
user> (def data
{1 ["bar" "choco"]
2 ["some" "thing"]})
#'user/data
user> (def inverted (map-invert+ (comp first val) key data))
#'user/inverted
user> inverted
;;=> {"bar" 1, "some" 2}
user> (inverted "bar")
;;=> 1

Simple "R-like" melt : better way to do?

Today I tried to implement a "R-like" melt function. I use it for Big Data coming from Big Query.
I do not have big constraints about time to compute and this function takes less than 5-10 seconds to work on millions of rows.
I start with this kind of data :
(def sample
'({:list "123,250" :group "a"} {:list "234,260" :group "b"}))
Then I defined a function to put the list into a vector :
(defn split-data-rank [datatab value]
(let [splitted (map (fn[x] (assoc x value (str/split (x value) #","))) datatab)]
(map (fn[y] (let [index (map inc (range (count (y value))))]
(assoc y value (zipmap index (y value)))))
splitted)))
Launch :
(split-data-rank sample :list)
As you can see, it returns the same sequence but it replaces :list by a map giving the position in the list of each item in quoted list.
Then, I want to melt the "dataframe" by creating for each item in a group its own row with its rank in the group.
So that I created this function :
(defn split-melt [datatab value]
(let [splitted (split-data-rank datatab value)]
(map (fn [y] (dissoc y value))
(apply concat
(map
(fn[x]
(map
(fn[[k v]]
(assoc x :item v :Rank k))
(x value)))
splitted)))))
Launch :
(split-melt sample :list)
The problem is that it is heavily indented and use a lot of map. I apply dissoc to drop :list (which is useless now) and I have also to use concat because without that I have a sequence of sequences.
Do you think there is a more efficient/shorter way to design this function ?
I am heavily confused with reduce, does not know whether it can be applied here since there are two arguments in a way.
Thanks a lot !
If you don't need the split-data-rank function, I will go for:
(defn melt [datatab value]
(mapcat (fn [x]
(let [items (str/split (get x value) #",")]
(map-indexed (fn [idx item]
(-> x
(assoc :Rank (inc idx) :item item)
(dissoc value)))
items)))
datatab))

Clojure get map key by value

I'm a new clojure programmer.
Given...
{:foo "bar"}
Is there a way to retrieve the name of the key with a value "bar"?
I've looked through the map docs and can see a way to retrieve key and value or just value but not just the key. Help appreciated!
There can be multiple key/value pairs with value "bar". The values are not hashed for lookup, contrarily to their keys. Depending on what you want to achieve, you can look up the key with a linear algorithm like:
(def hm {:foo "bar"})
(keep #(when (= (val %) "bar")
(key %)) hm)
Or
(filter (comp #{"bar"} hm) (keys hm))
Or
(reduce-kv (fn [acc k v]
(if (= v "bar")
(conj acc k)
acc))
#{} hm)
which will return a seq of keys. If you know that your vals are distinct from each other, you can also create a reverse-lookup hash-map with
(clojure.set/map-invert hm)
user> (->> {:a "bar" :b "foo" :c "bar" :d "baz"} ; initial map
(group-by val) ; sorted into a new map based on value of each key
(#(get % "bar")) ; extract the entries that had value "bar"
(map key)) ; get the keys that had value bar
(:a :c)
As in many other cases you can use for:
(def hm {:foo "bar"})
(for [[k v] hm :when (= v "bar")] k)
And with "some" you can return the first matching item instead of a list (as probably the original question implied):
(some (fn [[k v]] (if (= v "bar") k)) hm)

Traversing a list of maps

I am a beginner in Clojure, and I have a simple question
Lets say i have a List, composed of Maps.
Each Map has a :name and :age
My code is:
(def Person {:nom rob :age 31 } )
(def Persontwo {:nom sam :age 80 } )
(def Persontthree {:nom jim :age 21 } )
(def mylist (list Person Persontwo Personthree))
Now how do i traverse the list. Let's say for example, that i have a given :name. How do i traverse the list to see if any of the Maps :name matches my :name. And then if there is a map that matches, how do i get the index position of that map?
-Thank you
(defn find-person-by-name [name people]
(let
[person (first (filter (fn [person] (= (get person :nom) name)) people))]
(print (get person :nom))
(print (get person :age))))
EDIT: the above was the answer to the question as it was before question was edited; here's the updated one - filter and map were starting to get messy, so I rewrote it from scratch using loop:
; returns 0-based index of item with matching name, or nil if no such item found
(defn person-index-by-name [name people]
(loop [i 0 [p & rest] people]
(cond
(nil? p)
nil
(= (get p :nom) name)
i
:else
(recur (inc i) rest))))
This can be done with doseq:
(defn print-person [name people]
(doseq [person people]
(when (= (:nom person) name)
(println name (:age person)))))
I would suggest looking at the filter function. This will return a sequence of items that match some predicate. As long as you don't have name duplication (and your algorithm would seem to dictate this), it would work.
Since you changed your question I give you a new answer. (I don't want to edit my old answer since that would make the comments very confusing).
There might be a better way to do this...
(defn first-index-of [key val xs]
(loop [index 0
xs xs]
(when (seq xs)
(if (= (key (first xs)) val)
index
(recur (+ index 1)
(next xs))))))
This function is used like this:
> (first-index-of :nom 'sam mylist)
1
> (first-index-of :age 12 mylist)
nil
> (first-index-of :age 21 mylist)
2
How about using positions from clojure.contrib.seq (Clojure 1.2)?
(use '[clojure.contrib.seq :only (positions)])
(positions #(= 'jim (:nom %)) mylist)
It returns a sequence of the matched indices (you can use first or take if you want to shorten the list).
(defn index-of-name [name people]
(first (keep-indexed (fn [i p]
(when (= (:name p) name)
i))
people)))
(index-of-name "mark" [{:name "rob"} {:name "mark"} {:name "ted"}])
1