From 0ddebad8518efcd02cde2c4943b09759b0144e7e Mon Sep 17 00:00:00 2001 From: Jeff Evans Date: Fri, 7 Aug 2020 16:23:33 -0500 Subject: [PATCH] Adding new subseq-pred-fn macro to create the new form of predicate function (taking the previous and current item), to preserve backward compatibility (still allowing predicate functions that only take the current item). This macro also takes in a get-truthy-fn as its first argument, which is a function that marks whether that item in the sequence should be included in a subsequence. This is necessary because the predicate function can now be of any arbitrary form, so we cannot make any assumption about how the user intends for that result to be interpreted as a "filter". Adding SubseqsDynamicPredFn, which works the same as SrangeEndFn, to support backward compatibility Adding wrapper to take a predicate on [prev current] and turn it into a predicate also taking the current index as the first param Creating transducer to combine this with the user-supplied predicate function Adding tests for select and transform TODO: figure out how to make predicate function handle an open-ended subsequence (ex: end marker not yet seen) --- DEVELOPER.md | 3 +-- project.clj | 3 ++- src/clj/com/rpl/specter.cljc | 14 ++++++++++- src/clj/com/rpl/specter/impl.cljc | 32 ++++++++++++++++++++++++- test/com/rpl/specter/core_test.cljc | 37 +++++++++++++++++++++++++---- 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 0752ba75..072438bb 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -7,6 +7,5 @@ lein do clean, test # Running ClojureScript tests ``` -lein javac -lein doo node test-build once +lein do clean, javac, test-cljs ``` diff --git a/project.clj b/project.clj index f1d16b4b..9a3e01c9 100644 --- a/project.clj +++ b/project.clj @@ -40,4 +40,5 @@ [["clojars" {:url "https://repo.clojars.org" :sign-releases false}]] - :aliases {"deploy" ["do" "clean," "deploy" "clojars"]}) + :aliases {"deploy" ["do" "clean," "deploy" "clojars"] + "test-cljs" ["do" "doo" "node" "test-build" "once"]}) diff --git a/src/clj/com/rpl/specter.cljc b/src/clj/com/rpl/specter.cljc index e031ed38..3a293953 100644 --- a/src/clj/com/rpl/specter.cljc +++ b/src/clj/com/rpl/specter.cljc @@ -507,6 +507,11 @@ (defmacro end-fn [& args] `(n/->SrangeEndFunction (fn ~@args))) + (defmacro subseq-pred-fn + "Used in conjunction with `continuous-subseqs`. See [[continuous-subseqs]]." + [get-truthy-fn & args] + `(i/->SubseqsDynamicPredFn ~get-truthy-fn (i/wrap-pred-with-index (fn ~@args)))) + )) @@ -800,7 +805,14 @@ (defnav - ^{:doc "Navigates to every continuous subsequence of elements matching `pred`"} + ^{:doc "Navigates to every continuous subsequence of elements matching `pred`. `pred` can be specified one of two + forms. If a regular function (e.g. defined with `fn`), it takes in only the current element as input. If + defined using the special `subseq-pred-fn` macro, it takes in a `get-truthy-fn` as its first parameter, + followed by arguments to a predicate function [`elem` `prev`], followed by the predicate function body. The + `elem` argument to the predicate function is the current element, and the `pred` argument is the value + returned by your predicate on the previous element, so it can be in any structure you choose. `get-truthy-fn` + is a function that should return true from your predicate's return structure if that element should be + included in a subsequence."} continuous-subseqs [pred] (select* [this structure next-fn] diff --git a/src/clj/com/rpl/specter/impl.cljc b/src/clj/com/rpl/specter/impl.cljc index 24932712..cfc12dd1 100644 --- a/src/clj/com/rpl/specter/impl.cljc +++ b/src/clj/com/rpl/specter/impl.cljc @@ -546,8 +546,38 @@ res )))) +(defn wrap-pred-with-index [pred] + (fn [i elem prev] + [(pred elem (first prev)), i])) + +;; adapted from clojure.core$keep_indexed +(defn- subseq-pred-fn-transducer + ([pred-fn] + (fn [rf] + (let [last-val (volatile! nil) idx (volatile! -1)] + (fn + ([] (rf)) ;; init arity + ([result] (rf result)) ;; completion arity + ([result input] ;; reduction arity + (let [last @last-val + i (vswap! idx inc) + curr ((:pred-fn pred-fn) i input last)] + (vreset! last-val curr) + (if (nil? curr) + result + (rf result curr))))))))) + +;; see com.rpl.specter.navs.SrangeEndFunction +(defrecord SubseqsDynamicPredFn [get-truthy-fn pred-fn]) + (defn- matching-indices [aseq p] - (keep-indexed (fn [i e] (if (p e) i)) aseq)) + (if (instance? SubseqsDynamicPredFn p) + ;; use new subseq predicate form (taking current and previous vals) + (let [index-results (into [] (subseq-pred-fn-transducer p) aseq)] + ;; apply the get-truthy-fn to extract the truthy (i.e. include) result + (map last (filter (comp true? (:get-truthy-fn p) first) index-results))) + ;; else use the previous 1-arity predicate + (keep-indexed (fn [i e] (if (p e) i)) aseq))) (defn matching-ranges [aseq p] (first diff --git a/test/com/rpl/specter/core_test.cljc b/test/com/rpl/specter/core_test.cljc index 1ba123cd..8a78d337 100644 --- a/test/com/rpl/specter/core_test.cljc +++ b/test/com/rpl/specter/core_test.cljc @@ -11,7 +11,7 @@ select-any selected-any? collected? traverse multi-transform path dynamicnav recursive-path defdynamicnav traverse-all satisfies-protpath? end-fn - vtransform]])) + subseq-pred-fn vtransform]])) (:use #?(:clj [clojure.test :only [deftest is]]) #?(:clj [clojure.test.check.clojure-test :only [defspec]]) @@ -23,7 +23,7 @@ select-any selected-any? collected? traverse multi-transform path dynamicnav recursive-path defdynamicnav traverse-all satisfies-protpath? end-fn - vtransform]])) + subseq-pred-fn vtransform]])) @@ -949,6 +949,23 @@ (= (setval (s/continuous-subseqs pred) nil aseq) (filter (complement pred) aseq)))) +(defn- make-bounds-pred-fn-vecs [start end] + (s/subseq-pred-fn first [elem prev] + (let [[included last] prev] + (cond + (= start elem) [false start] + (= end elem) [false end] + (= end last) [false elem] + :else [(or (= start last) included) elem])))) + +(defn- make-bounds-pred-fn-maps [start end] + (s/subseq-pred-fn :include [elem prev] + (let [{include :include last :last} prev] + (cond + (= start elem) {:include false :last start} + (= end elem) {:include false :last end} + (= end last) {:include false :last elem} + :else {:include (or (= start last) include) :last elem})))) (deftest continuous-subseqs-test (is (= [1 "ab" 2 3 "c" 4 "def"] @@ -960,7 +977,19 @@ (is (= [[] [2] [4 6]] (select [(s/continuous-subseqs number?) (s/filterer even?)] - [1 "a" "b" 2 3 "c" 4 5 6 "d" "e" "f"])))) + [1 "a" "b" 2 3 "c" 4 5 6 "d" "e" "f"]))) + (is (= [[1 2 3] [8 9]] + (select + [(s/continuous-subseqs (make-bounds-pred-fn-vecs :START :END))] + [:START 1 2 3 :END 5 6 7 :START 8 9 :END]))) + + (is (= [1 2 3 :START-SUM 15 :END-SUM 7 8 9 :START-SUM 21 :END-SUM 12 :START-SUM 27 :END-SUM] + (transform + (s/continuous-subseqs (make-bounds-pred-fn-maps :START-SUM :END-SUM)) + (fn [vals] [(apply + vals)]) + [1 2 3 :START-SUM 4 5 6 :END-SUM 7 8 9 :START-SUM 10 11 :END-SUM 12 :START-SUM 13 14 :END-SUM]))) + ) + @@ -1589,7 +1618,7 @@ (s/comp-paths (s/srange-dynamic (fn [aseq] (long (/ (count aseq) 2))) - (end-fn [aseq s] (if (empty? aseq) 0 (inc s)))) + (s/end-fn [aseq s] (if (empty? aseq) 0 (inc s)))) s/FIRST ))