I'd like to have a function that removes any node (sub-collection) from a collection containing that node.
(def coll {:a ["b" {:c "d" :e ["f" {:g "h"}]}]})
(def node {:g "h"})
What would be a good remove-node function?
(remove-node coll node)
;=> {:a ["b" {:c "d" :e ["f"]}]})
Thanks!
EDIT :
What I want to do is delete an enlive-node
(def enlive-node
[{:type :dtd, :data ["html" nil nil]}
{:tag :html,
:attrs nil,
:content ["\n"
{:tag :head,
:attrs nil,
:content ["\n \n "
{:tag :title,
:attrs nil,
:content ["Stack Overflow"]}
"\n "
{:tag :link,
:attrs {:href "//cdn.sstatic.net/stackoverflow/img/favicon.ico",
:rel "shortcut icon"},
:content nil}]}]}])
The node to remove is always a string or an entire hash-map.
(remove-node enlive-node {:tag :title,
:attrs nil,
:content ["Stack Overflow"]})
For the example you have, you can use clojure.walk/postwalk to walk the hashmap and remove the node.
(require '[clojure.walk :as walk])
(defn remove-node [coll target]
(walk/postwalk
(fn [item]
(if (vector? item)
(filterv #(not= target %) item)
item))
coll))
(remove-node coll node)
EDIT:
From you updated question, it looks like you are operating on an Enlive node collection. An additional solution, in your case, would be to generate an Enlive node selector and transform the collection using the net.cgrand.enlive-html/at* function.
(require '[net.cgrand.enlive-html :as e])
(defn gen-transform [target]
[[(cond
(string? target) e/text-node
(map? target) (:tag target)
:else e/any-node)]
#(when (not= target %) %)])
(defn remove-node [coll & nodes]
(e/at* coll (map gen-transform nodes)))
(remove-node enlive-node
{:tag :title, :attrs nil, :content ["Stack Overflow"]}
"\n")
Related
Lets say I have a collection like:
(def xs
[{:name "Apple" :type "Fruit is a type"}
{:name "Tomato" :type "Vegetable are food"}
{:name "Pear" :type "the type can also be Fruit"}
{:name "Steak" :type "eat less Meat"}])
And I want to filter and group-by the collection into something like this:
{:Fruit [{:name "Apple" :type "Fruit is a type"} {:name "Pear" :type "the type can also be Fruit"}] :Vegetable [{:name "Tomato" :type "Vegetable are food"}]
I currently just filter the results but can't seem to figure out a good way to group-by. Here's what I have so far:
(defn filter-response [x query]
(filter #(s/includes? (:type %) query) x))
(defn group-by-types [queries]
(map #(filter-response xs %) queries))
(group-by-types ["Fruit" "Vegetable"])
How can I accomplish this?
Updated Answer
You can use a list comprehension to check each item in the collection for each pattern.
(defn- all-occurrences [xs patterns]
(for [x xs
pattern patterns
:when (clojure.string/includes? (:type x) pattern)]
[(keyword pattern) x]))
Or using your filter-response function:
(defn- all-occurrences [xs patterns]
(for [pattern patterns
x (filter-response xs pattern)]
[(keyword pattern) x]))
Then use reduce with update to merge the list of occurrences into a single map:
(defn group-by-patterns [xs patterns]
(reduce (fn [m [pattern text]] (update m pattern conj text))
{}
(all-occurrences xs patterns)))
Calling it with the new input:
(def xs
[{:name "Apple" :type "Fruit is a type"}
{:name "Tomato" :type "Vegetable are food"}
{:name "Pear" :type "the type can also be Fruit"}
{:name "Steak" :type "eat less Meat"}])
(group-by-patterns xs ["Fruit" "Vegetable"])
=> {:Fruit ({:name "Pear", :type "the type can also be Fruit"} {:name "Apple", :type "Fruit is a type"}),
:Vegetable ({:name "Tomato", :type "Vegetable are food"})}
Original Answer
First you can use group-by to group by values under specified keys:
(def xs
[{:name "Apple" :type "Fruit"}
{:name "Tomato" :type "Vegetable"}
{:name "Pear" :type "Fruit"}
{:name "Steak" :type "Meat"}])
erdos=> (group-by :type xs)
{"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
"Vegetable" [{:name "Tomato", :type "Vegetable"}],
"Meat" [{:name "Steak", :type "Meat"}]}
Then use select-keys to filter the keys:
erdos=> (select-keys (group-by :type xs) ["Fruit" "Vegetable"])
{"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
"Vegetable" [{:name "Tomato", :type "Vegetable"}]}
If you need keyword keys, you need an extra mapping step:
erdos=> (into {}
(for [[k v] (select-keys (group-by :type xs) ["Fruit" "Vegetable"])]
[(keyword k) v]))
{:Fruit [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
:Vegetable [{:name "Tomato", :type "Vegetable"}]}
I need to match two kinds of tuples and produce maps from them.
Both have a keyword and a string. One can have a third item (a language code).
[<key> <value>] ~> {:type <key> :value <value>}
[<key> <value> <lang>] ~> {:type <key> :value <value> :lang <lang>}
I only need to match those which keyword is either :foo or :bar and decided that I would use clojure.core.match:
(ns so.example
(:require
[clojure.core.match :refer [match]]))
(defn example-1 [ast]
(let [l10n-key #{:foo :bar}]
(match ast
[(k :guard l10n-key) v lang] {:type k :value v :lang lang}
[(k :guard l10n-key) v] {:type k :value v})))
(example-1 [:foo 10])
;=> {:type :foo, :value 10}
(example-1 [:bar 20 "en"])
;=> {:type :bar, :value 20, :lang "en"}
That works but I wanted to reuse the matching pattern :guard l10n-key in different clauses. So I thought I could use some syntax quoting and unquote splicing:
(defn example-2 [ast]
(let [l10n-key-match [:guard #{:foo :bar}]]
(match ast
[`(k ~#l10n-key-match) v lang] {:type k :value v :lang lang}
[`(k ~#l10n-key-match) v] {:type k :value v})))
However the defn expression crashes with:
Unexpected error (AssertionError) macroexpanding match at (form-init11111096422056977084.clj:3:5).
Invalid list syntax (clojure.core/concat (clojure.core/list (quote so.example/k)) l10n-key-match) in (clojure.core/seq (clojure.core/concat (clojure.core/list (quote so.example/k)) l10n-key-match)). Valid syntax: [[:default :guard] [:or :default] [:default :only] [:default :seq] [:default :when] [:default :as] [:default :<<] [:default :clojure.core.match/vector]]
What am I doing wrong?
Isn't this what spec, that already ships with Clojure, does? You would define your pattern like
(ns playground.catspec
(:require [clojure.spec.alpha :as spec]))
(spec/def ::type #{:foo :bar})
(spec/def ::value number?)
(spec/def ::lang #{"en" "sv" "fr"})
(spec/def ::key-value-lang (spec/cat :type ::type
:value ::value
:lang (spec/? ::lang)))
We use spec/def to define a spec, spec/cat to concatenate specs and spec/? for a spec that is optional.
Then we use conform to parse the tuple:
(spec/conform ::key-value-lang [:foo 10])
;; => {:type :foo, :value 10}
(spec/conform ::key-value-lang [:bar 20 "en"])
;; => {:type :bar, :value 20, :lang "en"}
(spec/conform ::key-value-lang [:bar 119 "fr"])
;; => {:type :bar, :value 119, :lang "fr"}
(spec/conform ::key-value-lang [119 :foo])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar 119 "uj"])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar 119 "fr" :asdfasdf])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang {:a 1 :b 4})
;; => :clojure.spec.alpha/invalid
Doesn't solve the problem with clojure.core.match, but you don't really need it for something this simple:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(dotest
(let [data [[:foo 10]
[:bar 20 "en"]
[:fizz 10]
[:buzz 20 "en"]]
keep-tags #{:foo :bar}
data-keep (filterv #(contains? keep-tags (first %)) data)
result (forv [tuple data-keep]
(zipmap [:type :value :lang] tuple))]
(is= result [{:type :foo, :value 10}
{:type :bar, :value 20, :lang "en"}])))
You may also be interested in these helper functions:
tupelo.core/matches? A nicer interface to clojure.core.match
tupelo.core/submatch?
tupelo.core/wild-match?
tupelo.core/wild-submatch?
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.
Please look at the following code:
(def data {:color ["R", "B", "G"] :name "Hello" :up "down"})
(defn collapse-vector-kvp [k v]
(map #(hash-map k %) v))
(defn collapse-map [m]
(map #(let
[x %]
(if (vector? (val x))
(collapse-vector-kvp (key x) (val x))
(hash-map (key x) (val x))
)) m))
(collapse-map data)
=> ({:name "Hello"} ({:color "R"} {:color "B"} {:color "G"}) {:up "down"})
What I would like to do is create a single list, rather than have the 'color' entries be in a list within the list. Is this easily achievable?
user=> (def data2 '({:name "Hello"} ({:color "R"} {:color "B"} {:color "G"}) {:up "down"}))
#'user/data2
user=> (flatten data2)
({:name "Hello"} {:color "R"} {:color "B"} {:color "G"} {:up "down"})
Another version of collapse-map:
(defn collapse-map [m]
(let [sep-m (group-by (comp vector? val) m)]
(concat (map (fn [[k v]] {k v})
(sep-m false))
(apply concat (map (fn [[k v]]
(collapse-vector-kvp k v))
(sep-m true))))))
(def test-data {:color ["R" "B" "G"]
:name "Hello"
:k ["v1" "v2" "v3"]
:up "down"})
(collapse-map test-data)
=> ({:name "Hello"}
{:up "down"}
{:color "R"}
{:color "B"}
{:color "G"}
{:k "v1"}
{:k "v2"}
{:k "v3"})
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