├── .gitignore ├── scripts ├── repl.sh ├── repl.clj ├── test.sh ├── build.clj └── build-docs.sh ├── test └── potpuri │ ├── benchmark.clj │ ├── runner.cljs │ └── core_test.cljc ├── README.md ├── .travis.yml ├── project.clj ├── CHANGES.md ├── LICENSE └── src └── potpuri └── core.cljc /.gitignore: -------------------------------------------------------------------------------- 1 | .nrepl* 2 | target 3 | .lein* 4 | doc 5 | gh-pages 6 | pom.xml* 7 | -------------------------------------------------------------------------------- /scripts/repl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rlwrap lein trampoline run -m clojure.main scripts/repl.clj 3 | -------------------------------------------------------------------------------- /test/potpuri/benchmark.clj: -------------------------------------------------------------------------------- 1 | (ns potpuri.benchmark 2 | (:require [potpuri.core :as p] 3 | [criterium.core :as criterium])) 4 | 5 | (comment 6 | (quick-bench (into [1] [2 3 4])) 7 | (quick-bench (apply vector 1 [ 2 3 4]))) 8 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.node :as node]) 4 | 5 | (repl/repl* (node/repl-env) 6 | {:output-dir "target/generated/js/out" 7 | :optimizations :none 8 | :cache-analysis true 9 | :source-map true}) 10 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | case $1 in 4 | cljs) 5 | lein trampoline run -m clojure.main scripts/build.clj 6 | node target/generated/js/out/tests.js 7 | ;; 8 | clj) 9 | lein test-clj 10 | ;; 11 | *) 12 | echo "Please select [clj|cljs]" 13 | exit 1 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /scripts/build.clj: -------------------------------------------------------------------------------- 1 | (require 'cljs.closure) 2 | 3 | (cljs.closure/build 4 | ; Includes :source-paths and :test-paths already 5 | "test" 6 | {:output-to "target/generated/js/out/tests.js" 7 | :main "potpuri.runner" 8 | :source-map true 9 | :output-dir "target/generated/js/out" 10 | :optimizations :none 11 | :target :nodejs}) 12 | 13 | (shutdown-agents) 14 | -------------------------------------------------------------------------------- /test/potpuri/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns potpuri.runner 2 | (:require [cljs.test :as test] 3 | [cljs.nodejs :as nodejs] 4 | potpuri.core-test)) 5 | 6 | (nodejs/enable-util-print!) 7 | 8 | (def status (atom nil)) 9 | 10 | (defn -main [] 11 | (test/run-all-tests #"^potpuri.*-test$") 12 | (js/process.exit @status)) 13 | 14 | (defmethod test/report [:cljs.test/default :end-run-tests] [m] 15 | (reset! status (if (test/successful? m) 0 1))) 16 | 17 | (set! *main-cli-fn* -main) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # potpuri [![Build Status](https://travis-ci.org/metosin/potpuri.svg?branch=master)](https://travis-ci.org/metosin/potpuri) [![cljdoc badge](https://cljdoc.org/badge/metosin/potpuri)](https://cljdoc.org/jump/release/metosin/potpuri) 2 | 3 | [![Clojars Project](http://clojars.org/metosin/potpuri/latest-version.svg)](http://clojars.org/metosin/potpuri) 4 | 5 | Common stuff missing from the clojure.core. 6 | 7 | Inspired by [Medley](https://github.com/weavejester/medley). 8 | 9 | ## License 10 | 11 | Copyright © 2014-2020 [Metosin Oy](http://www.metosin.fi) 12 | 13 | Distributed under the Eclipse Public License 2.0. 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: clojure 3 | lein: 2.8.1 4 | script: ./scripts/test.sh $TEST 5 | env: 6 | matrix: 7 | - TEST=clj 8 | - TEST=cljs 9 | matrix: 10 | exclude: 11 | - env: TEST=cljs 12 | jdk: openjdk7 13 | - env: TEST=cljs 14 | jdk: oraclejdk11 15 | jdk: 16 | - oraclejdk8 17 | - oraclejdk11 18 | node_js: 19 | - 10 20 | cache: 21 | directories: 22 | - "$HOME/.m2" 23 | notifications: 24 | flowdock: 25 | secure: cYGPwnvB5mf4x8gLYKwWKK32LBT2eDuF7unpHwbyw+nVz/sMjpb3GXlcWY48rF2oqpE2pvKtqx07nfIsA89mhPCAjaXKP77fZv9QBlmLtMwnD26PDWIMlCIp9aPg4ktHYOvYjslycrFBUj69FWpV7ciLYojv4VC1/J5XEQgF2LE= 26 | -------------------------------------------------------------------------------- /scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | rev=$(git rev-parse HEAD) 6 | remoteurl=$(git ls-remote --get-url origin) 7 | repodir=gh-pages 8 | tag=$(git tag --points-at HEAD) 9 | name=$tag 10 | if [[ -z $name ]]; then 11 | name=master 12 | fi 13 | target=$repodir/$name 14 | 15 | git fetch 16 | if [[ -z $(git branch -r --list origin/gh-pages) ]]; then 17 | ( 18 | mkdir "$repodir" 19 | cd "$repodir" 20 | git init 21 | git remote add origin "${remoteurl}" 22 | git checkout -b gh-pages 23 | git commit --allow-empty -m "Init" 24 | git push -u origin gh-pages 25 | ) 26 | elif [[ ! -d "$repodir" ]]; then 27 | git clone --branch gh-pages "${remoteurl}" "$repodir" 28 | else 29 | ( 30 | cd "$repodir" 31 | git pull 32 | ) 33 | fi 34 | 35 | rm -fr doc 36 | lein codox 37 | 38 | # replace docs for current version with new docs 39 | rm -fr "$target" 40 | cp -r doc "$target" 41 | 42 | cd "$repodir" 43 | git add --all 44 | git commit -m "Build docs from ${rev}." 45 | git push origin gh-pages 46 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject metosin/potpuri "0.5.3" 2 | :description "Common stuff missing from the clojure.core." 3 | :url "https://github.com/metosin/potpuri" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html" 6 | :distribution :repo 7 | :comments "same as Clojure"} 8 | :dependencies [] 9 | :plugins [[lein-codox "0.10.7"] 10 | [metosin/bat-test "0.4.4"]] 11 | 12 | :bat-test {:report [:pretty 13 | {:type :junit :output-to "target/junit.xml"}]} 14 | 15 | :source-paths ["src" "target/generated/src"] 16 | :test-paths ["test" "target/generated/test"] 17 | 18 | :codox {:output-path "doc" 19 | :source-uri "https://github.com/metosin/potpuri/blob/{version}/{filepath}#L{line}" 20 | :metadata {:doc/format :markdown}} 21 | 22 | :profiles {:dev {:plugins [[jonase/eastwood "0.3.12"]] 23 | :dependencies [[org.clojure/clojure "1.9.0"] 24 | [criterium "0.4.6"] 25 | [org.clojure/clojurescript "1.10.520"]]} 26 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"] 27 | ;; Test aren't run with old cljs - but new cljs would bring in 28 | ;; new tools.reader which doesn't work with old Clojure. 29 | [org.clojure/clojurescript "1.7.228"]]} 30 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 31 | :1.10 {:dependencies [[org.clojure/clojure "1.10.0"]]}} 32 | :deploy-repositories [["releases" :clojars]] 33 | :aliases {"all" ["with-profile" "dev:dev,1.7:dev,1.8:dev,1.10"] 34 | "test-clj" ["all" "do" ["bat-test"] ["check"]]}) 35 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.5.3 (2021-01-04) 2 | 3 | [API Docs](https://cljdoc.org/d/metosin/potpuri/0.5.2) - [compare](https://github.com/metosin/potpuri/compare/0.5.2...0.5.3) 4 | 5 | - Add `update-if-contains` 6 | 7 | ## 0.5.2 (2018-12-14) 8 | 9 | [API Docs](http://metosin.github.io/potpuri/0.5.2/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.5.1...0.5.2) 10 | 11 | - Add `where-fn` function to return the predicate fn used by `find-first` and other 12 | similar functions 13 | 14 | ## 0.5.1 (7.11.2017) 15 | 16 | [API Docs](http://metosin.github.io/potpuri/0.5.1/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.5.0...0.5.1) 17 | 18 | - Added `map-entries`, `filter-entries` and `remove-entries` 19 | 20 | ## 0.5.0 (03.10.2017) 21 | 22 | [API Docs](http://metosin.github.io/potpuri/0.5.0/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.4.0...0.5.0) 23 | 24 | - Added `remove-keys` and `remove-vals` 25 | - Added `build-tree` 26 | 27 | ## 0.4.0 (30.11.2016) 28 | 29 | [API Docs](http://metosin.github.io/potpuri/0.4.0/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.3.0...0.4.0) 30 | 31 | - Added `zip` 32 | 33 | ## 0.3.0 (26.4.2016) 34 | 35 | [API Docs](http://metosin.github.io/potpuri/0.3.0/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.2.3...0.3.0) 36 | 37 | - Updated to use Cljc (drops Clojure 1.6 support) 38 | - Added `index-by` 39 | - Added `condas->` 40 | 41 | ## 0.2.3 (12.7.2015) 42 | 43 | [API Docs](http://metosin.github.io/potpuri/0.2.3/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.2.2...0.2.2) 44 | 45 | - Added `if-all-let` 46 | 47 | ## 0.2.2 (7.4.2015) 48 | 49 | [API Docs](http://metosin.github.io/potpuri/0.2.2/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.2.1...0.2.2) 50 | 51 | - `map-keys` and `map-vals` should now work with records 52 | - Added `filter-keys` and `filter-vals` 53 | 54 | ## 0.2.1 (16.1.2015) 55 | 56 | [API Docs](http://metosin.github.io/potpuri/0.2.1/index.html) - [compare](https://github.com/metosin/potpuri/compare/0.2.0...0.2.1) 57 | 58 | - Added `assoc-first` and `update-first` functions which can be used to 59 | update first matching element in collection 60 | - Tests are now using `clojure.test` / `cljs.test` instead of Midje 61 | - CI is running Cljs tests using Node 62 | -------------------------------------------------------------------------------- /test/potpuri/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns potpuri.core-test 2 | (:require #?(:clj [clojure.test :refer :all] 3 | :cljs [cljs.test :refer-macros [deftest is testing]]) 4 | [potpuri.core :as p])) 5 | 6 | (deftest fn->test 7 | (let [inc-x (p/fn-> :x inc) 8 | sum-doubled-vals (p/fn->> vals (map (partial * 2)) (apply +)) 9 | m {:x 1 :y 2 :z 3}] 10 | (is (= (inc-x m) 2)) 11 | (is (= (sum-doubled-vals m) 12)))) 12 | 13 | (deftest condas->test 14 | (is (= 3 15 | (p/condas-> 1 number 16 | (= 1 number) (inc number) 17 | (= 2 number) (inc number)))) 18 | (is (= 2 19 | (p/condas-> 1 number 20 | (= 1 number) (inc number) 21 | (= 1 number) (inc number))))) 22 | 23 | (deftest if-all-let 24 | (is (true? (p/if-all-let [a 1] true))) 25 | (is (true? (p/if-all-let [a 1 b 2] true))) 26 | (is (= :else (p/if-all-let [a nil b 2] :true :else))) 27 | (is (nil? (p/if-all-let [a 1 b nil] true)))) 28 | 29 | (def original {:a {:b {:c 1 30 | :d 2} 31 | :e 3}}) 32 | (def target [[[:a :e] 3] 33 | [[:a :b :d] 2] 34 | [[:a :b :c] 1]]) 35 | 36 | (deftest path-vals-test 37 | (is (= (set (p/path-vals original)) (set target)))) 38 | 39 | (deftest assoc-in-path-vals-test 40 | (is (= (p/assoc-in-path-vals target) original))) 41 | 42 | (def m {:a {:b1 {:c1 "kikka" 43 | :c2 "kakka"} 44 | :b2 "kukka"}}) 45 | 46 | (deftest dissoc-in-test 47 | (is (= (p/dissoc-in m [:a]) {})) 48 | (is (= (p/dissoc-in m [:b :c]) m)) 49 | (is (= (p/dissoc-in {:a m} [:a :a]) {})) 50 | 51 | (is (= (p/dissoc-in m [:a :b2]) 52 | {:a {:b1 {:c1 "kikka" 53 | :c2 "kakka"}}})) 54 | 55 | (is (= (p/dissoc-in m [:a :b1 :c2]) 56 | {:a {:b1 {:c1 "kikka"} 57 | :b2 "kukka"}})) 58 | 59 | (is (= (p/dissoc-in m [nil]) m))) 60 | 61 | (deftest map-of-test 62 | (let [a 1 b true c [:abba :jabba]] 63 | (is (= (p/map-of a b c) 64 | {:a 1 :b true :c [:abba :jabba]})))) 65 | 66 | (deftest deep-merge-test 67 | (testing "basics" 68 | (is (= (p/deep-merge {:a {:c 2}} {:a {:b 1}}) {:a {:b 1 :c 2}})) 69 | (is (= (p/deep-merge {:a 1} {:a 2}) {:a 2})) 70 | (is (= (p/deep-merge {:a {:b 1}} {:a {:b 2}}) {:a {:b 2}})) 71 | (is (= (p/deep-merge {:a {:b 1}} {:a {:b nil}}) {:a {:b nil}})) 72 | (is (= (p/deep-merge {:a 1} nil) nil)) 73 | ) 74 | 75 | (testing "sequentials" 76 | (is (= (p/deep-merge {:a [1]} {:a [2]}) {:a [2]})) 77 | (is (= (p/deep-merge :into {:a [1]} {:a [2]}) {:a [1 2]})) 78 | (is (= (p/deep-merge :into {:a #{:a}} {:a #{:b}}) {:a #{:b :a}})))) 79 | 80 | (deftest wrap-into-test 81 | (is (= (p/wrap-into [] "foo") ["foo"])) 82 | (is (= (p/wrap-into [] ["a" "b"]) ["a" "b"])) 83 | (is (= (p/wrap-into #{} "a") #{"a"})) 84 | (is (= (p/wrap-into #{} ["a" "b"]) #{"a" "b"}))) 85 | 86 | (deftest assoc-if-test 87 | (is (= (p/assoc-if {} :a 5) {:a 5})) 88 | (is (= (p/assoc-if {:a 5} :b nil) {:a 5})) 89 | (is (= (p/assoc-if {:a 5} :a nil) {:a 5})) 90 | (is (= (p/assoc-if {} :a 1 :b false :c 2) {:a 1 :b false :c 2}))) 91 | 92 | (deftest where-fn-test 93 | (testing "fn?'s are returned as is" 94 | (let [f (constantly true)] 95 | (is (identical? f (p/where-fn f))))) 96 | 97 | (testing "ifn?'s are returned as is" 98 | (is (= :foo (p/where-fn :foo))) 99 | (is (= #{42} (p/where-fn #{42})))) 100 | 101 | (testing "map predicates" 102 | (let [p (p/where-fn {:foo 42})] 103 | (is (= (p {:foo 42}) true)) 104 | (is (= (p {:foo 42 105 | :bar 1337}) true)) 106 | (is (= (p {:bar 1337}) false))))) 107 | 108 | (deftest conjv-test 109 | (is (= (p/conjv [1 2] 3) [1 2 3])) 110 | (is (= (update-in {:a [1 2]} [:a] p/conjv 3) {:a [1 2 3]})) 111 | (is (= (-> [1 2] (p/conjv 3)) [1 2 3])) 112 | (testing "conjv to nil will create vec instead of seq" 113 | (is (vector? (:a (update-in {} [:a] p/conjv 1)))))) 114 | 115 | (deftest consv-test 116 | (is (= (p/consv [2 3] 1) [1 2 3])) 117 | (is (= (update-in {:a [2 3]} [:a] p/consv 1) {:a [1 2 3]})) 118 | (is (= (-> [2 3] (p/consv 1)) [1 2 3]))) 119 | 120 | (def test-coll [{:id 1 :foo "bar"} 121 | {:id 2 :foo "foo"}]) 122 | 123 | (deftest find-index-test 124 | (testing "map based where" 125 | (is (= (p/find-index test-coll {:id 1}) 0)) 126 | (is (= (p/find-index test-coll {:id 2}) 1)) 127 | (is (= (p/find-index test-coll {:id 2 :foo "foo"}) 1))) 128 | 129 | (testing "predicate where" 130 | (is (= (p/find-index test-coll (comp even? :id)) 1))) 131 | 132 | (testing "keyword where" 133 | (is (= (p/find-index test-coll :id) 0))) 134 | 135 | (testing "set where" 136 | (is (= (p/find-index ["a" "b" "c"] #{"c"}) 2))) 137 | 138 | (testing "value identity where" 139 | (is (= (p/find-index [4 3 2] 3) 1))) 140 | 141 | (testing "-> syntax" 142 | (is (= (-> test-coll (p/find-index {:id 2})) 1))) 143 | 144 | (testing "different coll types" 145 | (testing "seq" 146 | (is (= (p/find-index (seq test-coll) {:id 1}) 0))))) 147 | 148 | (deftest find-first-test 149 | (is (= (p/find-first test-coll {:id 2}) (nth test-coll 1))) 150 | (is (= (-> test-coll (p/find-first {:id 2})) (nth test-coll 1)))) 151 | 152 | (deftest assoc-first-test 153 | (is (= (p/assoc-first test-coll {:id 2} {:id 2 :foo "zzz"}) (assoc-in test-coll [1 :foo] "zzz"))) 154 | (testing "seq" 155 | (is (= (p/assoc-first (seq test-coll) {:id 2} {:id 2 :foo "zzz"}) (seq (assoc-in test-coll [1 :foo] "zzz")))))) 156 | 157 | (deftest update-first-test 158 | (is (= (p/update-first test-coll {:id 2} #(assoc % :foo "zzz")) (assoc-in test-coll [1 :foo] "zzz"))) 159 | (testing "rest args" 160 | (is (= (p/update-first test-coll {:id 2} assoc :foo "zzz") (assoc-in test-coll [1 :foo] "zzz")))) 161 | (testing "seq" 162 | (is (= (p/update-first (seq test-coll) {:id 2} assoc :foo "zzz") (seq (assoc-in test-coll [1 :foo] "zzz")))))) 163 | 164 | (deftest map-keys-test 165 | (is (= (p/map-keys keyword {"a" 1 "b" 2}) {:a 1 :b 2}))) 166 | 167 | (deftest map-vals-test 168 | (is (= (p/map-vals inc {:a 1 :b 2}) {:a 2 :b 3}))) 169 | 170 | (deftest map-entries-test 171 | (is (= (p/map-entries (fn [[k v]] [k (inc v)]) {:a 1 :b 2}) {:a 2 :b 3}))) 172 | 173 | (deftest filter-keys-test 174 | (is (= {:a 1} (p/filter-keys #{:a} {:a 1 :b 2})))) 175 | 176 | (deftest filter-vals-test 177 | (is (= {:a 1} (p/filter-vals #{1} {:a 1 :b 2})))) 178 | 179 | (deftest filter-entries-test 180 | (is (= {:a 1} (p/filter-entries (comp #{1} second) {:a 1 :b 2})))) 181 | 182 | (deftest remove-keys-test 183 | (is (= {:b 2} (p/remove-keys #{:a} {:a 1 :b 2})))) 184 | 185 | (deftest remove-vals-test 186 | (is (= {:b 2} (p/remove-vals #{1} {:a 1 :b 2})))) 187 | 188 | (deftest remove-entries-test 189 | (is (= {:b 2} (p/remove-entries (comp #{1} second) {:a 1 :b 2})))) 190 | 191 | (deftest index-by-test 192 | (is (= {1 {:id 1 :v "foo"} 193 | 2 {:id 2 :v "bar"}} 194 | (p/index-by :id [{:id 1 :v "foo"} {:id 2 :v "bar"}])))) 195 | 196 | (deftest zip-test 197 | (is (= [[1 :a] [2 :b] [3 :c]] (p/zip [1 2 3] [:a :b :c])))) 198 | 199 | (deftest build-tree-test 200 | (testing "Basic case, depth 1" 201 | (is (= [{:id 1 202 | :parent nil 203 | :children [{:id 2 :parent 1} 204 | {:id 3 :parent 1}]}] 205 | (p/build-tree 206 | {:id-fn :id 207 | :parent-fn :parent 208 | :assoc-children-fn #(assoc %1 :children %2)} 209 | [{:id 1 :parent nil} 210 | {:id 2 :parent 1} 211 | {:id 3 :parent 1}])))) 212 | 213 | (testing "Basic case, modify items after building tree" 214 | (is (= [{:id 1 215 | :children [{:id 2} 216 | {:id 3}]}] 217 | (p/build-tree 218 | {:id-fn :id 219 | :parent-fn :parent 220 | :item-fn #(dissoc % :parent) 221 | :assoc-children-fn #(assoc %1 :children %2)} 222 | [{:id 1 :parent nil} 223 | {:id 2 :parent 1} 224 | {:id 3 :parent 1}])))) 225 | 226 | (testing "Depth 2" 227 | (is (= [{:id 1 228 | :parent nil 229 | :children [{:id 2 :parent 1 230 | :children [{:id 3 :parent 2}]}]}] 231 | (p/build-tree 232 | {:id-fn :id 233 | :parent-fn :parent 234 | :children-fn vec 235 | :assoc-children-fn #(assoc %1 :children %2)} 236 | [{:id 1 :parent nil} 237 | {:id 2 :parent 1} 238 | {:id 3 :parent 2}])))) 239 | 240 | (testing "Multiple roots" 241 | (is (= [{:id 1 242 | :parent nil 243 | :children [{:id 3 :parent 1}]} 244 | {:id 2 245 | :parent nil 246 | :children [{:id 4 :parent 2}]}] 247 | (p/build-tree 248 | {:id-fn :id 249 | :parent-fn :parent 250 | :assoc-children-fn #(assoc %1 :children %2)} 251 | [{:id 1 :parent nil} 252 | {:id 2 :parent nil} 253 | {:id 3 :parent 1} 254 | {:id 4 :parent 2}])))) 255 | 256 | (testing "Children as map" 257 | (is (= {1 {:id 1 258 | :parent nil 259 | :children {2 {:id 2 :parent 1} 260 | 3 {:id 3 :parent 1}}}} 261 | (p/build-tree 262 | {:id-fn :id 263 | :parent-fn :parent 264 | :children-fn #(p/index-by :id %) 265 | :assoc-children-fn #(assoc %1 :children %2)} 266 | [{:id 1 :parent nil} 267 | {:id 2 :parent 1} 268 | {:id 3 :parent 1}])))) 269 | 270 | (testing "Duplicate items" 271 | (is (= [{:id 1 272 | :parent nil 273 | :children [{:id 3 :parent 1} 274 | {:id 4 :parent 1}]} 275 | {:id 2 276 | :parent nil 277 | :children [{:id 3 :parent 2} 278 | {:id 4 :parent 2}]}] 279 | ;; In this case, user could separate :parents to items with :parent 280 | (p/build-tree 281 | {:id-fn :id 282 | :parent-fn :parent 283 | :assoc-children-fn #(assoc %1 :children %2)} 284 | (mapcat (fn [item] 285 | (if (seq (:parents item)) 286 | (map #(-> item (dissoc :parents) (assoc :parent %)) (:parents item)) 287 | [(-> item (dissoc :parents) (assoc :parent nil))])) 288 | [{:id 1 :parents nil} 289 | {:id 2 :parents nil} 290 | {:id 3 :parents [1 2]} 291 | {:id 4 :parents [1 2]}]))))) 292 | ) 293 | 294 | (deftest update-if-contains-test 295 | (is (nil? (p/update-if-contains nil :a inc))) 296 | (is (= (p/update-if-contains {} :a inc) {})) 297 | (is (= (p/update-if-contains {:a 0} :a inc) {:a 1})) 298 | (is (= (p/update-if-contains {:a 0} :a + 5) {:a 5}))) 299 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /src/potpuri/core.cljc: -------------------------------------------------------------------------------- 1 | (ns potpuri.core 2 | #?(:cljs (:require-macros potpuri.core))) 3 | 4 | (defmacro fn-> 5 | "Creates a function that threads on input with `some->`" 6 | {:added "0.1.0"} 7 | [& body] `(fn [x#] (some-> x# ~@body))) 8 | 9 | (defmacro fn->> 10 | "Creates a function that threads on input with `some->>`" 11 | {:added "0.1.0"} 12 | [& body] `(fn [x#] (some->> x# ~@body))) 13 | 14 | ;; https://blog.juxt.pro/posts/condas.html 15 | (defmacro condas-> 16 | "A mixture of cond-> and as-> allowing more flexibility in the test and step forms" 17 | {:added "0.3.0"} 18 | [expr name & clauses] 19 | (assert (even? (count clauses))) 20 | (let [pstep (fn [[test step]] `(if ~test ~step ~name))] 21 | `(let [~name ~expr 22 | ~@(interleave (repeat name) (map pstep (partition 2 clauses)))] 23 | ~name))) 24 | 25 | (defmacro if-all-let 26 | "`bindings => [binding-form test, binding-form test ...]` 27 | 28 | If all tests are `true`, evaluates then with binding-forms bound to the values of 29 | tests, if not, yields `else.`" 30 | {:added "0.2.3"} 31 | ([bindings then] `(if-all-let ~bindings ~then nil)) 32 | ([bindings then else] 33 | (reduce (fn [subform binding] 34 | `(if-let [~@binding] ~subform ~else)) 35 | then (reverse (partition 2 bindings))))) 36 | 37 | (defn path-vals 38 | "Returns vector of tuples containing path vector to the value and the value." 39 | {:added "0.1.0"} 40 | [m] 41 | (letfn 42 | [(pvals [l p m] 43 | (reduce 44 | (fn [l [k v]] 45 | (if (map? v) 46 | (pvals l (conj p k) v) 47 | (cons [(conj p k) v] l))) 48 | l m))] 49 | (pvals [] [] m))) 50 | 51 | (defn assoc-in-path-vals 52 | "Re-created a map from it's path-vals extracted with (path-vals)." 53 | {:added "0.1.0"} 54 | [c] (reduce (partial apply assoc-in) {} c)) 55 | 56 | ;; https://github.com/clojure/core.incubator/blob/master/src/main/clojure/clojure/core/incubator.clj 57 | (defn dissoc-in 58 | "Dissociates an entry from a nested associative structure returning a new 59 | nested structure. `keys` is a sequence of keys. Any empty maps that result 60 | will not be present in the new structure." 61 | {:added "0.1.0"} 62 | [m [k & ks :as keys]] 63 | (if ks 64 | (if-let [nextmap (get m k)] 65 | (let [newmap (dissoc-in nextmap ks)] 66 | (if (seq newmap) 67 | (assoc m k newmap) 68 | (dissoc m k))) 69 | m) 70 | (dissoc m k))) 71 | 72 | (defmacro map-of 73 | "Creates map with symbol names as keywords as keys and 74 | symbol values as values." 75 | {:added "0.1.0"} 76 | [& syms] 77 | `(zipmap ~(vec (map keyword syms)) ~(vec syms))) 78 | 79 | (defn deep-merge 80 | "Recursively merges maps. 81 | 82 | If the first parameter is a keyword it tells the strategy to 83 | use when merging non-map collections. Options are 84 | 85 | - `:replace`, the default, the last value is used 86 | - `:into`, if the value in every map is a collection they are concatenated 87 | using into. Thus the type of (first) value is maintained. 88 | 89 | Examples: 90 | 91 | (deep-merge {:a {:c 2}} {:a {:b 1}}) => {:a {:b 1 :c 2}} 92 | (deep-merge :replace {:a [1]} {:a [2]}) => {:a [2]} 93 | (deep-merge :into {:a [1]} {:a [2]}) => {:a [1 2]} 94 | (deep-merge {:a 1} nil) => nil 95 | 96 | See also: [meta-merge](https://github.com/weavejester/meta-merge)." 97 | {:added "0.2.0" 98 | :arglists '([strategy & values] [values])} 99 | [& values] 100 | (let [[values strategy] (if (keyword? (first values)) 101 | [(rest values) (first values)] 102 | [values :replace])] 103 | (cond 104 | (every? map? values) 105 | (apply merge-with (partial deep-merge strategy) values) 106 | 107 | (and (= strategy :into) (every? coll? values)) 108 | (reduce into values) 109 | 110 | :else 111 | (last values)))) 112 | 113 | (defn wrap-into 114 | "Wrap non-collection values into given collection. 115 | Collections are only put into the collection (non-wrapped). 116 | 117 | Examples: 118 | 119 | (wrap-into [] :a) => [:a] 120 | (wrap-into [] [:a]) => [:a] 121 | (wrap-into #{} [:a]) => #{:a}" 122 | {:added "0.2.0"} 123 | [coll v] 124 | (into coll (if (coll? v) 125 | v 126 | [v]))) 127 | 128 | (defn assoc-if 129 | "Assoc key-value pairs with non-nil values into map." 130 | {:added "0.2.0"} 131 | ([m key val] (if-not (nil? val) (assoc m key val) m)) 132 | ([m key val & kvs] 133 | (let [ret (assoc-if m key val)] 134 | (if kvs 135 | (if (next kvs) 136 | (recur ret (first kvs) (second kvs) (nnext kvs)) 137 | (throw 138 | #?(:clj (IllegalArgumentException. "assoc expects even number of arguments after map/vector, found odd number") 139 | :cljs "assoc expects even number of arguments after map/vector, found odd number"))) 140 | ret)))) 141 | 142 | (defn where-fn 143 | "Returns a predicate that accepts a value and performs a check based on `where` argument. 144 | 145 | If `where` is a map, returns a predicate that compares all key/value pairs of `where` to 146 | the key/values of the value given to the predicate, and returns truthy value if all 147 | pairs are found. 148 | 149 | If `where` is a function (either `fn?` or `ifn?`), returns `where`. 150 | 151 | For all other values of `where` returns a predicate that compares the argument of predicate 152 | against `where` using `clojure.core/=`." 153 | {:added "0.5.2"} 154 | [where] 155 | (cond 156 | ; fn? and map? first as map also implements IFn 157 | (map? where) 158 | (fn [v] 159 | (every? (fn [[where-k where-v]] 160 | (= (get v where-k) where-v)) 161 | where)) 162 | 163 | ; Keywords, sets, ... 164 | (ifn? where) 165 | where 166 | 167 | :default 168 | (fn [v] 169 | (= v where)))) 170 | 171 | (defn find-index 172 | "Find index of vector which matches the `where` parameter. 173 | 174 | If `where` parameter is: 175 | 176 | - a map, a predicate is created which compares every key/value pair of `where` value to 177 | collection value. 178 | - something which implements IFn, e.g. keywords and sets, is used as is 179 | - other values, compares the item using using `clojure.core/=` 180 | 181 | Examples: 182 | 183 | (find-index [1 2 3] even?) => 1 184 | (find-index [{:id 1} {:id 2}] {:id 2}) => 1 185 | (find-index [{:a 1} {:b 2}] :b) => 1 186 | (find-index [1 2 3] #{3}) => 2 187 | (find-index [1 2 3] 3) => 2 188 | (-> [1 2 3] (find-index odd?)) => 0" 189 | {:added "0.2.0"} 190 | [coll where] 191 | (let [pred (where-fn where)] 192 | (first (keep-indexed #(if (pred %2) %1) coll)))) 193 | 194 | (defn find-first 195 | "Find first value from collection which matches the `where` parameter. 196 | 197 | If `where` parameter is: 198 | 199 | - a map, a predicate is created which compares every key/value pair of `where` value to 200 | collection value. 201 | - something which implements IFn, e.g. keywords and sets, is used as is 202 | - other values, compares the item using using `clojure.core/=` 203 | 204 | Examples: 205 | 206 | (find-first [1 2 3] even?) => 2 207 | (find-first [{:id 1} {:id 2, :foo :bar}] {:id 2}) => {:id 2, :foo :bar} 208 | (find-first [{:a 1} {:b 2, :foo :bar}] :b) => {:b 2, :foo :bar} 209 | (find-first [1 2 3] #{3}) => 3 210 | (find-first [1 2 3] 3) => 3 211 | (-> [1 2 3] (find-first odd?)) => 1" 212 | {:added "0.2.0"} 213 | [coll where] 214 | (let [pred (where-fn where)] 215 | (some #(if (pred %) %) coll))) 216 | 217 | (defn assoc-first 218 | "Finds the first element in collection matching `where` parameter and 219 | replaces that with `v.` 220 | 221 | Implementation depends on collection type." 222 | {:added "0.2.1"} 223 | [coll where v] 224 | (let [pred (where-fn where)] 225 | (cond 226 | (vector? coll) (assoc coll (find-index coll pred) v) 227 | :default (map (fn [x] 228 | (if (pred x) v x)) 229 | coll)))) 230 | 231 | (defn update-first 232 | "Finds the first element in collection matching the `where` parameter 233 | and updates that using `f.` `f` is called with current value and 234 | rest of update-first params. 235 | 236 | Implementation depends on collection type." 237 | {:added "0.2.1"} 238 | [coll where f & args] 239 | (let [pred (where-fn where)] 240 | (cond 241 | (vector? coll) (apply update-in coll [(find-index coll pred)] f args) 242 | :default (map (fn [x] 243 | (if (pred x) (apply f x args) x)) 244 | coll)))) 245 | 246 | (def ^:private conjv' (fnil conj [])) 247 | 248 | (defn conjv 249 | "Append an element to a collection. If collection is `nil`, creates vector 250 | instead of sequence. The appending might happen on different places 251 | depending on the type of collection. 252 | 253 | Examples: 254 | 255 | (conjv nil 5) => [5] 256 | (conjv [1] 2) => [1 2] 257 | (update-in {} [:a] conjv 5) => {:a [5]} 258 | (-> [] (conjv 5)) => [5]" 259 | {:added "0.2.0"} 260 | [coll el] 261 | (conjv' coll el)) 262 | 263 | (defn consv 264 | "Prepend an element to a collection. Returns a vector. 265 | 266 | Examples: 267 | 268 | (consv nil 1) => [1] 269 | (consv [2] 1) => [1 2] 270 | (update-in {:a 2} [:a] consv 1) => {:a [1 2]} 271 | (-> [2] (consv 5)) => [1 2]" 272 | {:added "0.2.0"} 273 | [coll el] 274 | (apply vector el coll)) 275 | 276 | ;;;; map for kv collections 277 | 278 | ;; These are like ones in medley 279 | 280 | (defn- editable? [coll] 281 | #?(:clj (instance? clojure.lang.IEditableCollection coll) 282 | :cljs (satisfies? cljs.core.IEditableCollection coll))) 283 | 284 | (defn- reduce-map [f coll] 285 | (if (editable? coll) 286 | (persistent! (reduce-kv (f assoc!) (transient (empty coll)) coll)) 287 | (reduce-kv (f assoc) (empty coll) coll))) 288 | 289 | (defn map-keys 290 | "Map the keys of given associative collection using function." 291 | {:added "0.2.0"} 292 | [f coll] 293 | (reduce-map (fn [xf] (fn [m k v] 294 | (xf m (f k) v))) 295 | coll)) 296 | 297 | (defn map-vals 298 | "Map the values of given associative collection using function." 299 | {:added "0.2.0"} 300 | [f coll] 301 | (reduce-map (fn [xf] (fn [m k v] 302 | (xf m k (f v)))) 303 | coll)) 304 | 305 | (defn map-entries 306 | "Map the entries of given associative collection using function." 307 | {:added "0.5.1"} 308 | [f coll] 309 | (reduce-map (fn [xf] (fn [m k v] 310 | (let [[k v] (f [k v])] 311 | (xf m k v)))) 312 | coll)) 313 | 314 | (defn filter-keys 315 | "Filter given associative collection using function on the keys." 316 | {:added "0.2.2"} 317 | [pred coll] 318 | (reduce-map (fn [xf] (fn [m k v] 319 | (if (pred k) (xf m k v) m))) 320 | coll)) 321 | 322 | (defn filter-vals 323 | "Filter given associative collection using function on the values." 324 | {:added "0.2.2"} 325 | [pred coll] 326 | (reduce-map (fn [xf] (fn [m k v] 327 | (if (pred v) (xf m k v) m))) 328 | coll)) 329 | 330 | (defn filter-entries 331 | "Filter given associative collection using function on the values." 332 | {:added "0.5.1"} 333 | [pred coll] 334 | (reduce-map (fn [xf] (fn [m k v] 335 | (if (pred [k v]) (xf m k v) m))) 336 | coll)) 337 | 338 | (defn remove-keys 339 | "Removes given associative collection using function on the keys." 340 | {:added "0.5.0"} 341 | [pred coll] 342 | (filter-keys (complement pred) coll)) 343 | 344 | (defn remove-vals 345 | "Removes given associative collection using function on the values." 346 | {:added "0.5.0"} 347 | [pred coll] 348 | (filter-vals (complement pred) coll)) 349 | 350 | (defn remove-entries 351 | "Removes given associative collection using function on the values." 352 | {:added "0.5.1"} 353 | [pred coll] 354 | (filter-entries (complement pred) coll)) 355 | 356 | (defn index-by 357 | "Returns a map of the elements of coll keyed by the result of 358 | f on each element. The value at each key will the last item 359 | for given f result." 360 | {:added "0.3.0"} 361 | [f coll] 362 | ; FIXME: perf test against reduce+transient and zipmap 363 | (into {} (map (juxt f identity)) coll)) 364 | 365 | (defn zip 366 | "Returns a sequence of vectors where the i-th vector contains 367 | the i-th element from each of the argument collections. The returned 368 | sequence is as long as the shortest argument. 369 | 370 | Example: 371 | 372 | (zip [1 2 3] [:a :b :c]) => ([1 :a] [2 :b] [3 :c]) 373 | (zip [1] [1 2] [1 2 3]) => ([1 1 1])" 374 | {:added "0.4.0"} 375 | [& colls] 376 | (apply map vector colls)) 377 | 378 | (defn- build-tree' [{:keys [id-fn item-fn children-fn assoc-children-fn] 379 | :or {children-fn identity 380 | item-fn identity} 381 | :as opts} 382 | g 383 | items] 384 | (children-fn 385 | (map (fn [item] 386 | (let [children (build-tree' opts g (get g (id-fn item)))] 387 | (item-fn 388 | (if (seq children) 389 | (assoc-children-fn item children) 390 | item)))) 391 | items))) 392 | 393 | (defn build-tree 394 | "Builds a tree from given items collections. 395 | 396 | ID is what is used to match parents and children. 397 | Root items are those where parent-fn returns nil. 398 | 399 | Options: 400 | 401 | - :parent-fn (required) Used to create a map from ID => children 402 | - :id-fn (required) Used to get the ID from an item 403 | - :assoc-children-fn (required) Attach the children to an item 404 | - :item-fn (optional) Called for each item, after children has been attached to the item 405 | - :children-fn (optional) Called for each children collection 406 | 407 | Example: 408 | (build-tree {:id-fn :id, :parent-fn :parent, :assoc-children-fn #(assoc %1 :children %2)} 409 | [{:id 1} {:id 2 :parent 1} {:id 3 :parent 1}]) 410 | => [{:id 1 :children [{:id 2} {:id 3}]}] 411 | 412 | Check test file for more examples." 413 | {:added "0.5.0"} 414 | [{:keys [parent-fn id-fn assoc-children-fn] :as opts} items] 415 | (assert parent-fn ":parent-fn option is required.") 416 | (assert id-fn ":id-fn option is required.") 417 | (assert assoc-children-fn ":assoc-children-fn option is required.") 418 | 419 | (let [g (group-by parent-fn items)] 420 | ;; Start with items which have no parent => root items 421 | (build-tree' opts g (get g nil)))) 422 | 423 | (defn update-if-contains 424 | "Applies `clojure.core/update` on an associative structure, when k 425 | is a key contained in that structure. If the key does not exist, the 426 | associative structure is returned as is. 427 | 428 | Examples: 429 | 430 | (update-if-contains {} :a inc) => {} 431 | (update-if-contains {:a 0} :a inc) => {:a 1} 432 | " 433 | [m k & rest] 434 | (if (contains? m k) 435 | (apply update (conj rest k m)) 436 | m)) 437 | --------------------------------------------------------------------------------