I can't understand why the following spec error occurred.
Could anyone tell me why?
(ns spec-test
(:require [clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as stest]))
;;; spec definitions
(s/def :msds.fontspec/tag #{:fontspec})
(s/def ::id string?)
(s/def ::size int?)
(s/def ::family string?)
(s/def ::color string?)
(s/def :msds.fontspec/attrs
(s/keys :req-un [::id ::size ::family ::color]))
(s/def :msds.fontspec/content (s/cat))
(s/def ::fontspec
(s/keys :req-un [:msds.fontspec/tag :msds.fontspec/attrs :msds.fontspec/content]))
(s/def ::fontspecs (s/* ::fontspec))
;;; spec application
(defn transform-fontspecs
[fontspecs]
(->> fontspecs
(map (fn [{:keys [attrs] :as fontspec}]
[(get attrs :id) (dissoc attrs :id)] ))
(into {}) ))
(s/fdef transform-fontspecs
:args (s/cat :fontspec ::fontspecs))
(stest/instrument `transform-fontspecs)
;;; test code
(def test-sample
'({:tag :fontspec,
:attrs {:id "283f1EV", :size 21, :family "c2S6", :color ""},
:content ()}
{:tag :fontspec,
:attrs
{:id "BzYMt4eEm", :size -5, :family "q0xhOE3", :color "bhV3PsVh"},
:content ()}
{:tag :fontspec,
:attrs {:id "uA2i", :size -5, :family "HHGoD", :color "J47ZX935"},
:content ()}))
(transform-fontspecs test-sample)
The following is the error descriptions.
2. Unhandled clojure.lang.Compiler$CompilerException
Error compiling src/spec_test.clj at (55:1)
#:clojure.error{:phase :compile-syntax-check,
:line 55,
:column 1,
:source "/home/philos/work/metapdf/src/spec_test.clj"}
Compiler.java: 7648 clojure.lang.Compiler/load
REPL: 1 metapdf.core/eval19243
REPL: 1 metapdf.core/eval19243
Compiler.java: 7177 clojure.lang.Compiler/eval
Compiler.java: 7132 clojure.lang.Compiler/eval
core.clj: 3214 clojure.core/eval
core.clj: 3210 clojure.core/eval
main.clj: 437 clojure.main/repl/read-eval-print/fn
main.clj: 437 clojure.main/repl/read-eval-print
main.clj: 458 clojure.main/repl/fn
main.clj: 458 clojure.main/repl
main.clj: 368 clojure.main/repl
RestFn.java: 1523 clojure.lang.RestFn/invoke
interruptible_eval.clj: 79 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 55 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 142 nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
AFn.java: 22 clojure.lang.AFn/run
session.clj: 171 nrepl.middleware.session/session-exec/main-loop/fn
session.clj: 170 nrepl.middleware.session/session-exec/main-loop
AFn.java: 22 clojure.lang.AFn/run
Thread.java: 748 java.lang.Thread/run
1. Caused by clojure.lang.ExceptionInfo
Spec assertion failed.
Spec: #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x3660c4ee "clojure.spec.alpha$regex_spec_impl$reify__2509#3660c4ee"]
Value: (({:tag :fontspec,
:attrs {:id "283f1EV", :size 21, :family "c2S6", :color ""},
:content ()}
{:tag :fontspec,
:attrs
{:id "BzYMt4eEm", :size -5, :family "q0xhOE3", :color "bhV3PsVh"},
:content ()}
{:tag :fontspec,
:attrs {:id "uA2i", :size -5, :family "HHGoD", :color "J47ZX935"},
:content ()}))
Problems:
val: ({:tag :fontspec,
:attrs {:id "283f1EV", :size 21, :family "c2S6", :color ""},
:content ()}
{:tag :fontspec,
:attrs
{:id "BzYMt4eEm", :size -5, :family "q0xhOE3", :color "bhV3PsVh"},
:content ()}
{:tag :fontspec,
:attrs {:id "uA2i", :size -5, :family "HHGoD", :color "J47ZX935"},
:content ()})
in: [0]
failed: map?
spec: :spec-test/fontspec
alpha.clj: 132 clojure.spec.test.alpha/spec-checking-fn/conform!
alpha.clj: 140 clojure.spec.test.alpha/spec-checking-fn/fn
RestFn.java: 408 clojure.lang.RestFn/invoke
spec_test.clj: 55 spec-test/eval19293
spec_test.clj: 55 spec-test/eval19293
Compiler.java: 7177 clojure.lang.Compiler/eval
Compiler.java: 7636 clojure.lang.Compiler/load
REPL: 1 metapdf.core/eval19243
REPL: 1 metapdf.core/eval19243
Compiler.java: 7177 clojure.lang.Compiler/eval
Compiler.java: 7132 clojure.lang.Compiler/eval
core.clj: 3214 clojure.core/eval
core.clj: 3210 clojure.core/eval
main.clj: 437 clojure.main/repl/read-eval-print/fn
main.clj: 437 clojure.main/repl/read-eval-print
main.clj: 458 clojure.main/repl/fn
main.clj: 458 clojure.main/repl
main.clj: 368 clojure.main/repl
RestFn.java: 1523 clojure.lang.RestFn/invoke
interruptible_eval.clj: 79 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 55 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 142 nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
AFn.java: 22 clojure.lang.AFn/run
session.clj: 171 nrepl.middleware.session/session-exec/main-loop/fn
session.clj: 170 nrepl.middleware.session/session-exec/main-loop
AFn.java: 22 clojure.lang.AFn/run
.lang.Thread/run
You should check the Problems part where it says val and failed: map?.
The problem is that you're using regex operations like s/* which, when nested, sort of flattens the structure so it's expecting a map rather than a collection.
You should just use coll-of here, imho.
See https://clojure.org/guides/spec#_collections
Also the :msds.fontspec/content spec should probably be updated to not use cat but just vector? or something like that.
Related
We have a cursor or atom map with this example data:
#<Cursor: [:customer] {:name Diego Peña,
:addresses [{:id 23, :province Madrid, :country 1, :descripcion aaeeeeeeee iii oooo4444, :locality Gali gali, :country_name SPAIN, :direccion Street Cierva, :id 3, :postalcode 30203, :principal true, :customer 17}
{:id 35, :province Madrid, :country nil, :descripcion yyy lalala3, :locality Lalala, :direccion calle Maria 3 , :postalcode 333, :principal false, :customer 17}
{:id 6, :province Madrid, :country 2, :descripcion otra direccioncita444, :locality Leleele, :country_name SPAIN, :direccion Direccion calle Ooo, :postalcode 1236, :main false, :customer 17}
{:id 27, :province Madrid, :country 1, :descripcion grandisima, :locality Alcantarilla, :country_name SPAIN, :direccion C/ 3 Mayo, :postalcode 3001, :main false, :customer 17}
]}>
I need to change the values of a searched address by id. I have managed to locate the address by the value of the id:
(defn get-address [pk]
(->> #db/customer :addresses (filter #(= (int pk) (int (:id %)))) first)
)
I can change all addresses with this: :ok #(swap! db/customer assoc-in [:addresses] %)}). I need to change data for a specific address from the API response.
I am close to getting it, but with this approach I am missing the position or the index of the item: #(swap! db/client assoc-in [:addresses ¿position or index in map?] %) we have the id of item address.
Perhaps this approach is wrong, a better one?
The assoc, assoc-in, update, and update-in functions work also on vectors. In Clojure, vectors are associative data structures where the key is the numeric index (O..n) and the value is the item at position n.
So you can do:
(assoc [:a :b :c] 1 :new-value)
;; ^ ^ ^
;; 0 1 2
;; => [:a :new-value :c]
Based on your example, you will need this:
(defn address-index-by-id
"Take an `address` map and look it up by `:id` in the `addresses` list.
Return the numeric index where it was found, nil if not found."
[address addresses]
(->> (map-indexed vector addresses)
;; produce a seq `([0 val-at-index-0] … [n val-at-index-n])`
(filter (fn [[_index {:keys [id]}]] (= id (:id address)))) ;; filter by id
(ffirst) ;; get the index of the first match
))
(defn set-address
"Take a `customer` map and an `address` map. Will put the `address` in the
customer's addresses list. If an address with the same :id key is already
present in this list, it will be overwritten."
[customer address]
(if-let [existing-index (address-index-by-id address (:addresses customer))]
(assoc-in customer [:addresses existing-index] address)
(update customer :addresses conj address)))
Usage:
(set-address {:name "Diego Peña"
:addresses []}
{:id 1
:province "Madrid"})
;; => {:name "Diego Peña", :addresses [{:id 1, :province "Madrid"}]}
(-> {:name "Diego Peña"
:addresses [{:id 1
:province "Madrid"
:main true}
{:id 2
:province "Barcelona"
:main false}]}
(set-address {:id 2
:province "Barcelona"
:main true})
(set-address {:id 1
:province "Madrid"
:main false}))
;; => {:name "Diego Peña", :addresses [{:id 1, :province "Madrid", :main false} {:id 2, :province "Barcelona", :main true}]}
;; And of course if your `customer` is stored in an Atom:
(swap! customer set-address {:id 1, :province "Madrid", :main true})
It looks like you are pulling data out of a database of some kind. If so, you should let the DB search for the ID in question. You can then read or update that record.
If you really need to do it in Clojure, you can search for the desired customer map using the tupelo.forest library. Here is an example with your data:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require [tupelo.forest :as tf]))
(def customer
{:name "Diego Peña",
:addresses
[{:id 23
:province "Madrid"
:country 1
:descripcion " aaeeeeeeee iii oooo4444"
:locality "Gali gali"
:country_name "SPAIN"
:direccion "Street Cierva"
:postalcode 30203
:principal true
}
{:id 35
:province "Madrid"
:country nil
:descripcion "yyy lalala3"
:locality "Lalala"
:direccion "calle Maria 3"
:postalcode 333
:principal false
:customer 17}
{:id 6
:province "Madrid"
:country 2
:descripcion "otra direccioncita444"
:locality "Leleele"
:country_name "SPAIN"
:direccion "Direccion calle Ooo"
:postalcode 1236
:main false
:customer 17}
{:id 27
:province "Madrid"
:country 1
:descripcion "grandisima"
:locality "Alcantarilla"
:country_name "SPAIN"
:direccion "C / 3 Mayo"
:postalcode 3001
:main false
:customer 17}
]})
and some code to find customer with :id 35
(dotest
(tf/with-forest (tf/new-forest)
(let [root-hid (tf/add-tree-edn customer)
cust-num 35
paths-found (tf/find-paths root-hid [:**
{:tag :tupelo.forest/entry, :tupelo.forest/key :id}
{:tupelo.forest/value cust-num}])
cust-path-rev (reverse (last paths-found))
cust-entity-hid (xthird cust-path-rev)
]
(is= (tf/hid->bush (xfirst cust-path-rev)) [#:tupelo.forest{:value 35, :index nil}])
(is= (tf/hid->tree cust-entity-hid)
{:tag :tupelo.forest/entity,
:tupelo.forest/index 1,
:tupelo.forest/kids [{:tag :tupelo.forest/entry,
:tupelo.forest/key :locality,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value "Lalala", :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :customer,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value 17, :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :descripcion,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value "yyy lalala3", :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :direccion,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value "calle Maria 3", :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :id,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value 35, :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :postalcode,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value 333, :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :principal,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value false, :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :province,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value "Madrid", :index nil}]}
{:tag :tupelo.forest/entry,
:tupelo.forest/key :country,
:tupelo.forest/kids [#:tupelo.forest{:kids [], :value nil, :index nil}]}]})
You can convert the data from the internal tree format back into EDN data:
(is= (tf/hid->edn cust-entity-hid)
{:locality "Lalala",
:customer 17,
:descripcion "yyy lalala3",
:direccion "calle Maria 3",
:id 35,
:postalcode 333,
:principal false,
:province "Madrid",
:country nil})
)))
You never really said how you want to change the data. Again, it would probably be best to do this with the DB rather than changing Clojure data structures.
Update
Another option is to use a recursive walk like clojure.walk/postwalk or the enhanced tupelo.core/walk-with-parents. Example to uppercase street name for cust ID #35
(dotest
(let [modified (t/walk-with-parents customer
{:enter (fn [parents item]
(with-nil-default item
(when (and (map? item)
(t/submap? {:id 35} item))
(spyx-pretty item)
(update item :direccion str/upper-case))))})]
(is= modified {:name "Diego Peña",
:addresses
[{:locality "Gali gali",
:descripcion " aaeeeeeeee iii oooo4444",
:country_name "SPAIN",
:direccion "Street Cierva",
:id 23,
:postalcode 30203,
:principal true,
:province "Madrid",
:country 1}
{:locality "Lalala",
:customer 17,
:descripcion "yyy lalala3",
:direccion "CALLE MARIA 3",
:id 35,
:postalcode 333,
:principal false,
:province "Madrid",
:country nil}
{:locality "Leleele",
:customer 17,
:descripcion "otra direccioncita444",
:country_name "SPAIN",
:direccion "Direccion calle Ooo",
:id 6,
:postalcode 1236,
:main false,
:province "Madrid",
:country 2}
{:locality "Alcantarilla",
:customer 17,
:descripcion "grandisima",
:country_name "SPAIN",
:direccion "C / 3 Mayo",
:id 27,
:postalcode 3001,
:main false,
:province "Madrid",
:country 1}]})))
I have a vector of maps (result of xml/parse) which contains the following vector of nested maps (I already got rid of some parts I don't want to keep):
[
{:tag :SoapObject, :attrs nil, :content [
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_1"]}
{:tag :FieldValue, :attrs nil, :content ["Value_1a"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_2"]}
{:tag :FieldValue, :attrs nil, :content ["Value_2a"]}
]}
]}
{:tag :SoapObject, :attrs nil, :content [
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_1"]}
{:tag :FieldValue, :attrs nil, :content ["Value_1b"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_2"]}
{:tag :FieldValue, :attrs nil, :content ["Value_2b"]}
]}
]}
]
Now I want to extract only some specific data from this structure, producing a result which looks like this:
[
{"ID" "8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1",
"Attribute_1" "Value_1a",
"Attribute_2" "Value_1a"}
{"ID" "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1",
"Attribute_1" "Value_1b",
"Attribute_2" "Value_1b"}
]
Which clojure tool could help me accomplish this?
I've found another question which is a bit similar, but whenever I tried some version of a map call the result I got was some kind of clojure.lang.LazySeq or clojure.core$map which I couldn't get to print properly to verify the result.
usually you can start from the bottom, gradually going up:
first you would like to parse the attr item:
(def first-content (comp first :content))
(defn get-attr [{[k v] :content}]
[(first-content k)
(first-content v)])
user> (get-attr {:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"]}
]})
;;=> ["ID" "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"]
then you would turn every item into a map of attrs:
(defn parse-item [item]
(into {} (map get-attr (:content item))))
(parse-item {:tag :SoapObject, :attrs nil, :content [
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_1"]}
{:tag :FieldValue, :attrs nil, :content ["Value_1b"]}
]}
{:tag :ObjectData, :attrs nil, :content [
{:tag :FieldName, :attrs nil, :content ["Attribute_2"]}
{:tag :FieldValue, :attrs nil, :content ["Value_2b"]}
]}
]})
;;=> {"ID" "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1", "Attribute_1" "Value_1b", "Attribute_2" "Value_2b"}
so the last thing you need do, is to map over the top level form, producing the required result:
(mapv parse-item data)
;;=> [{"ID" "8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1", "Attribute_1" "Value_1a", "Attribute_2" "Value_2a"}
;; {"ID" "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1", "Attribute_1" "Value_1b", "Attribute_2" "Value_2b"}]
You can easily solve tree-based problems using the Tupelo Forest library. You can see a video introduction from last year's Clojure Conj here.
For your problem, I'd approach it as follows. First, the data:
(dotest
(let [data-enlive
{:tag :root
:attrs nil
:content
[{:tag :SoapObject, :attrs nil,
:content
[{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1"]}]}
{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["Attribute_1"]}
{:tag :FieldValue, :attrs nil, :content ["Value_1a"]}]}
{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["Attribute_2"]}
{:tag :FieldValue, :attrs nil, :content ["Value_2a"]}]}]}
{:tag :SoapObject, :attrs nil,
:content
[{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["ID"]}
{:tag :FieldValue, :attrs nil, :content ["90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"]}]}
{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["Attribute_1"]}
{:tag :FieldValue, :attrs nil, :content ["Value_1b"]}]}
{:tag :ObjectData, :attrs nil,
:content [{:tag :FieldName, :attrs nil, :content ["Attribute_2"]}
{:tag :FieldValue, :attrs nil, :content ["Value_2b"]}]}]}]}]
and then the code
(with-debug-hid
(with-forest (new-forest)
(let [root-hid (add-tree-enlive data-enlive)
soapobj-hids (find-hids root-hid [:root :SoapObject])
objdata->map (fn [objdata-hid]
(let [fieldname-node (hid->node (find-hid objdata-hid [:ObjectData :FieldName]))
fieldvalue-node (hid->node (find-hid objdata-hid [:ObjectData :FieldValue]))]
{ (grab :value fieldname-node) (grab :value fieldvalue-node) }))
soapobj->map (fn [soapobj-hid]
(apply glue
(for [objdata-hid (hid->kids soapobj-hid)]
(objdata->map objdata-hid))))
results (mapv soapobj->map soapobj-hids)]
with intermediate results:
(is= (hid->bush root-hid)
[{:tag :root}
[{:tag :SoapObject}
[{:tag :ObjectData}
[{:tag :FieldName, :value "ID"}]
[{:tag :FieldValue, :value "8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1"}]]
[{:tag :ObjectData}
[{:tag :FieldName, :value "Attribute_1"}]
[{:tag :FieldValue, :value "Value_1a"}]]
[{:tag :ObjectData}
[{:tag :FieldName, :value "Attribute_2"}]
[{:tag :FieldValue, :value "Value_2a"}]]]
[{:tag :SoapObject}
[{:tag :ObjectData}
[{:tag :FieldName, :value "ID"}]
[{:tag :FieldValue, :value "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1"}]]
[{:tag :ObjectData}
[{:tag :FieldName, :value "Attribute_1"}]
[{:tag :FieldValue, :value "Value_1b"}]]
[{:tag :ObjectData}
[{:tag :FieldName, :value "Attribute_2"}]
[{:tag :FieldValue, :value "Value_2b"}]]]])
(is= soapobj-hids [:0009 :0013])
and the final results:
(is= results
[{"ID" "8d8edbb6-cb0f-11e8-a8d5-f2801f1b9fd1",
"Attribute_1" "Value_1a",
"Attribute_2" "Value_2a"}
{"ID" "90e39036-cb0f-11e8-a8d5-f2801f1b9fd1",
"Attribute_1" "Value_1b",
"Attribute_2" "Value_2b"}]))))))
Further documentation is still in progress, but you can see API docs here and a live example of your problem here.
You can also compose transducers. I was reading the other day something on JUXT blog about creating xpath like functionality with transducers.
(def children (map :content))
(defn tagp [pred]
(filter (comp pred :tag)))
(defn tag= [tag-name]
(tagp (partial = tag-name)))
(def text (comp (mapcat :content) (filter string?)))
(defn fields [obj-datas]
(sequence (comp
(tag= :ObjectData)
(mapcat :content)
text)
obj-datas))
(defn clean [xml-map]
(let [fields-list (sequence (comp
(tag= :SoapObject)
children
(map fields))
xml-map)]
(map (partial apply hash-map) fields-list)))
No need for fancy tools here. You can get away with the simplest chunk of code.
(use '[plumbing.core])
(let [A ...your-data...]
(map (fn->> :content
(mapcat :content)
(mapcat :content)
(apply hash-map))
A))
I have a vector of maps wherein I need to remove the maps where the value of the name key is a duplicate, keeping the one that has the highest value of age. I have a solution but I don't think it looks clean. Is there a better way to do it without breaking it up into multiple functions?
Here is my data:
(def my-maps
[{:name "jess", :age 32}
{:name "ruxpin", :age 4}
{:name "jess", :age 35}
{:name "aero", :age 33}
{:name "banner", :age 4}])
Here is my solution:
(map first (vals (group-by :name (reverse (sort-by :name my-maps)))))
Result:
({:name "ruxpin", :age 4} {:name "jess", :age 35} {:name "banner", :age 4} {:name "aero", :age 33})
another way is the combination of group-by and max-key. The advantage of this method is that you don't need to sort your collection, and sort in turn has an impact on performance and if it can be avoided it should be.
(for [[_ vs] (group-by :name my-maps)]
(apply max-key :age vs))
;;=> ({:name "jess", :age 35}
;; {:name "ruxpin", :age 4}
;; {:name "aero", :age 33}
;; {:name "banner", :age 4})
short version
(->> my-set
(sort-by (juxt :name :age) #(compare %2 %1)) ; sort-by :name, :age in reverse order
(partition-by :name)
(map first))
a transducer version
(def xf (comp (partition-by :name) (map first)))
(->> my-set
(sort-by (juxt :name :age) #(compare %2 %1))
(into [] xf))
for large dataset, the transducer should be better
Your original solution was actually broken unfortunately. It just seemed to work because of the order you had the data in my-set in. Note how you never actually sort by age, so you can never guarantee what order the ages are in.
I solved this with another call to map:
(->> my-set (group-by :name)
(vals)
; Sort by age each list that group-by returns
(map #(sort-by :age %))
(map last)) ; This could also happen in the above map
Note how I'm sorting each :name group by :age, then I take the last of each grouping.
I would do it a little differently, using the max function instead of sorting:
(def my-maps
[{:name "jess", :age 32}
{:name "ruxpin", :age 4}
{:name "jess", :age 35}
{:name "aero", :age 33}
{:name "banner", :age 4}])
(dotest
(let [grouped-data (group-by :name my-maps)
name-age-maps (for [[name map-list] grouped-data]
(let [max-age (apply max
(map :age map-list))
name-age-map {name max-age}]
name-age-map))
final-result (reduce into {} name-age-maps)]
final-result))
with results:
grouped-data =>
{"jess" [{:name "jess", :age 32} {:name "jess", :age 35}],
"ruxpin" [{:name "ruxpin", :age 4}],
"aero" [{:name "aero", :age 33}],
"banner" [{:name "banner", :age 4}]}
name-age-maps =>
({"jess" 35} {"ruxpin" 4} {"aero" 33} {"banner" 4})
final-result =>
{"jess" 35, "ruxpin" 4, "aero" 33, "banner" 4}
Compare by vector fields with different weight and data type (size has more weight), size is descending, name is ascending:
(def some-vector [{:name "head" :size 3}
{:name "mouth" :size 1}
{:name "nose" :size 1}
{:name "neck" :size 2}
{:name "chest" :size 10}
{:name "back" :size 10}
{:name "abdomen" :size 6}
])
(->> (some-vector)
(sort #(compare (str (format "%3d" (:size %2)) (:name %1))
(str (format "%3d" (:size %1)) (:name %2))
)))
Suppose that I have a Clojure map like this:
(def mymap {:a [1 2 3] :b {:c [] :d [1 2 3]}})
I would like a function remove-empties that produces a new map in which entries from (:b mymap) that have an empty sequence as a value are removed. So (remove-empties mymap) would give the value:
{:a [1 2 3] :b {:d [1 2 3]}}
Is there a way to write a function to do this using Specter?
Here's how to do it with Specter:
(use 'com.rpl.specter)
(setval [:b MAP-VALS empty?] NONE my-map)
=> {:a [1 2 3], :b {:d [1 2 3]}}
In English, this says "Under :b, find all the map values that are empty?. Set them to NONE, i.e. remove them."
(update my-map :b (fn [b]
(apply dissoc b
(map key (filter (comp empty? val) b)))))
This is the specter solution:
(ns myns.core
(:require
[com.rpl.specter :as spc]))
(def my-map
{:a [1 2 3]
:b {:c []
:d [1 2 3]}})
(defn my-function
[path data]
(let [pred #(and (vector? %) (empty? %))]
(spc/setval [path spc/MAP-VALS pred] spc/NONE data)))
;; (my-function [:b] my-map) => {:a [1 2 3]
;; :b {:d [1 2 3]}}
I don't know specter either, but this is pretty simple to do in plain clojure.
(defn remove-empties [m]
(reduce-kv (fn [acc k v]
(cond (map? v) (let [new-v (remove-empties v)]
(if (seq new-v)
(assoc acc k new-v)
acc))
(empty? v) acc
:else (assoc acc k v)))
(empty m), m))
Caveat: For extremely nested data structures it might stack overflow.
So far I haven't found an approach with specter's filterer, because when I test filterers they seem to receive each map entry twice (once as a map entry and once as a 2-length vector) and giving different results between those seems to cause issues. However, we shouldn't be removing empty sequences anywhere they might appear, just map entries where they're the value.
I did seem to get a clojure.walk approach working that might still interest you, though.
(ns nested-remove
(:require [com.rpl.specter :as s]
[clojure.walk :refer [postwalk]]))
(defn empty-seq-entry? [entry]
(and (map-entry? entry) (sequential? (val entry)) (empty? (val entry))))
(defn remove-empties [root]
(postwalk #(if (map? %) (into (empty %) (remove empty-seq-entry? %)) %) root))
(remove-empties mymap) ;;=> {:a [1 2 3], :b {:d [1 2 3]}}
Assuming we only need to go one level deep and not search recursively like the accepted answer:
(setval [:b MAP-VALS empty?] NONE mymap)
A fully recursive solution that removes empty values in a map at any level
(def my-complex-map {:a [1] :b {:c [] :d [1 2 3] :e {:f "foo" :g []}}})
; declare recursive path that traverses map values
(declarepath DEEP-MAP-VALS)
(providepath DEEP-MAP-VALS (if-path map? [MAP-VALS DEEP-MAP-VALS] STAY))
(setval [DEEP-MAP-VALS empty?] NONE my-complex-map)
; => {:a [1], :b {:d [1 2 3, :e {:f "foo"}}}}
Reference the wiki on using specter recursively.
While I am not very familiar with Specter, in addition to the postwalk solution, you can solve this using tupelo.forest from the Tupelo library. You do need to rearrange the data a bit into Hiccup or Enlive format, then it's easy to identify any nodes with no child nodes:
(ns tst.clj.core
(:use clj.core tupelo.test)
(:require
[tupelo.core :as t]
[tupelo.forest :as tf] ))
(t/refer-tupelo)
(defn hid->enlive [hid]
(tf/hiccup->enlive (tf/hid->hiccup hid)))
(defn empty-kids?
[path]
(let [hid (last path)
result (and (tf/node-hid? hid)
(empty? (grab :kids (tf/hid->tree hid))))]
result))
; delete any nodes without children
(dotest
(tf/with-forest (tf/new-forest)
(let [e0 {:tag :root
:attrs {}
:content [{:tag :a
:attrs {}
:content [1 2 3]}
{:tag :b
:attrs {}
:content [{:tag :c
:attrs {}
:content []}
{:tag :d
:attrs {}
:content [1 2 3]}
]}]}
root-hid (tf/add-tree-enlive e0)
empty-paths (tf/find-paths-with root-hid [:** :*] empty-kids?)
empty-hids (mapv last empty-paths)]
(is= (hid->enlive root-hid) ; This is the original tree structure (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :c, :attrs {}, :content []}
{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(apply tf/remove-hid empty-hids) ; remove the nodes with no child nodes
(is= (hid->enlive root-hid) ; this is the result (Enlive format)
{:tag :root,
:attrs {},
:content
[{:tag :a,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}
{:tag :b,
:attrs {},
:content
[{:tag :d,
:attrs {},
:content
[{:tag :tupelo.forest/raw, :attrs {}, :content [1]}
{:tag :tupelo.forest/raw, :attrs {}, :content [2]}
{:tag :tupelo.forest/raw, :attrs {}, :content [3]}]}]}]})
(is= (tf/hid->hiccup root-hid) ; same result in Hiccup format
[:root
[:a
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]
[:b
[:d
[:tupelo.forest/raw 1]
[:tupelo.forest/raw 2]
[:tupelo.forest/raw 3]]]])
)))
I have parse xml and get the following result
(({:tag :Column,
:attrs {:Name "VENDOR_KEY", :Type "Int", :NotNull "Yes"},
:content nil}
{:tag :Column,
:attrs {:Name "RETAILER_KEY", :Type "Int", :NotNull "Yes"},
:content nil}
{:tag :Column,
:attrs {:Name "ITEM_KEY", :Type "Int", :NotNull "Yes"},
:content nil})
({:tag :Column,
:attrs {:Name "Store_Key", :Type "Int", :NotNull "Yes"},
:content nil}))
then how to convert it to the following, basically I want to extract the value of key :attrs in nested list.
(
({:Name "VENDOR_KEY", :Type "Int", :NotNull "Yes"},
{:Name "RETAILER_KEY", :Type "Int", :NotNull "Yes"},
{:Name "ITEM_KEY", :Type "Int", :NotNull "Yes"}),
({:Name "Store_Key", :Type "Int", :NotNull "Yes"})
)
so yes right here your solution as
hsestupin said
(map #(map :attrs %) result)
i am assuming result is your input data.