diff --git a/CHANGES.md b/CHANGES.md index dedfd38..5bd9ad7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## 1.1.4 +* Add SORTED, sorted, and sorted-by navs (thanks @IGJoshua) * Add arglist metadata to navs (thanks @phronmophobic) * Improve before-index performance by 150x on lists and 5x on vectors (thanks @jeff303) * Bug fix: BEFORE-ELEM, AFTER-ELEM, FIRST, LAST, BEGINNING, and END on subvecs now produce vector type in cljs diff --git a/src/clj/com/rpl/specter.cljc b/src/clj/com/rpl/specter.cljc index e031ed3..45e6c5e 100644 --- a/src/clj/com/rpl/specter.cljc +++ b/src/clj/com/rpl/specter.cljc @@ -1504,3 +1504,61 @@ [& path] (map compact* path) )) + +(defnav + ^{:doc "Navigates to a sequence resulting from (sort ...), but is a + view to the original structure that can be transformed. + + If the transformed sequence is smaller than the input sequence, values + which are included are sorted by the same indices as the input value's + index in the input sequence. + + If the transformed sequence is larger than the input sequence, values + added to the end of the sequence will be appended to the end of the + original sequence."} + SORTED + [] + (select* [this structure next-fn] + (n/sorted-select structure identity compare next-fn)) + (transform* [this structure next-fn] + (n/sorted-transform structure identity compare next-fn))) + +(defnav + ^{:doc "Navigates to a sequence resulting from (sort comparator ...), but + is a view to the original structure that can be transformed. + + If the transformed sequence is smaller than the input sequence, values + which are included are sorted by the same indices as the input value's + index in the input sequence. + + If the transformed sequence is larger than the input sequence, values + added to the end of the sequence will be appended to the end of the + original sequence."} + sorted + [comparator] + (select* [this structure next-fn] + (n/sorted-select structure identity comparator next-fn)) + (transform* [this structure next-fn] + (n/sorted-transform structure identity comparator next-fn))) + +(defdynamicnav sorted-by + "Navigates to a sequence sorted by the value stored in the keypath, by the + comparator, if one is provided. + + This sequence is a view to the original structure that can be transformed. If + the transformed sequence is smaller than the input sequence, values which are + included are sorted by the same indices as the input value's index in the + input sequence. + + If the transformed sequence is larger than the input sequence, values added to + the end of the sequence will be appended to the end of the original sequence. + + Value collection (e.g. collect, collect-one) may not be used in the keypath." + ([keypath] (sorted-by keypath compare)) + ([keypath comparator] + (late-bound-nav [late (late-path keypath) + late-fn comparator] + (select* [this structure next-fn] + (n/sorted-select structure #(compiled-select-one! late %) late-fn next-fn)) + (transform* [this structure next-fn] + (n/sorted-transform structure #(compiled-select-one! late %) late-fn next-fn))))) diff --git a/src/clj/com/rpl/specter/impl.cljc b/src/clj/com/rpl/specter/impl.cljc index 9a92b2c..a5378f9 100644 --- a/src/clj/com/rpl/specter/impl.cljc +++ b/src/clj/com/rpl/specter/impl.cljc @@ -570,6 +570,18 @@ res )))) +(defn sorted-transform* + [structure keyfn comparator next-fn] + (let [sorted (sort-by (comp keyfn second) comparator (map-indexed vector structure)) + indices (map first sorted) + result (next-fn (map second sorted)) + unsorted (sort-by first compare (map vector indices result))] + (if (seq? structure) + (doall (map second unsorted)) + (into (empty structure) + (map second) + unsorted)))) + (defn- matching-indices [aseq p] (keep-indexed (fn [i e] (if (p e) i)) aseq)) diff --git a/src/clj/com/rpl/specter/navs.cljc b/src/clj/com/rpl/specter/navs.cljc index 3cd1084..b87b7e2 100644 --- a/src/clj/com/rpl/specter/navs.cljc +++ b/src/clj/com/rpl/specter/navs.cljc @@ -401,6 +401,11 @@ (def srange-transform i/srange-transform*) +(defn sorted-select + [structure keyfn comparator next-fn] + (next-fn (sort-by keyfn comparator structure))) + +(def sorted-transform i/sorted-transform*) (defn extract-basic-filter-fn [path] (cond (fn? path) diff --git a/test/com/rpl/specter/core_test.cljc b/test/com/rpl/specter/core_test.cljc index 8323b4f..8dc3434 100644 --- a/test/com/rpl/specter/core_test.cljc +++ b/test/com/rpl/specter/core_test.cljc @@ -1,6 +1,6 @@ (ns com.rpl.specter.core-test #?(:cljs (:require-macros - [cljs.test :refer [is deftest]] + [cljs.test :refer [is deftest testing]] [clojure.test.check.clojure-test :refer [defspec]] [com.rpl.specter.cljs-test-helpers :refer [for-all+]] [com.rpl.specter.test-helpers :refer [ic-test]] @@ -13,7 +13,7 @@ defdynamicnav traverse-all satisfies-protpath? end-fn vtransform]])) (:use - #?(:clj [clojure.test :only [deftest is]]) + #?(:clj [clojure.test :only [deftest is testing]]) #?(:clj [clojure.test.check.clojure-test :only [defspec]]) #?(:clj [com.rpl.specter.test-helpers :only [for-all+ ic-test]]) #?(:clj [com.rpl.specter @@ -1711,3 +1711,22 @@ (is (satisfies-protpath? FooPP "a")) (is (not (satisfies-protpath? FooPP 1))) ))) + +(deftest sorted-test + (let [initial-list [3 4 2 1]] + (testing "the SORTED navigator" + (is (= (sort initial-list) (select-one s/SORTED initial-list))) + (is (= [2 1 3 4] (transform s/SORTED reverse initial-list))) + (is (= [3 2 1] (transform s/SORTED butlast initial-list))) + (is (= [3 5 2 1] (setval [s/SORTED s/LAST] 5 initial-list))) + (is (= (list 1 2 3 4 5) (transform [s/SORTED s/ALL] inc (range 5))))) + (testing "the sorted navigator with comparator" + (let [reverse-comparator (comp - compare)] + (is (= (sort reverse-comparator initial-list) + (select-one (s/sorted reverse-comparator) initial-list))) + (is (= 4 (select-one [(s/sorted reverse-comparator) s/FIRST] initial-list)))))) + (testing "the sorted-by navigator with keypath" + (let [initial-list [{:a 3} {:a 4} {:a 2} {:a 1}]] + (is (= (sort-by :a initial-list) + (select-one (s/sorted-by :a) initial-list))) + (is (= {:a 4} (select-one [(s/sorted-by :a (comp - compare)) s/FIRST] initial-list))))))