How to filter vector of maps by multiple keys in Clojure

我与影子孤独终老i 提交于 2019-12-21 03:03:06

问题


Assume we have a datastructure like this one:

(def data
     (atom [{: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 have learned how to filter it by one key, for example:

(defn my-filter
  [str-input]
  (filter #(re-find (->> (str str-input)
                         (lower-case)
                         (re-pattern))
                         (lower-case (:first-name %)))
            @data))

> (my-filter "John1")
> ({:last-name "Dow1", :age "14", :first-name "John1", :id 1})

But now I'm a little bit confused on how to filter data by :first-name, :last-name and :age simple way?

Update: Sorry for being not too clear enough in explanation of what the problem is... Actually, I want all keys :first-name, :last-name and :age to paticipate in filter function, so that, if str-input doesn't match :first-name's val, check if it matches :last-name's val and so on.

Update 2: After trying some-fn, every-pred and transducers, I didn't get what I need, e.g. regex in filter predicates, I guess it's a lack of knowledge for now. So, I ended up with this function which works fine, but the code is ugly and duplicated. How I can get rid of code duplication?

(defn my-filter [str-input]
  (let [firstname (filter #(re-find (->> (str str-input)
                                         (upper-case)
                                         (re-pattern))
                                    (upper-case (:first-name %)))
                     @data)
        lastname (filter #(re-find (->> (str str-input)
                                        (upper-case)
                                        (re-pattern))
                                   (upper-case (:last-name %)))
                    @data)
        age (filter #(re-find (->> (str str-input)
                                   (upper-case)
                                   (re-pattern))
                              (upper-case (:age %)))
               @data)]
    (if-not (empty? firstname)
      firstname
      (if-not (empty? lastname)
        lastname
        (if-not (empty? age)
          age)))))

回答1:


This can also be achieved with the help of functional composition, e.g. you can use every-pred function, which creates a function, checking if all the preds are truthy for its arguments, and use it to filter data. For example if you want to find all items with odd :id value having :last-name of "Dow1", "Dow2", or "Dow3" and :age starting with \3:

user> (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"}])

user> (filter (every-pred (comp odd? :id)
                          (comp #{"Dow1" "Dow2" "Dow3"} :last-name)
                          (comp #{\3} first :age))
              data)

;;=> ({:id 3, :first-name "John3", :last-name "Dow3", :age "34"})

another way to do it, is to use transducers:

user> (sequence (comp (filter (comp odd? :id))
                      (filter (comp #{"Dow1" "Dow2" "Dow3"} :last-name)))
                data)

notice that the actual filtering would happen just once for every item, so it won't create any intermediate collections.

Update

According to your update you need to keep the value when any of the predicetes is true, so you can use some function instead of every-pred:

user> (filter #(some (fn [pred] (pred %))
                     [(comp odd? :id)
                      (comp #{"Dow1" "Dow2" "Dow4"} :last-name)
                      (comp (partial = \3) first :age)])
              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"})



回答2:


I think this would just work for you. Using the fact that in Clojure :first-name is a function that can be used to look up its corresponding value in a hashmap.

(defn find-all 
  [field value data] 
  (filter #(= value (field %)) data))

This will return a list of the matching hashmaps in your vector.

user=> (find-all :first-name "John1" @data)
({:id 1, :first-name "John1", :last-name "Dow1", :age "14"})

I would suggest you to store age as integer instead of string if you do not have a strong case not to.

More about keywords:

(:key map)

  • works when your map is nil
user=> (:key-word nil)
nil
  • can be used with map or filter
user=> (map :last-name @data)
("Dow1" "Dow2" "Dow3" "Dow4" "Dow5")
  • :key cannot be nil
user=> (nil (first @data))
CompilerException java.lang.IllegalArgumentException: Can't call nil, form: (nil (first (clojure.core/deref data))),

compiling:(/private/var/folders/nr/g50ld9t91c555dzv91n43bg40000gn/T/form-init5403593628725666667.clj:1:1)

(map :key)

  • better when :key is nil
user=> ((first @data) nil)
nil


来源:https://stackoverflow.com/questions/42643091/how-to-filter-vector-of-maps-by-multiple-keys-in-clojure

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!