;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns common-tests.data-test (:require [app.common.data :as d] [clojure.test :as t])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Basic Predicates ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest boolean-or-nil-predicate (t/is (d/boolean-or-nil? nil)) (t/is (d/boolean-or-nil? true)) (t/is (d/boolean-or-nil? false)) (t/is (not (d/boolean-or-nil? 0))) (t/is (not (d/boolean-or-nil? ""))) (t/is (not (d/boolean-or-nil? :kw)))) (t/deftest in-range-predicate (t/is (d/in-range? 5 0)) (t/is (d/in-range? 5 4)) (t/is (not (d/in-range? 5 5))) (t/is (not (d/in-range? 5 -1))) (t/is (not (d/in-range? 0 0)))) (t/deftest get-initials-test (t/is (= "JD" (d/get-initials "John Doe"))) (t/is (= "A" (d/get-initials "acme"))) (t/is (= "AB" (d/get-initials "123 Alpha ## beta"))) (t/is (= "PD" (d/get-initials " penpot design tool "))) (t/is (= "" (d/get-initials nil))) (t/is (= "" (d/get-initials "!!! ???")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Ordered Data Structures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest ordered-set-creation (let [s (d/ordered-set)] (t/is (d/ordered-set? s)) (t/is (empty? s))) (let [s (d/ordered-set :a)] (t/is (d/ordered-set? s)) (t/is (contains? s :a))) (let [s (d/ordered-set :a :b :c)] (t/is (d/ordered-set? s)) (t/is (= (seq s) [:a :b :c])))) (t/deftest ordered-set-preserves-order (let [s (d/ordered-set :c :a :b)] (t/is (= (seq s) [:c :a :b]))) ;; Duplicates are ignored; order of first insertion is kept (let [s (-> (d/ordered-set) (conj :a) (conj :b) (conj :a))] (t/is (= (seq s) [:a :b])))) (t/deftest ordered-map-creation (let [m (d/ordered-map)] (t/is (d/ordered-map? m)) (t/is (empty? m))) (let [m (d/ordered-map :a 1)] (t/is (d/ordered-map? m)) (t/is (= (get m :a) 1))) (let [m (d/ordered-map :a 1 :b 2)] (t/is (d/ordered-map? m)) (t/is (= (keys m) [:a :b])))) (t/deftest ordered-map-preserves-insertion-order (let [m (-> (d/ordered-map) (assoc :c 3) (assoc :a 1) (assoc :b 2))] (t/is (= (keys m) [:c :a :b])))) (t/deftest oassoc-test ;; oassoc on nil creates a new ordered-map (let [m (d/oassoc nil :a 1 :b 2)] (t/is (d/ordered-map? m)) (t/is (= (get m :a) 1)) (t/is (= (get m :b) 2))) ;; oassoc on existing ordered-map updates it (let [m (d/oassoc (d/ordered-map :x 10) :y 20)] (t/is (= (get m :x) 10)) (t/is (= (get m :y) 20)))) (t/deftest oassoc-in-test (let [m (d/oassoc-in nil [:a :b] 42)] (t/is (d/ordered-map? m)) (t/is (= (get-in m [:a :b]) 42))) (let [m (-> (d/ordered-map) (d/oassoc-in [:x :y] 1) (d/oassoc-in [:x :z] 2))] (t/is (= (get-in m [:x :y]) 1)) (t/is (= (get-in m [:x :z]) 2)))) (t/deftest oupdate-in-test (let [m (-> (d/ordered-map) (d/oassoc-in [:a :b] 10) (d/oupdate-in [:a :b] + 5))] (t/is (= (get-in m [:a :b]) 15)))) (t/deftest oassoc-before-test (let [m (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3)) m2 (d/oassoc-before m :b :x 99)] ;; :x should be inserted just before :b (t/is (= (keys m2) [:a :x :b :c])) (t/is (= (get m2 :x) 99))) ;; When before-k does not exist, assoc at the end (let [m (-> (d/ordered-map) (assoc :a 1)) m2 (d/oassoc-before m :z :x 99)] (t/is (= (get m2 :x) 99)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Ordered Set / Map Index Helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest adds-at-index-test (let [s (d/ordered-set :a :b :c) s2 (d/adds-at-index s 1 :x)] (t/is (= (seq s2) [:a :x :b :c]))) (let [s (d/ordered-set :a :b :c) s2 (d/adds-at-index s 0 :x)] (t/is (= (seq s2) [:x :a :b :c]))) (let [s (d/ordered-set :a :b :c) s2 (d/adds-at-index s 3 :x)] (t/is (= (seq s2) [:a :b :c :x])))) (t/deftest inserts-at-index-test (let [s (d/ordered-set :a :b :c) s2 (d/inserts-at-index s 1 [:x :y])] (t/is (= (seq s2) [:a :x :y :b :c]))) (let [s (d/ordered-set :a :b :c) s2 (d/inserts-at-index s 0 [:x])] (t/is (= (seq s2) [:x :a :b :c])))) (t/deftest addm-at-index-test (let [m (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3)) m2 (d/addm-at-index m 1 :x 99)] (t/is (= (keys m2) [:a :x :b :c])) (t/is (= (get m2 :x) 99)))) (t/deftest insertm-at-index-test (let [m (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3)) m2 (d/insertm-at-index m 1 (d/ordered-map :x 10 :y 20))] (t/is (= (keys m2) [:a :x :y :b :c])))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Concat / remove helpers (pre-existing tests preserved) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest concat-vec (t/is (= [] (d/concat-vec))) (t/is (= [1] (d/concat-vec [1]))) (t/is (= [1] (d/concat-vec #{1}))) (t/is (= [1 2] (d/concat-vec [1] #{2}))) (t/is (= [1 2] (d/concat-vec '(1) [2])))) (t/deftest concat-set (t/is (= #{} (d/concat-set))) (t/is (= #{1 2} (d/concat-set [1] [2])))) (t/deftest remove-at-index (t/is (= [1 2 3 4] (d/remove-at-index [1 2 3 4 5] 4))) (t/is (= [1 2 3 4] (d/remove-at-index [5 1 2 3 4] 0))) (t/is (= [1 2 3 4] (d/remove-at-index [1 5 2 3 4] 1)))) (t/deftest with-next (t/is (= [[0 1] [1 2] [2 3] [3 4] [4 nil]] (d/with-next (range 5))))) (t/deftest with-prev (t/is (= [[0 nil] [1 0] [2 1] [3 2] [4 3]] (d/with-prev (range 5))))) (t/deftest with-prev-next (t/is (= [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]] (d/with-prev-next (range 5))))) (t/deftest join (t/is (= [[1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b]] (d/join [1 2 3] [:a :b]))) (t/is (= [1 10 100 2 20 200 3 30 300] (d/join [1 2 3] [1 10 100] *)))) (t/deftest num-predicate (t/is (not (d/num? ##NaN))) (t/is (not (d/num? nil))) (t/is (d/num? 1)) (t/is (d/num? -0.3)) (t/is (not (d/num? {})))) (t/deftest check-num-helper (t/is (= 1 (d/check-num 1 0))) (t/is (= 0 (d/check-num ##NaN 0))) (t/is (= 0 (d/check-num {} 0))) (t/is (= 0 (d/check-num [] 0))) (t/is (= 0 (d/check-num :foo 0))) (t/is (= 0 (d/check-num nil 0)))) (t/deftest insert-at-index ;; insert different object (t/is (= (d/insert-at-index [:a :b] 1 [:c :d]) [:a :c :d :b])) ;; insert on the start (t/is (= (d/insert-at-index [:a :b] 0 [:c]) [:c :a :b])) ;; insert on the end 1 (t/is (= (d/insert-at-index [:a :b] 2 [:c]) [:a :b :c])) ;; insert on the end with not existing index (t/is (= (d/insert-at-index [:a :b] 10 [:c]) [:a :b :c])) ;; insert existing in a contiguous index (t/is (= (d/insert-at-index [:a :b] 1 [:a]) [:a :b])) ;; insert existing in the same index (t/is (= (d/insert-at-index [:a :b] 0 [:a]) [:a :b])) ;; insert existing in other index case 1 (t/is (= (d/insert-at-index [:a :b :c] 2 [:a]) [:b :a :c])) ;; insert existing in other index case 2 (t/is (= (d/insert-at-index [:a :b :c :d] 0 [:d]) [:d :a :b :c])) ;; insert existing in other index case 3 (t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a]) [:a :b :c :d]))) (t/deftest reorder (let [v ["a" "b" "c" "d"]] (t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"])) (t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"])) (t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"])) (t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"])) (t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"])) (t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"])) (t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"])))) (t/deftest nth-last-index-of-test (t/is (= (d/nth-last-index-of "" "*" 1) nil)) (t/is (= (d/nth-last-index-of "*abc" "*" 1) 0)) (t/is (= (d/nth-last-index-of "**abc" "*" 2) 0)) (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 3) nil)) (t/is (= (d/nth-last-index-of "" "*" 2) nil)) (t/is (= (d/nth-last-index-of "abc*" "*" 1) 3)) (t/is (= (d/nth-last-index-of "abc*" "*" 2) nil)) (t/is (= (d/nth-last-index-of "*abc[*" "*" 1) 5)) (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 1) 7)) (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 2) 3))) (t/deftest nth-index-of-test (t/is (= (d/nth-index-of "" "*" 1) nil)) (t/is (= (d/nth-index-of "" "*" 2) nil)) (t/is (= (d/nth-index-of "abc*" "*" 1) 3)) (t/is (= (d/nth-index-of "abc*" "*" 1) 3)) (t/is (= (d/nth-index-of "abc**" "*" 2) 4)) (t/is (= (d/nth-index-of "abc*" "*" 2) nil)) (t/is (= (d/nth-index-of "*abc[*" "*" 1) 0)) (t/is (= (d/nth-index-of "abc*def*ghi" "*" 1) 3)) (t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7)) (t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil))) (t/deftest natural-sort-by-test (t/is (= (d/natural-sort-by identity ["10" "2" "1" "11" "3" "30"]) ["1" "2" "3" "10" "11" "30"])) (t/is (= (d/natural-sort-by identity ["banana" "apple" "cherry"]) ["apple" "banana" "cherry"])) (t/is (= (d/natural-sort-by identity ["size10" "size2" "size1" "size20" "size3"]) ["size1" "size2" "size3" "size10" "size20"])) (t/is (= (d/natural-sort-by identity ["b1" "a2" "a10" "a1"]) ["a1" "a2" "a10" "b1"])) (t/is (= (d/natural-sort-by identity []) [])) (t/is (= (d/natural-sort-by identity ["solo"]) ["solo"])) (t/is (= (d/natural-sort-by identity ["b" "a" "a" "c"]) ["a" "a" "b" "c"])) (t/is (= (d/natural-sort-by :name [{:name "big"} {:name "small"} {:name "medium"}]) [{:name "big"} {:name "medium"} {:name "small"}])) (t/is (= (d/natural-sort-by :name [{:name "size10"} {:name "size2"} {:name "size1"}]) [{:name "size1"} {:name "size2"} {:name "size10"}])) (t/is (= (d/natural-sort-by :name [{:name "border-radius-10"} {:name "border-radius-2"} {:name "border-radius-1"}]) [{:name "border-radius-1"} {:name "border-radius-2"} {:name "border-radius-10"}])) (t/is (= (d/natural-sort-by :name [{:name "border-10-radius"} {:name "border-2-radius"} {:name "border-1-radius"}]) [{:name "border-1-radius"} {:name "border-2-radius"} {:name "border-10-radius"}])) (t/is (= (d/natural-sort-by :name [{:name "border-10-radius"} {:name "border-2-extra"} {:name "border-2-radius"} {:name "border-1-radius"}]) [{:name "border-1-radius"} {:name "border-2-extra"} {:name "border-2-radius"} {:name "border-10-radius"}]))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Lazy / sequence helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest concat-all-test (t/is (= [1 2 3 4 5 6] (d/concat-all [[1 2] [3 4] [5 6]]))) (t/is (= [] (d/concat-all []))) (t/is (= [1 2 3] (d/concat-all [[1] [2] [3]]))) ;; It's lazy — works with infinite-ish inner seqs truncated by outer limit (t/is (= [0 1 2] (take 3 (d/concat-all (map list (range))))))) (t/deftest mapcat-test (t/is (= [0 1 1 2 2 3] (d/mapcat (fn [x] [x (inc x)]) [0 1 2]))) ;; fully lazy — can operate on infinite sequences (t/is (= [0 0 1 1 2 2] (take 6 (d/mapcat (fn [x] [x x]) (range)))))) (t/deftest zip-test (t/is (= [[1 :a] [2 :b] [3 :c]] (d/zip [1 2 3] [:a :b :c]))) (t/is (= [] (d/zip [] [])))) (t/deftest zip-all-test ;; same length (t/is (= [[1 :a] [2 :b]] (d/zip-all [1 2] [:a :b]))) ;; col1 longer — col2 padded with nils (t/is (= [[1 :a] [2 nil] [3 nil]] (d/zip-all [1 2 3] [:a]))) ;; col2 longer — col1 padded with nils (t/is (= [[1 :a] [nil :b] [nil :c]] (d/zip-all [1] [:a :b :c])))) (t/deftest enumerate-test (t/is (= [[0 :a] [1 :b] [2 :c]] (d/enumerate [:a :b :c]))) (t/is (= [[5 :a] [6 :b]] (d/enumerate [:a :b] 5))) (t/is (= [] (d/enumerate [])))) (t/deftest interleave-all-test (t/is (= [] (d/interleave-all))) (t/is (= [1 2 3] (d/interleave-all [1 2 3]))) (t/is (= [1 :a 2 :b 3 :c] (d/interleave-all [1 2 3] [:a :b :c]))) ;; unequal lengths — longer seq is not truncated (t/is (= [1 :a 2 :b 3] (d/interleave-all [1 2 3] [:a :b]))) (t/is (= [1 :a 2 :b :c] (d/interleave-all [1 2] [:a :b :c])))) (t/deftest add-at-index-test (t/is (= [:a :x :b :c] (d/add-at-index [:a :b :c] 1 :x))) (t/is (= [:x :a :b :c] (d/add-at-index [:a :b :c] 0 :x))) (t/is (= [:a :b :c :x] (d/add-at-index [:a :b :c] 3 :x)))) (t/deftest take-until-test ;; stops (inclusive) when predicate is true (t/is (= [1 2 3] (d/take-until #(= % 3) [1 2 3 4 5]))) ;; if predicate never true, returns whole collection (t/is (= [1 2 3] (d/take-until #(= % 9) [1 2 3]))) ;; first element matches (t/is (= [1] (d/take-until #(= % 1) [1 2 3])))) (t/deftest safe-subvec-test ;; normal range (t/is (= [2 3] (d/safe-subvec [1 2 3 4] 1 3))) ;; single arg — from index to end (t/is (= [2 3 4] (d/safe-subvec [1 2 3 4] 1))) ;; start=0 returns the full vector (t/is (= [1 2 3 4] (d/safe-subvec [1 2 3 4] 0))) ;; out-of-range returns nil (t/is (nil? (d/safe-subvec [1 2 3] 5))) (t/is (nil? (d/safe-subvec [1 2 3] 0 5))) ;; nil v returns nil (t/is (nil? (d/safe-subvec nil 0 1)))) (t/deftest domap-test (let [side-effects (atom []) result (d/domap #(swap! side-effects conj %) [1 2 3])] (t/is (= [1 2 3] result)) (t/is (= [1 2 3] @side-effects))) ;; transducer arity (let [side-effects (atom [])] (into [] (d/domap #(swap! side-effects conj %)) [4 5]) (t/is (= [4 5] @side-effects)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Collection lookup helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest group-by-test (t/is (= {:odd [1 3] :even [2 4]} (d/group-by #(if (odd? %) :odd :even) [1 2 3 4]))) ;; two-arity with value function (t/is (= {:odd [10 30] :even [20 40]} (d/group-by #(if (odd? %) :odd :even) #(* % 10) [1 2 3 4]))) ;; three-arity with initial value (t/is (= {:a #{1} :b #{2}} (d/group-by :k :v #{} [{:k :a :v 1} {:k :b :v 2}])))) (t/deftest seek-test (t/is (= 3 (d/seek odd? [2 4 3 5]))) (t/is (nil? (d/seek odd? [2 4 6]))) (t/is (= :default (d/seek odd? [2 4 6] :default))) (t/is (= 1 (d/seek some? [nil nil 1 2])))) (t/deftest index-by-test (t/is (= {1 {:id 1 :name "a"} 2 {:id 2 :name "b"}} (d/index-by :id [{:id 1 :name "a"} {:id 2 :name "b"}]))) ;; two-arity with value fn (t/is (= {1 "a" 2 "b"} (d/index-by :id :name [{:id 1 :name "a"} {:id 2 :name "b"}])))) (t/deftest index-of-pred-test (t/is (= 0 (d/index-of-pred [1 2 3] odd?))) (t/is (= 1 (d/index-of-pred [2 3 4] odd?))) (t/is (nil? (d/index-of-pred [2 4 6] odd?))) (t/is (nil? (d/index-of-pred [] odd?))) ;; works correctly when collection contains nil elements (t/is (= 2 (d/index-of-pred [nil nil 3] some?))) (t/is (= 0 (d/index-of-pred [nil 1 2] nil?))) ;; works correctly when collection contains false elements (t/is (= 1 (d/index-of-pred [false true false] true?)))) (t/deftest index-of-test (t/is (= 0 (d/index-of [:a :b :c] :a))) (t/is (= 2 (d/index-of [:a :b :c] :c))) (t/is (nil? (d/index-of [:a :b :c] :z))) ;; works when searching for nil in a collection (t/is (= 1 (d/index-of [:a nil :c] nil)))) (t/deftest replace-by-id-test (let [items [{:id 1 :v "a"} {:id 2 :v "b"} {:id 3 :v "c"}] new-v {:id 2 :v "x"}] (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}] (d/replace-by-id items new-v))) ;; transducer arity (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}] (sequence (d/replace-by-id new-v) items))))) (t/deftest getf-test (let [m {:a 1 :b 2} get-from-m (d/getf m)] (t/is (= 1 (get-from-m :a))) (t/is (= 2 (get-from-m :b))) (t/is (nil? (get-from-m :z))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Map manipulation helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest vec-without-nils-test (t/is (= [1 2 3] (d/vec-without-nils [1 nil 2 nil 3]))) (t/is (= [] (d/vec-without-nils [nil nil]))) (t/is (= [1] (d/vec-without-nils [1])))) (t/deftest without-nils-test (t/is (= {:a 1 :d 2} (d/without-nils {:a 1 :b nil :c nil :d 2 :e nil})) "removes all nil values") ;; transducer arity — works on map entries (t/is (= {:a 1} (into {} (d/without-nils) {:a 1 :b nil})))) (t/deftest without-qualified-test (t/is (= {:a 1} (d/without-qualified {:a 1 :ns/b 2 :ns/c 3}))) ;; transducer arity — works on map entries (t/is (= {:a 1} (into {} (d/without-qualified) {:a 1 :ns/b 2})))) (t/deftest without-keys-test (t/is (= {:c 3} (d/without-keys {:a 1 :b 2 :c 3} [:a :b]))) (t/is (= {:a 1 :b 2 :c 3} (d/without-keys {:a 1 :b 2 :c 3} [])))) (t/deftest deep-merge-test (t/is (= {:a 1 :b {:c 3 :d 4}} (d/deep-merge {:a 1 :b {:c 2 :d 4}} {:b {:c 3}}))) ;; non-map values get replaced (t/is (= {:a 2} (d/deep-merge {:a 1} {:a 2}))) ;; three-way merge (t/is (= {:a 1 :b 2 :c 3} (d/deep-merge {:a 1} {:b 2} {:c 3})))) (t/deftest dissoc-in-test (t/is (= {:a {:b 1}} (d/dissoc-in {:a {:b 1 :c 2}} [:a :c]))) ;; removes parent when child map becomes empty (t/is (= {} (d/dissoc-in {:a {:b 1}} [:a :b]))) ;; no-op when path does not exist (t/is (= {:a 1} (d/dissoc-in {:a 1} [:b :c])))) (t/deftest patch-object-test ;; normal update (t/is (= {:a 2 :b 2} (d/patch-object {:a 1 :b 2} {:a 2}))) ;; nil value removes key (t/is (= {:b 2} (d/patch-object {:a 1 :b 2} {:a nil}))) ;; nested map is merged recursively (t/is (= {:a {:x 10 :y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x 10}}))) ;; nested nil removes nested key (t/is (= {:a {:y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x nil}}))) ;; nil value removes only the specified key, not other keys (t/is (= {nil 0 :b 2} (d/patch-object {nil 0 :a 1 :b 2} {:a nil}))) ;; transducer arity (1-arg returns a fn) (let [f (d/patch-object {:a 99})] (t/is (= {:a 99 :b 2} (f {:a 1 :b 2}))))) (t/deftest without-obj-test (t/is (= [1 3] (d/without-obj [1 2 3] 2))) (t/is (= [1 2 3] (d/without-obj [1 2 3] 9))) (t/is (= [] (d/without-obj [1] 1)))) (t/deftest update-vals-test (t/is (= {:a 2 :b 4} (d/update-vals {:a 1 :b 2} #(* % 2)))) (t/is (= {} (d/update-vals {} identity)))) (t/deftest update-in-when-test ;; key exists — applies function (t/is (= {:a {:b 2}} (d/update-in-when {:a {:b 1}} [:a :b] inc))) ;; key absent — returns unchanged (t/is (= {:a 1} (d/update-in-when {:a 1} [:b :c] inc)))) (t/deftest update-when-test ;; key exists — applies function (t/is (= {:a 2} (d/update-when {:a 1} :a inc))) ;; key absent — returns unchanged (t/is (= {:a 1} (d/update-when {:a 1} :b inc)))) (t/deftest assoc-in-when-test ;; key exists — updates value (t/is (= {:a {:b 99}} (d/assoc-in-when {:a {:b 1}} [:a :b] 99))) ;; key absent — returns unchanged (t/is (= {:a 1} (d/assoc-in-when {:a 1} [:b :c] 99)))) (t/deftest assoc-when-test ;; key exists — updates value (t/is (= {:a 99} (d/assoc-when {:a 1} :a 99))) ;; key absent — returns unchanged (t/is (= {:a 1} (d/assoc-when {:a 1} :b 99)))) (t/deftest merge-test (t/is (= {:a 1 :b 2 :c 3} (d/merge {:a 1} {:b 2} {:c 3}))) (t/is (= {:a 2} (d/merge {:a 1} {:a 2}))) (t/is (= {} (d/merge)))) (t/deftest txt-merge-test ;; sets value when not nil (t/is (= {:a 2 :b 2} (d/txt-merge {:a 1 :b 2} {:a 2}))) ;; removes key when value is nil (t/is (= {:b 2} (d/txt-merge {:a 1 :b 2} {:a nil}))) ;; adds new key (t/is (= {:a 1 :b 2 :c 3} (d/txt-merge {:a 1 :b 2} {:c 3})))) (t/deftest mapm-test ;; two-arity: transform map in place (t/is (= {:a 2 :b 4} (d/mapm (fn [k v] (* v 2)) {:a 1 :b 2}))) ;; one-arity: transducer (t/is (= {:a 10 :b 20} (into {} (d/mapm (fn [k v] (* v 10))) {:a 1 :b 2})))) (t/deftest removev-test (t/is (= [2 4] (d/removev odd? [1 2 3 4]))) (t/is (= [nil nil] (d/removev some? [nil nil]))) (t/is (= [1 2 3] (d/removev nil? [1 nil 2 nil 3])))) (t/deftest filterm-test (t/is (= {:a 1 :c 3} (d/filterm (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4})) "keeps entries where value is odd") (t/is (= {} (d/filterm (fn [[_ v]] (> v 10)) {:a 1 :b 2})))) (t/deftest removem-test (t/is (= {:b 2 :d 4} (d/removem (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4}))) (t/is (= {:a 1 :b 2} (d/removem (fn [[_ v]] (> v 10)) {:a 1 :b 2})))) (t/deftest map-perm-test ;; default: all pairs (t/is (= [[1 2] [1 3] [1 4] [2 3] [2 4] [3 4]] (d/map-perm vector [1 2 3 4]))) ;; with predicate (t/is (= [[1 3]] (d/map-perm vector (fn [a b] (and (odd? a) (odd? b))) [1 2 3]))) ;; empty collection (t/is (= [] (d/map-perm vector [])))) (t/deftest distinct-xf-test (t/is (= [1 2 3] (into [] (d/distinct-xf identity) [1 2 1 3 2]))) ;; keeps the first occurrence for each key (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"}] (into [] (d/distinct-xf :id) [{:id 1 :v "a"} {:id 2 :v "x"} {:id 2 :v "b"}])))) (t/deftest deep-mapm-test ;; mfn is applied once per entry (t/is (= {:a 2 :b {:c 4}} (d/deep-mapm (fn [[k v]] [k (if (number? v) (* v 2) v)]) {:a 1 :b {:c 2}}))) ;; Keyword renaming: keys are transformed once per entry (let [result (d/deep-mapm (fn [[k v]] [(keyword (str (name k) "!")) v]) {:a 1})] (t/is (contains? result (keyword "a!")))) ;; Vectors inside maps are recursed into (t/is (= {:items [{:x 10}]} (d/deep-mapm (fn [[k v]] [k (if (number? v) (* v 10) v)]) {:items [{:x 1}]}))) ;; Plain scalar at top level map (t/is (= {:a "hello"} (d/deep-mapm identity {:a "hello"})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Numeric helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest nan-test (t/is (d/nan? ##NaN)) (t/is (not (d/nan? 0))) (t/is (not (d/nan? 1))) (t/is (not (d/nan? nil))) ;; CLJS js/isNaN coerces non-numbers; JVM Double/isNaN is number-only #?(:cljs (t/is (d/nan? "hello"))) #?(:clj (t/is (not (d/nan? "hello"))))) (t/deftest safe-plus-test (t/is (= 5 (d/safe+ 3 2))) ;; when first arg is not finite, return it unchanged (t/is (= ##Inf (d/safe+ ##Inf 10)))) (t/deftest max-test (t/is (= 3 (d/max 3))) (t/is (= 5 (d/max 3 5))) (t/is (= 9 (d/max 1 9 4))) (t/is (= 10 (d/max 1 2 3 4 5 6 7 8 9 10)))) (t/deftest min-test (t/is (= 3 (d/min 3))) (t/is (= 3 (d/min 3 5))) (t/is (= 1 (d/min 1 9 4))) (t/is (= 1 (d/min 10 9 8 7 6 5 4 3 2 1)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Parsing helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest parse-integer-test (t/is (= 42 (d/parse-integer "42"))) (t/is (= -1 (d/parse-integer "-1"))) (t/is (nil? (d/parse-integer "abc"))) (t/is (= 0 (d/parse-integer "abc" 0))) (t/is (nil? (d/parse-integer nil)))) (t/deftest parse-double-test (t/is (= 3.14 (d/parse-double "3.14"))) (t/is (= -1.0 (d/parse-double "-1.0"))) (t/is (nil? (d/parse-double "abc"))) (t/is (= 0.0 (d/parse-double "abc" 0.0))) (t/is (nil? (d/parse-double nil)))) (t/deftest parse-uuid-test (let [uuid-str "550e8400-e29b-41d4-a716-446655440000"] (t/is (some? (d/parse-uuid uuid-str)))) (t/is (nil? (d/parse-uuid "not-a-uuid"))) (t/is (nil? (d/parse-uuid nil)))) (t/deftest coalesce-str-test (t/is (= "default" (d/coalesce-str nil "default"))) ;; Numbers always stringify on both platforms (t/is (= "42" (d/coalesce-str 42 "default"))) ;; ##NaN returns default on both platforms now that nan? is fixed on JVM (t/is (= "default" (d/coalesce-str ##NaN "default"))) ;; Strings: in CLJS js/isNaN("hello")=true so "default" is returned; ;; in CLJ nan? is false for strings so (str "hello")="hello" is returned. #?(:cljs (t/is (= "default" (d/coalesce-str "hello" "default")))) #?(:clj (t/is (= "hello" (d/coalesce-str "hello" "default"))))) (t/deftest coalesce-test (t/is (= "hello" (d/coalesce "hello" "default"))) (t/is (= "default" (d/coalesce nil "default"))) ;; coalesce uses `or`, so false is falsy and returns the default (t/is (= "default" (d/coalesce false "default"))) (t/is (= 42 (d/coalesce 42 0)))) (t/deftest read-string-test (t/is (= {:a 1} (d/read-string "{:a 1}"))) (t/is (= [1 2 3] (d/read-string "[1 2 3]"))) (t/is (= :keyword (d/read-string ":keyword")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; String / keyword helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest name-test (t/is (= "foo" (d/name :foo))) (t/is (= "foo" (d/name "foo"))) (t/is (nil? (d/name nil))) (t/is (= "42" (d/name 42)))) (t/deftest prefix-keyword-test (t/is (= :prefix-test (d/prefix-keyword "prefix-" :test))) (t/is (= :ns-id (d/prefix-keyword :ns- :id))) (t/is (= :ab (d/prefix-keyword "a" "b")))) (t/deftest kebab-keys-test (t/is (= {:foo-bar 1 :baz-qux 2} (d/kebab-keys {"fooBar" 1 "bazQux" 2}))) (t/is (= {:my-key {:nested-key 1}} (d/kebab-keys {:myKey {:nestedKey 1}})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Utility helpers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest regexp-test (t/is (d/regexp? #"foo")) (t/is (not (d/regexp? "foo"))) (t/is (not (d/regexp? nil)))) (t/deftest nilf-test (let [safe-inc (d/nilf inc)] (t/is (nil? (safe-inc nil))) (t/is (= 2 (safe-inc 1)))) (let [safe-add (d/nilf +)] (t/is (nil? (safe-add 1 nil))) (t/is (= 3 (safe-add 1 2))))) (t/deftest nilv-test (t/is (= "default" (d/nilv nil "default"))) (t/is (= "value" (d/nilv "value" "default"))) (t/is (= false (d/nilv false "default"))) ;; transducer arity (t/is (= ["a" "default" "b"] (into [] (d/nilv "default") ["a" nil "b"])))) (t/deftest any-key-test (t/is (d/any-key? {:a 1 :b 2} :a)) (t/is (d/any-key? {:a 1 :b 2} :z :b)) (t/is (not (d/any-key? {:a 1} :z :x)))) (t/deftest tap-test (let [received (atom nil)] (t/is (= [1 2 3] (d/tap #(reset! received %) [1 2 3]))) (t/is (= [1 2 3] @received)))) (t/deftest tap-r-test (let [received (atom nil)] (t/is (= [1 2 3] (d/tap-r [1 2 3] #(reset! received %)))) (t/is (= [1 2 3] @received)))) (t/deftest map-diff-test ;; identical maps produce empty diff (t/is (= {} (d/map-diff {:a 1} {:a 1}))) ;; changed value (t/is (= {:a [1 2]} (d/map-diff {:a 1} {:a 2}))) ;; removed key (t/is (= {:b [2 nil]} (d/map-diff {:a 1 :b 2} {:a 1}))) ;; added key (t/is (= {:c [nil 3]} (d/map-diff {:a 1} {:a 1 :c 3}))) ;; nested diff (t/is (= {:b {:c [1 2]}} (d/map-diff {:b {:c 1}} {:b {:c 2}})))) (t/deftest unique-name-test ;; name not in used set — returned as-is (t/is (= "foo" (d/unique-name "foo" #{}))) ;; name already used — append counter (t/is (= "foo-1" (d/unique-name "foo" #{"foo"}))) (t/is (= "foo-2" (d/unique-name "foo" #{"foo" "foo-1"}))) ;; name already has numeric suffix (t/is (= "foo-2" (d/unique-name "foo-1" #{"foo-1"}))) ;; prefix-first? mode — skips foo-1 (counter=1 returns bare prefix) ;; so with #{} not used, still returns "foo" (t/is (= "foo" (d/unique-name "foo" #{} true))) ;; with prefix-first? and "foo" used, counter=1 produces "foo" again (used), ;; so jumps to counter=2 → "foo-2" (t/is (= "foo-2" (d/unique-name "foo" #{"foo"} true)))) (t/deftest toggle-selection-test ;; without toggle, always returns set with just the value (let [s (d/ordered-set :a :b)] (t/is (= (d/ordered-set :c) (d/toggle-selection s :c)))) ;; with toggle=true, adds if not present (let [s (d/ordered-set :a)] (t/is (contains? (d/toggle-selection s :b true) :b))) ;; with toggle=true, removes if already present (let [s (d/ordered-set :a :b)] (t/is (not (contains? (d/toggle-selection s :a true) :a))))) (t/deftest invert-map-test (t/is (= {1 :a 2 :b} (d/invert-map {:a 1 :b 2}))) (t/is (= {} (d/invert-map {})))) (t/deftest obfuscate-string-test ;; short string (< 10) — all stars (t/is (= "****" (d/obfuscate-string "abcd"))) ;; long string — first 5 chars kept (t/is (= "hello*****" (d/obfuscate-string "helloworld"))) ;; full? mode (t/is (= "***" (d/obfuscate-string "abc" true))) ;; empty string (t/is (= "" (d/obfuscate-string "")))) (t/deftest unstable-sort-test (t/is (= [1 2 3 4] (d/unstable-sort [3 1 4 2]))) ;; In CLJS, garray/sort requires a comparator returning -1/0/1 (not boolean). ;; Use compare with reversed args for descending sort on both platforms. (t/is (= [4 3 2 1] (d/unstable-sort #(compare %2 %1) [3 1 4 2]))) ;; Empty collection: CLJ returns '(), CLJS returns nil (from seq on []) (t/is (empty? (d/unstable-sort [])))) (t/deftest opacity-to-hex-test ;; opacity-to-hex uses JavaScript number methods (.toString 16 / .padStart) ;; so it only produces output in CLJS environments. #?(:cljs (t/is (= "ff" (d/opacity-to-hex 1)))) #?(:cljs (t/is (= "00" (d/opacity-to-hex 0)))) #?(:cljs (t/is (= "80" (d/opacity-to-hex (/ 128 255))))) #?(:clj (t/is true "opacity-to-hex is CLJS-only"))) (t/deftest format-precision-test (t/is (= "12" (d/format-precision 12.0123 0))) (t/is (= "12" (d/format-precision 12.0123 1))) (t/is (= "12.01" (d/format-precision 12.0123 2))) (t/is (= "12.012" (d/format-precision 12.0123 3))) (t/is (= "0.1" (d/format-precision 0.1 2)))) (t/deftest format-number-test (t/is (= "3.14" (d/format-number 3.14159))) (t/is (= "3" (d/format-number 3.0))) (t/is (= "3.14" (d/format-number "3.14159"))) (t/is (nil? (d/format-number nil))) (t/is (= "3.1416" (d/format-number 3.14159 {:precision 4})))) (t/deftest append-class-test (t/is (= "foo bar" (d/append-class "foo" "bar"))) (t/is (= "bar" (d/append-class nil "bar"))) ;; empty string is treated like nil — no leading space (t/is (= "bar" (d/append-class "" "bar")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Additional helpers (5th batch) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (t/deftest not-empty-predicate (t/is (d/not-empty? [1 2 3])) (t/is (d/not-empty? {:a 1})) (t/is (d/not-empty? "abc")) (t/is (not (d/not-empty? []))) (t/is (not (d/not-empty? {}))) (t/is (not (d/not-empty? nil)))) (t/deftest editable-collection-predicate (t/is (d/editable-collection? [])) (t/is (d/editable-collection? [1 2 3])) (t/is (d/editable-collection? {})) (t/is (d/editable-collection? {:a 1})) (t/is (not (d/editable-collection? nil))) (t/is (not (d/editable-collection? "hello"))) (t/is (not (d/editable-collection? 42)))) (t/deftest num-predicate (t/is (d/num? 1)) (t/is (d/num? 0)) (t/is (d/num? -3.14)) (t/is (not (d/num? ##NaN))) (t/is (not (d/num? ##Inf))) (t/is (not (d/num? nil))) ;; In CLJS, js/isFinite coerces strings → (d/num? "1") is true on CLJS, false on JVM #?(:clj (t/is (not (d/num? "1")))) ;; multi-arity (t/is (d/num? 1 2)) (t/is (d/num? 1 2 3)) (t/is (d/num? 1 2 3 4)) (t/is (d/num? 1 2 3 4 5)) (t/is (not (d/num? 1 ##NaN))) (t/is (not (d/num? 1 2 ##Inf))) (t/is (not (d/num? 1 2 3 ##NaN))) (t/is (not (d/num? 1 2 3 4 ##Inf)))) (t/deftest num-string-predicate ;; num-string? returns true for strings that represent valid numbers (t/is (d/num-string? "42")) (t/is (d/num-string? "3.14")) (t/is (d/num-string? "-7")) (t/is (not (d/num-string? "hello"))) (t/is (not (d/num-string? nil))) ;; non-string types always return false (t/is (not (d/num-string? 42))) (t/is (not (d/num-string? :keyword))) ;; In CLJS, js/isNaN("") → false (empty string coerces to 0), so "" is numeric #?(:clj (t/is (not (d/num-string? "")))) #?(:cljs (t/is (d/num-string? "")))) (t/deftest percent-predicate (t/is (d/percent? "50%")) (t/is (d/percent? "100%")) (t/is (d/percent? "0%")) (t/is (d/percent? "3.5%")) ;; percent? uses str/rtrim which strips the trailing % then checks numeric, ;; so a plain numeric string without % also returns true (t/is (d/percent? "50")) (t/is (not (d/percent? "abc%"))) (t/is (not (d/percent? "abc")))) (t/deftest parse-percent-test (t/is (= 0.5 (d/parse-percent "50%"))) (t/is (= 1.0 (d/parse-percent "100%"))) (t/is (= 0.0 (d/parse-percent "0%"))) ;; falls back to parse-double when no % suffix (t/is (= 0.75 (d/parse-percent "0.75"))) ;; invalid value returns default (t/is (nil? (d/parse-percent "abc%"))) (t/is (= 0.0 (d/parse-percent "abc%" 0.0)))) (t/deftest lazy-map-test (let [calls (atom 0) m (d/lazy-map [:a :b :c] (fn [k] (swap! calls inc) (name k)))] ;; The map has the right keys (t/is (= #{:a :b :c} (set (keys m)))) ;; Values are delays — force them (t/is (= "a" @(get m :a))) (t/is (= "b" @(get m :b))) (t/is (= "c" @(get m :c))))) (t/deftest oreorder-before-test ;; No ks path: insert k v before before-k in a flat ordered-map (let [om (d/ordered-map :a 1 :b 2 :c 3) result (d/oreorder-before om [] :d 4 :b)] (t/is (= [:a :d :b :c] (vec (keys result))))) ;; before-k not found → appended at end (let [om (d/ordered-map :a 1 :b 2) result (d/oreorder-before om [] :c 3 :z)] (t/is (= [:a :b :c] (vec (keys result))))) ;; nil before-k → appended at end (let [om (d/ordered-map :a 1 :b 2) result (d/oreorder-before om [] :c 3 nil)] (t/is (= [:a :b :c] (vec (keys result))))) ;; existing key k is removed from its old position (let [om (d/ordered-map :a 1 :b 2 :c 3) result (d/oreorder-before om [] :c 99 :a)] (t/is (= [:c :a :b] (vec (keys result)))))) (t/deftest oassoc-in-before-test ;; Simple case: add a new key before an existing key (let [om (d/ordered-map :a 1 :b 2 :c 3) result (d/oassoc-in-before om [:b] [:x] 99)] (t/is (= [:a :x :b :c] (vec (keys result)))) (t/is (= 99 (get result :x)))) ;; before-k not found → oassoc-in behaviour (append) (let [om (d/ordered-map :a 1 :b 2) result (d/oassoc-in-before om [:z] [:x] 99)] (t/is (= 99 (get result :x))))) (t/deftest reorder-test ;; Move element from index 0 to position between index 2 and 3 (t/is (= [:b :c :a :d] (d/reorder [:a :b :c :d] 0 3))) ;; Move last element to the front (t/is (= [:d :a :b :c] (d/reorder [:a :b :c :d] 3 0))) ;; No-op: same logical position (from-pos == to-space-between-pos) (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] 1 1))) ;; Clamp out-of-range positions (t/is (= [:b :c :d :a] (d/reorder [:a :b :c :d] 0 100))) (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] -5 0))))