Clojure. Update double nested value - clojure

I'm kind of newbie with Clojure, all is new but pretty fun too. So I have this data:
{:test {:title "Some Title"}, :questions [
{:id 1, :full-question {:question "Foo question", :id 1, :answers [{:id 7, :question_id 1, :answer "Foobar answer"}, {:id 8, :question_id 1, :answer "Foobar answer two"}]}},
{:id 5, :full-question {:question "Foo question", :id 5, :answers [{:id 12, :question_id 5, :answer "Foobar answer"}]}},
{:id 9, :full-question {:question "Foo question", :id 9, :answers [{:id 14, :question_id 9, :answer "Foobar answer"}, {:id 20, :question_id 9, :answer "Foobar answer two"}]}}
]}
A "classic" Test->Question->Answer kind of data structure. And I have this new info:
(def new-answer {:id 33, :answer "Another foobar answer", :question-id 9 })
I need to update the first structure to add "new-answer" into the "answers" for the :id number 9 in the :questions vector.
I tried with the update-in function but I don't know what to tell the correspondent :id in the maps inside the two vectors. I mean, I don't know how to build the "path" where I want to make the change.

also, there is a nice library for that kind of structural editing, called specter
your case could be solved like this:
(require '[com.rpl.specter :refer [ALL AFTER-ELEM setval]])
(defn add-answer [data {question-id :question-id :as new-answer}]
(setval [:questions ALL #(== question-id (:id %)) :full-question :answers AFTER-ELEM]
new-answer data))
user> (add-answer data {:id 33, :answer "Another foobar answer", :question-id 9 })
;;=> {:test {:title "Some Title"},
;; :questions
;; [
;; ;; ... all other ids
;; {:id 9,
;; :full-question
;; {:question "Foo question",
;; :id 9,
;; :answers
;; [{:id 14, :question_id 9, :answer "Foobar answer"}
;; {:id 20, :question_id 9, :answer "Foobar answer two"}
;; {:id 33, :answer "Another foobar answer", :question-id 9}]}}]}

You have the right idea with update-in. You can first calculate the index in your questions vector, then create the path :questions, "question-index", :full-question, :answers. Then you may conj in your new answer:
(def data {...})
(defn index-by-id
[v id]
(first (filter #(= (:id (v %)) id) (range (count v)))))
(defn add-answer
[answer]
(let [q-index (index-by-id (:questions data) (:question-id answer))]
(update-in data [:questions q-index :full-question :answers]
conj answer)))

Using clojure, you have the update-in function found here and the assoc found here. You can use the code suggested by Alex for update-in. assoc is fairly similar,
(defn change [ma a-map id]
(assoc (:questions ma)
(if-let [xq (first (filter int? (map-indexed (fn [idx mp] (if (= (:id mp) id) idx nil)) (:questions ma))))]
xq
(inc (count ma)))
a-map))
You can update your map as
(change o-map n-map idx) ;;param-1 is map to change,
;;param-2 is new-answer,
;;idx is the :id to change.
You can also refer to assoc-in found here which also associates a value in a nested associative structure.
Hope this helps.

Related

How do I filter a list of vectors in Clojure?

I am new to Clojure and learning the properties of various data structures in Clojure. Here, I have a list of vectors as follows:
(["1" "Christiano Ronaldo" "Portugal" "35"]
["2" "Lionel Messi" "Argentina" "32"]
["3" "Zinedine Zidane" "France" "47"])
where the first element of each vector is the id. How do I filter out single vectors from the list based on the id? For eg., id = 1 should return
["1" "Christiano Ronaldo" "Portugal" "35"]
I tried doing the same on a nested-map:
(def footballers
[
{:id 1 :name "Christiano Ronaldo" :country "Portugal" :age 35}
{:id 2 :name "Lionel Messi" :country "Argentina" :age 32}
{:id 3 :name "Zinedine Zidane" :country "France" :age 47}
]
)
and was successful using the filter function
(filter #(= (:id %) 1) footballers)
Result:
({:id 1, :name "Christiano Ronaldo", :country "Portugal", :age 35})
How do I do the same in a list of vectors using filter function?
(filterv #(= "1" (first %)) footballers) ; or `filter`
;=> [["1" "Christiano Ronaldo" "Portugal" "35"]] ; vector containing 1 vector
Please see this list of documentation.

Accessing elements of a Clojure map in a vector of maps

I have:
(def moo (my-func))
which returns:
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}]
How do I now access moo to get the :name where :id=3? Thanks.
I would rather prefer using some (since it is more logical than using filter i guess, because it is designed to find exactly one value):
(def data
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
(defn name-by-id [id data]
(some #(when (= (:id %) id) (:name %)) data))
user>
(name-by-id 3 data)
"Greg"
user>
(name-by-id 100 data)
nil
One way would be to use a filter
(def moos
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
(defn name-for-id
[id]
(:name (first (filter #(= (:id %) id) moos))))
(name-for-id 3) ; => "Greg"
(def names
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
;;get the :name where :id=3
(defn answer []
(:name (first (filter (fn [e] (= 3 (:id e))) names))))
In the above rather than names you could have moo.

Clojure - How i can count vector of entry maps

How i can count mobile and web access discarding a nil values from a list of maps? the output should be anything like this " Statistic mobile = 1 web = 2", but all is imutable on other languagens a simple i++ resolve but how is in clojure. thanks.
def data [{:name "app1" :type "mobile" }
{:name "site1" :type "web" }
{:name "site1" :type "web" }
{:name "boot" :type nil }]
(frequencies (map :type data))
gives
{"mobile" 1, "web" 2, nil 1}
user=> (for [[k v] (group-by :type data) :when k] [k (count v)])
(["mobile" 1] ["web" 2])

Merging Arrays in Clojure

I need to merge a collection of arrays based on id.
Example data:
EDIT: (changed to match Clojure data structures)
[{:id 1, :region :NA, :name :Test1, :OS :W}
{:id 1, :region :EU, :name :Test2, :OS :W}
{:id 2, :region :AS, :name :test3, :OS :L}
{:id 2, :region :AS, :name :test4, :OS :M}]
Becomes:
EDIT: (changed to match Clojure data structures)
[{:id 1, :region [:NA :EU], :name [:Test1 :Test2] ,:OS [:W]}
{:id 2, :region [:AS] :name [:test3 :Test4], :OS [:L :M]}]
| is the delimiter (changeable)
If possible, also would like alphabetical order as well.
(def data
[{:id 1, :region :NA, :name :Test1, :OS :W}
{:id 1, :region :EU, :name :Test2, :OS :W}
{:id 2, :region :AS, :name :test3, :OS :L}
{:id 2, :region :AS, :name :test4, :OS :M}])
(defn key-join
"join of map by key , value is distinct."
[map-list]
(let [keys (keys (first map-list))]
(into {} (for [k keys] [k (vec (set (map #(% k) map-list)))]))))
(defn group-reduce [key map-list]
(let [gdata (group-by key map-list)]
(into [] (for [[k m] gdata] (let [m (key-join m)](assoc m key ((key m) 0)))))))
user=> (group-reduce :id data)
[{:name [:Test2 :Test1], :OS [:W], :region [:EU :NA], :id 1} {:name [:test3 :test4], :OS [:L :M], :region [:AS], :id 2}]
You can use some combination of functions from clojure.set (if you change the outermost vector to set). Specifically clojure.set/index looks promising.
You can use the merge-with function as shown below in the example.
Firstly, we define some helper functions
(defn collect [& xs]
(apply vector (-> xs distinct sort)))
The collect function makes sure that the items in xs are unique and sorted and finally returns them in a vector.
(defn merge-keys [k xs]
(map #(apply merge-with collect %) (vals (group-by k xs))))
merge-keys first groups the hash-maps in xs by a primary key (in your case :id), takes each list of grouped items and merges the values of the keys using the collect function from above.
(def xs [{:id 1, :region :NA, :name :Test1, :OS :W}
{:id 1, :region :EU, :name :Test2, :OS :W}
{:id 2, :region :AS, :name :test3, :OS :L}
{:id 2, :region :AS, :name :test4, :OS :M}])
(merge-keys :id xs)
=> ({:id [1],
:region [:EU :NA],
:name [:Test1 :Test2],
:OS [:W]}
{:id [2],
:region [:AS],
:name [:test3 :test4],
:OS [:L :M]})
Note however that even the :id key now has vector associated with it. You can easily un-vector it by either introducing an if statement in collect which associates a single value with the key instead of a vector...
(defn collect [& xs]
(let [cs (apply vector (-> xs distinct sort))]
(if (= 1 (count cs)) (first cs) cs)))
...or take the result from merge-keys and do
(map #(update-in % [:id] first) result)
which will only un-vector the :id map entry

hiccup code not responding without (do (html5 at each level

I am not able to run inner [:tr] without (do (html5 ..)) when i am using nested let statement having for loop.
(defpartial column-settings-layout [& content]
(html5
[:head
[:title "my-noir-proj"]
(include-css "assets/css/bootstrap.css")
]
[:body
[:div
[:div
[:image {:src "assets/img/ceva.gif" :alt "ceva-logo"}]
(toolbar)
]
[:div {:style "overflow: auto; overflow-x: hidden"}
content]
[:form {:id "col_settings_form" :name "col_settings_form" :method="post" :enctype="multipart/form-data"}
[:input {:type "button" :value "Save" :onclick "ajaxWithSerialize('save_cols_response_div','/save_cols_settings',$(col_settings_form).serialize());"}]
[:table {:class "table-striped" :border "1"}
[:tr [:td {:id "processing_status" } ][:td {:id "save_cols_response_div" :colspan 6} ]]
[:tr [:td ][:td {:colspan "3"} "SP" ] [:td {:colspan "3"} "WP"]]
(let [wp_settings (session/get :wp_settings)
sp_settings (session/get :sp_settings)]
(do (html5 [:tr [:td {:colspan "7"} "jhyghjghj"]]))
(for [col (:all_cols (session/get :setting_doc))]
(let
[
dest_station (keyword (session/get :dest_station))
;col_nm (:col_nm (nth col 1))
field_nm (nth col 0)
sp_col_nm (:col_nm (field_nm (dest_station sp_settings)))
wp_col_nm (:col_nm (field_nm (dest_station wp_settings)))
sp_editable (:editable (field_nm (dest_station sp_settings)))
wp_editable (:editable (field_nm (dest_station wp_settings)))
]
(do (html5 [:tr[:td "sfsdfgfds"]]
[:tr
[:th { :align "right" :class "input-small" } field_nm ]
[:td {:title sp_editable }[:input {:type "text" :class "input-large" :name (str "page_sp[" dest_station "][" field_nm "][col_nm]") :value sp_col_nm } ] ]
[:td [:input {:type "checkbox" :name (str "page_sp[" dest_station "][" field_nm "][col_nm]") :value field_nm}]]
[:td [:input {:type "checkbox" :name (str "page_sp[" dest_station "][" field_nm "][editable]") :value field_nm}]]
[:td {:title wp_editable }[:input {:type "text" :class "input-large" :name (str "page_wp[" dest_station "][" field_nm "][col_nm]") :value wp_col_nm} ] ]
[:td [:input {:type "checkbox" :name (str "page_wp[" dest_station "][" field_nm "][col_nm]") :value field_nm}]]
[:td [:input {:type "checkbox" :name (str "page_wp[" dest_station "][" field_nm "][editable]") :value field_nm}]]
]))
)
)
)
]
]
(footer)
;my includes of js and css
]]))
Your problem is likely that you're attempting to do something like
[:tr (for ... [:td .])]
which results in the invalid hiccup format
[:tr [[:td ..] [:td ..] ..]] ; note the vector of vectors inside the :tr
where hiccup expects
[:tr [:td ..] [:td ..] ..] ; :td vectors are direct elements of :tr
To get the expected formatting, you need something like
(into [:tr] (for ... [:td .]))
update: the reason your (html..) construct also fixes this problem is that it turns the whole sequence of formatted hiccup tags into a single HTML string. And you can drop the do - it's not doing anything useful.
You want to instead write
(html5
[:tr ...]
(for ...))
Or even just
(cons [:tr ...]
(for ...])
do is for side effects, and hiccup is purely functional, with no side effects. Instead of using do to group side effects, you use lists to group document elements.