2
\$\begingroup\$

Assume we have a data-structure like this:

(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 want to filter it by specific keys using regex in predicates, so I ended up with this function which works fine, but I guess code is ugly and duplicated. How can I 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)))))
\$\endgroup\$
4
\$\begingroup\$

First up, notice that you're creating 3 leted names by using exactly the same expression but replacing one key lookup function. This is a prime candidate for deduplication by making a local function.

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

Next we can notice that the remainder is just doing the same "return this if it's not empty" logic with each of the three keys in sequence. some returns the first non-falsy result of a function on a sequence so by adding a seq to the seek function we can turn empty filter sequences into nils we can combine the two and get the logic we want. I used letfn this time because we're only leting fns, might as well.

(defn my-filter [str-input]
  (letfn[(seek [k]
               (seq (filter #(re-find (->> (str str-input)
                                         (upper-case)
                                         (re-pattern))
                                    (upper-case (k %)))
                     @data)))]
    (some seek [:first-name :last-name :age])))
\$\endgroup\$
  • 2
    \$\begingroup\$ Should be seq, not not-empty. seq is for general sequence handling; not-empty is only needed for non-sequence collection types where you need to preserve the original input's type while also testing it for emptiness. Also, comp improves things here: (some (comp seq seek [...])) But better still, move that seq call into seek: (fn [k] (seq (filter ...))) ... (some seek [...]) \$\endgroup\$ – amalloy Mar 15 '17 at 20:58
0
\$\begingroup\$

I reworked the code a bit to something I thought was easier to parse mentally. I also printed out some intermediate values to show the processing chain in action:

(ns tst.clj.core
  (:use clojure.test tupelo.test)
  (:require
    [tupelo.core :as t]
    [clojure.string :as str]
    [clojure.pprint :refer [pprint]]
  ))
(t/refer-tupelo)
(t/print-versions)

(def data
  [{:id 1 :first-name "John"        :last-name "Dog"            :age 14}
   {:id 2 :first-name "Johnny"      :last-name "Cat"            :age 54}
   {:id 3 :first-name "Joe"         :last-name "Doggy"          :age 34}
   {:id 4 :first-name "Jerry"       :last-name "Gyrfalcon"      :age 12}
   {:id 5 :first-name "Jenkins"     :last-name "TheButler"      :age 24}])

(defn case-insensitive-match [pat-str search-str]
  (re-find (re-pattern (str "(?i)" pat-str)) search-str)) ; pattern like #"(?i)xyz" => case-insensitive search

(defn find-rec [rec-list pat-str field-kw]
  (filter #(case-insensitive-match pat-str (str (get % field-kw))) rec-list))

(defn my-filter [rec-list strpat]
  (let [firstname-matches (find-rec rec-list strpat :first-name)
        lastname-matches  (find-rec rec-list strpat :last-name)
        age-matches       (find-rec rec-list strpat :age)]
    (println "-----------------------------------------------------------------------------")
    (spyx firstname-matches)
    (spyx lastname-matches)
    (spyx age-matches)
    (first (remove empty? [firstname-matches lastname-matches age-matches]))))

(newline) (pprint [:john (my-filter data "john")])
(newline) (pprint [:jo (my-filter data "jo")])
(newline) (pprint [:y (my-filter data "y")])
(newline) (pprint [:og (my-filter data "og")])
(newline) (pprint [:3 (my-filter data "3")])
(newline) (pprint [:7 (my-filter data "7")])

One thing I don't understand is searching by age and treating it like a string instead of a number. Note that str/upper-case silently converts a number like 42 into a string "42". Here I forced it to be explicit.

I also used the regular expression pattern like #"(?i)abc" which does a case-insensitive search for "abc".

The results are:

-----------------------------------------------------------------------------
firstname-matches => ({:id 1, :first-name "John", :last-name "Dog", :age 14} {:id 2, :first-name "Johnny", :last-name "Cat", :age 54})
lastname-matches => ()
age-matches => ()
[:john
 ({:id 1, :first-name "John", :last-name "Dog", :age 14}
  {:id 2, :first-name "Johnny", :last-name "Cat", :age 54})]

-----------------------------------------------------------------------------
firstname-matches => ({:id 1, :first-name "John", :last-name "Dog", :age 14} {:id 2, :first-name "Johnny", :last-name "Cat", :age 54} {:id 3, :first-name "Joe", :last-name "Doggy", :age 34})
lastname-matches => ()
age-matches => ()
[:jo
 ({:id 1, :first-name "John", :last-name "Dog", :age 14}
  {:id 2, :first-name "Johnny", :last-name "Cat", :age 54}
  {:id 3, :first-name "Joe", :last-name "Doggy", :age 34})]

-----------------------------------------------------------------------------
firstname-matches => ({:id 2, :first-name "Johnny", :last-name "Cat", :age 54} {:id 4, :first-name "Jerry", :last-name "Gyrfalcon", :age 12})
lastname-matches => ({:id 3, :first-name "Joe", :last-name "Doggy", :age 34} {:id 4, :first-name "Jerry", :last-name "Gyrfalcon", :age 12})
age-matches => ()
[:y
 ({:id 2, :first-name "Johnny", :last-name "Cat", :age 54}
  {:id 4, :first-name "Jerry", :last-name "Gyrfalcon", :age 12})]

-----------------------------------------------------------------------------
firstname-matches => ()
lastname-matches => ({:id 1, :first-name "John", :last-name "Dog", :age 14} {:id 3, :first-name "Joe", :last-name "Doggy", :age 34})
age-matches => ()
[:og
 ({:id 1, :first-name "John", :last-name "Dog", :age 14}
  {:id 3, :first-name "Joe", :last-name "Doggy", :age 34})]

-----------------------------------------------------------------------------
firstname-matches => ()
lastname-matches => ()
age-matches => ({:id 3, :first-name "Joe", :last-name "Doggy", :age 34})
[:3 ({:id 3, :first-name "Joe", :last-name "Doggy", :age 34})]

-----------------------------------------------------------------------------
firstname-matches => ()
lastname-matches => ()
age-matches => ()
[:7 nil]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.