diff --git a/CHANGES.md b/CHANGES.md index 4473e85..2fd79d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * `select-first` and `select-any` now avoid traversal beyond the first value matched by the path (like when using `ALL`), so they are faster now for those use cases. * Add `NAME` and `NAMESPACE` navigators * Extend `srange`, `BEGINNING`, `END`, `FIRST`, and `LAST` on strings to navigate to substrings +* Add `BEFORE-ELEM`, `AFTER-ELEM`, and `NONE-ELEM` for efficiently adding a single element to a sequence or set * Improved `ALL` performance for PersistentHashSet * Dynamic navs automatically compile sequence returns if completely static * Eliminate reflection warnings for clj (thanks @mpenet) diff --git a/scripts/benchmarks.clj b/scripts/benchmarks.clj index db861b5..30b95bd 100644 --- a/scripts/benchmarks.clj +++ b/scripts/benchmarks.clj @@ -199,9 +199,10 @@ (let [v (vec (range 1000))] - (run-benchmark "END on large vector" + (run-benchmark "Append to a large vector" 2000000 (setval END [1] v) + (setval AFTER-ELEM 1 v) (reduce conj v [1]) (conj v 1))) diff --git a/src/clj/com/rpl/specter.cljc b/src/clj/com/rpl/specter.cljc index dc23ae6..84ce781 100644 --- a/src/clj/com/rpl/specter.cljc +++ b/src/clj/com/rpl/specter.cljc @@ -726,6 +726,51 @@ (let [to-append (next-fn [])] (n/append-all structure to-append))))) +(defnav + ^{:doc "Navigate to 'void' elem in the set. + For transformations - if result is not `NONE`, + then add that value to the set."} + NONE-ELEM + [] + (select* [this structure next-fn] + (next-fn NONE)) + (transform* [this structure next-fn] + (let [newe (next-fn NONE)] + (if (identical? NONE newe) + structure + (conj structure newe) + )))) + +(defnav + ^{:doc "Navigate to 'void' element before the sequence. + For transformations – if result is not `NONE`, + then prepend that value."} + BEFORE-ELEM + [] + (select* [this structure next-fn] + (next-fn NONE)) + (transform* [this structure next-fn] + (let [newe (next-fn NONE)] + (if (identical? NONE newe) + structure + (n/prepend-one structure newe) + )))) + +(defnav + ^{:doc "Navigate to 'void' element after the sequence. + For transformations – if result is not `NONE`, + then append that value."} + AFTER-ELEM + [] + (select* [this structure next-fn] + (next-fn NONE)) + (transform* [this structure next-fn] + (let [newe (next-fn NONE)] + (if (identical? NONE newe) + structure + (n/append-one structure newe) + )))) + (defnav ^{:doc "Navigates to the specified subset (by taking an intersection). In a transform, that subset in the original set is changed to the diff --git a/src/clj/com/rpl/specter/navs.cljc b/src/clj/com/rpl/specter/navs.cljc index 7be0aac..0868ce6 100644 --- a/src/clj/com/rpl/specter/navs.cljc +++ b/src/clj/com/rpl/specter/navs.cljc @@ -344,7 +344,10 @@ (defprotocol AddExtremes (append-all [structure elements]) - (prepend-all [structure elements])) + (prepend-all [structure elements]) + (append-one [structure elem]) + (prepend-one [structure elem]) + ) (extend-protocol AddExtremes nil @@ -352,6 +355,10 @@ elements) (prepend-all [_ elements] elements) + (append-one [_ elem] + (list elem)) + (prepend-one [_ elem] + (list elem)) #?(:clj clojure.lang.PersistentVector :cljs cljs.core/PersistentVector) (append-all [structure elements] @@ -362,13 +369,22 @@ (reduce conj! <> elements) (reduce conj! <> structure) (persistent! <>)))) + (append-one [structure elem] + (conj structure elem)) + (prepend-one [structure elem] + (into [elem] structure)) #?(:clj Object :cljs default) (append-all [structure elements] (concat structure elements)) (prepend-all [structure elements] - (concat elements structure))) + (concat elements structure)) + (append-one [structure elem] + (concat structure [elem])) + (prepend-one [structure elem] + (cons elem structure)) + ) diff --git a/test/com/rpl/specter/core_test.cljc b/test/com/rpl/specter/core_test.cljc index 2fcdb4e..df9ba59 100644 --- a/test/com/rpl/specter/core_test.cljc +++ b/test/com/rpl/specter/core_test.cljc @@ -1410,25 +1410,35 @@ ) (deftest name-namespace-test - (= :a (setval s/NAME "a" :e)) - (= :a/b (setval s/NAME "b" :a/e)) - (= 'a (setval s/NAME "a" 'e)) - (= 'a/b (setval s/NAME "b" 'a/e)) - (= :a/e (setval s/NAMESPACE "a" :e)) - (= :a/e (setval s/NAMESPACE "a" :f/e)) - (= 'a/e (setval s/NAMESPACE "a" 'e)) - (= 'a/e (setval s/NAMESPACE "a" 'f/e)) + (is (= :a (setval s/NAME "a" :e))) + (is (= :a/b (setval s/NAME "b" :a/e))) + (is (= 'a (setval s/NAME "a" 'e))) + (is (= 'a/b (setval s/NAME "b" 'a/e))) + (is (= :a/e (setval s/NAMESPACE "a" :e))) + (is (= :a/e (setval s/NAMESPACE "a" :f/e))) + (is (= 'a/e (setval s/NAMESPACE "a" 'e))) + (is (= 'a/e (setval s/NAMESPACE "a" 'f/e))) ) (deftest string-navigation-test - (= "ad" (setval (s/srange 1 4) "" "abcd")) - (= "bc" (select-any (s/srange 1 4) "abcd")) - (= "ab" (setval s/END "b" "a")) - (= "ba" (setval s/BEGINNING "b" "a")) - (= "" (select-any s/BEGINNING "abc")) - (= "" (select-any s/END "abc")) - (= \a (select-any s/FIRST "abc")) - (= \c (select-any s/LAST "abc")) - (= "qbc" (setval s/FIRST \q "abc")) - (= "abq" (setval s/FIRST "q" "abc")) + (is (= "ad" (setval (s/srange 1 3) "" "abcd"))) + (is (= "bc" (select-any (s/srange 1 3) "abcd"))) + (is (= "ab" (setval s/END "b" "a"))) + (is (= "ba" (setval s/BEGINNING "b" "a"))) + (is (= "" (select-any s/BEGINNING "abc"))) + (is (= "" (select-any s/END "abc"))) + (is (= \a (select-any s/FIRST "abc"))) + (is (= \c (select-any s/LAST "abc"))) + (is (= "qbc" (setval s/FIRST \q "abc"))) + (is (= "abq" (setval s/LAST "q" "abc"))) + ) + +(deftest single-value-none-navigators-test + (is (predand= vector? [1 2 3] (setval s/AFTER-ELEM 3 [1 2]))) + (is (predand= list? '(1 2 3) (setval s/AFTER-ELEM 3 '(1 2)))) + (is (predand= list? '(1) (setval s/AFTER-ELEM 1 nil))) + (is (predand= vector? [3 1 2] (setval s/BEFORE-ELEM 3 [1 2]))) + (is (predand= list? '(3 1 2) (setval s/BEFORE-ELEM 3 '(1 2)))) + (is (predand= list? '(1) (setval s/BEFORE-ELEM 1 nil))) + (is (= #{1 2 3} (setval s/NONE-ELEM 3 #{1 2}))) )