Skip to content

Instantly share code, notes, and snippets.

@mbezjak
Last active November 30, 2022 12:37
Show Gist options
  • Save mbezjak/baa6622a6edfa40e61aeff27041266dc to your computer and use it in GitHub Desktop.
Save mbezjak/baa6622a6edfa40e61aeff27041266dc to your computer and use it in GitHub Desktop.
clojure.core extensions
(ns my.company.coll
"Missing functions in `clojure.core` that accept a collection.
Argument names and expected positions use the same convention as `clojure.core.`"
(:refer-clojure :exclude [any?]))
(defn any?
"Opposite of `clojure.core/not-any?`.
Returns `false` for empty `coll`."
[pred coll]
(boolean (some pred coll)))
(defn includes?
"Does `coll` include `x`?
Guaranteed to return a boolean. Time complexity linear, so don't use for large
collections."
[x coll]
(any? #(= x %) coll))
(defn elem
"Find the first element matching `pred` in `coll`.
Returns `nil` on no match. Time complexity linear, so don't use for large
collections."
[pred coll]
(some #(when (pred %) %) coll))
(defn groups
"Group elements in `coll` together based on the value returned by `f`.
Group order is not guaranteed. Element order inside each group is guaranteed
to be stable. Useful when the value given by `f` is not needed. Returns `nil`
for empty collections.
Example:
(= '([{:id 1 :name :a} {:id 1 :name :c}]
[{:id 2 :name :b}])
(groups :id [{:id 1 :name :a}
{:id 2 :name :b}
{:id 1 :name :c}]))"
[f coll]
(vals (group-by f coll)))
(defn separate-2
"Separate `coll` into two collections based on boolean value of `f`.
Returns a pair [those-that-satisfy-f those-that-do-not-satisfy-f].
Will always return a pair, so expect `nil` when one or both values to not satifsy `f`.
Guarantees stable order of each collection."
[pred coll]
(let [g (group-by (comp boolean pred) coll)]
[(get g true) (get g false)]))
(defn adjacent-pairs
"Returns pairs of adjacent (to the right) elements in `coll`.
Example:
(= [[1 2] [2 3]]
(adjacent-pairs [1 2 3]))"
[coll]
(mapv vector coll (rest coll)))
(defn combinations-2
"Returns a lazy sequence of all possible pairs in `coll`.
Returns `nil` if no pairs exist."
[coll]
(when (seq coll)
(let [[x & rst] coll
other (lazy-seq (combinations-2 rst))]
(seq
(lazy-cat (map vector (repeat x) rst)
other)))))
(defn splits-by
"Splits `coll` into multiple groups each time `split-fn` returns `true`."
[split-fn coll]
(loop [coll coll
splits []]
(if-not (seq coll)
splits
(let [[f l] (split-with (complement split-fn) coll)]
(recur (rest l) (conj splits f))))))
(ns my.company.coll-test
(:require
[clojure.test :refer [deftest is testing]]
[clojure.test.check.clojure-test :refer [defspec]]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop]
[my.company.coll :as sut]))
(deftest coll-any?
(is (false? (sut/any? identity nil)))
(is (false? (sut/any? identity [])))
(is (false? (sut/any? identity [nil false])))
(is (false? (sut/any? even? [1 3 5])))
(is (false? (sut/any? false? [true true])))
(is (true? (sut/any? odd? [1])))
(is (true? (sut/any? nil? [1 nil 3])))
(is (true? (sut/any? false? [true true false])))
(is (true? (sut/any? identity [1 2 3]))))
(defspec includes?-always-returns-a-boolean
(prop/for-all
[x gen/any
coll (gen/one-of [(gen/vector gen/any)
(gen/list gen/any)])]
(boolean? (sut/includes? x coll))))
(defspec includes?-must-return-true-if-collection-includes-element
(prop/for-all
[x gen/any
coll (gen/one-of [(gen/vector gen/any)
(gen/list gen/any)])]
(true? (sut/includes? x (conj coll x)))))
(deftest includes?
(is (false? (sut/includes? 1 nil)))
(is (false? (sut/includes? 1 [])))
(is (false? (sut/includes? 1 [2])))
(is (false? (sut/includes? 1 [:a :b])))
(is (false? (sut/includes? nil [:a :b])))
(is (true? (sut/includes? 1 [1 2 3])))
(is (true? (sut/includes? 1 [3 2 1])))
(is (true? (sut/includes? :a [:b :a :c])))
(is (true? (sut/includes? nil [1 nil 3]))))
(deftest elem
(is (nil? (sut/elem some? nil)))
(is (nil? (sut/elem some? [])))
(is (nil? (sut/elem some? [nil])))
(is (nil? (sut/elem true? [false])))
(is (true? (sut/elem true? [false true])))
(is (= 1 (sut/elem #(= 1 %) [1 2 3])))
(is (= 1 (sut/elem #(= 1 %) [3 2 1])))
(is (= :a (sut/elem #(= :a %) [:b :a :c])))
(is (= :a (sut/elem #(= :a %) [:b :a :c :a :e :a]))))
(defspec groups-size-returned<=coll-size
(prop/for-all
[coll (gen/one-of [(gen/vector gen/any)
(gen/list gen/any)])]
(<= (count (sut/groups some? coll))
(count coll))))
(defspec groups-stable-order-inside-each-group
(prop/for-all
[coll (gen/fmap sort (gen/list gen/large-integer))]
(every? #(= % (sort %))
(sut/groups pos-int? coll))))
(deftest groups
(is (nil? (sut/groups some? nil)))
(is (nil? (sut/groups some? [])))
(is (= '([1]) (sut/groups some? [1])))
(is (= '([1 2]) (sut/groups some? [1 2])))
(is (= '([1 2] [nil]) (sut/groups some? [1 nil 2])))
(is (= '([nil] [1 2]) (sut/groups some? [nil 1 2])))
(is (= '([{:id 1 :name :a} {:id 1 :name :c}]
[{:id 2 :name :b}])
(sut/groups :id [{:id 1 :name :a}
{:id 2 :name :b}
{:id 1 :name :c}]))))
(defspec separate-2-always-returns-a-pair
(prop/for-all
[coll (gen/one-of [(gen/vector gen/any)
(gen/list gen/any)
(gen/set gen/any)])]
(= 2 (count (sut/separate-2 some? coll)))))
(defspec separate-2-stable-order-inside-each-group
(prop/for-all
[coll (gen/fmap sort (gen/list gen/large-integer))]
(let [[a b] (sut/separate-2 pos-int? coll)]
(and (= a (seq (sort a)))
(= b (seq (sort b)))))))
(defspec separate-2-works-even-if-pred-doesnt-return-a-boolean
(prop/for-all
[coll (gen/one-of [(gen/vector gen/any)
(gen/list gen/any)
(gen/set gen/any)])]
(let [[a b] (sut/separate-2 some? coll)]
(and (every? some? a)
(every? nil? b)))))
(deftest separate-2
(is (= [nil nil] (sut/separate-2 some? nil)))
(is (= [nil nil] (sut/separate-2 some? [])))
(is (= ['(1) nil] (sut/separate-2 some? [1])))
(is (= [nil '(nil)] (sut/separate-2 some? [nil])))
(is (= ['(:a) '(1)] (sut/separate-2 keyword? [:a 1])))
(is (= ['(:a :b :c) '(1 2 3)] (sut/separate-2 keyword? [:a 1 :b 2 :c 3]))))
(deftest adjacent-pairs
(is (= [] (sut/adjacent-pairs nil)))
(is (= [] (sut/adjacent-pairs [])))
(is (= [] (sut/adjacent-pairs [1])))
(is (= [[1 2]] (sut/adjacent-pairs [1 2])))
(is (= [[1 2] [2 3]] (sut/adjacent-pairs [1 2 3])))
(is (= [[1 2] [2 3] [3 4]] (sut/adjacent-pairs [1 2 3 4]))))
(deftest combinations-2
(is (nil? (sut/combinations-2 nil)))
(is (nil? (sut/combinations-2 [])))
(is (nil? (sut/combinations-2 [1])))
(is (= '([1 2]) (sut/combinations-2 [1 2])))
(is (= '([1 2] [1 3] [2 3])
(sut/combinations-2 [1 2 3])))
(is (= '([1 2] [1 3] [1 4] [2 3] [2 4] [3 4])
(sut/combinations-2 [1 2 3 4])))
(testing "is lazy"
(is (= '([0 1] [0 2] [0 3] [0 4] [0 5])
(take 5 (sut/combinations-2 (range))))))
(testing "does not consume the stack"
(is (= 100000
(count (take 100000 (sut/combinations-2 (range 1000))))))))
(deftest splits-by
(is (= [] (sut/splits-by keyword? [])))
(is (= [[1 2]] (sut/splits-by keyword? [1 2])))
(is (= [[1 2] [3 4]] (sut/splits-by keyword? [1 2 :here 3 4])))
(is (= [[1] [2] [3] [4]] (sut/splits-by keyword? [1 :here 2 :here 3 :here 4])))
(is (= [[1]] (sut/splits-by keyword? [1 :here])))
(is (= [[1] []] (sut/splits-by keyword? [1 :here :here])))
(is (= [[] [1]] (sut/splits-by keyword? [:here 1])))
(is (= [[] [] [1]] (sut/splits-by keyword? [:here :here 1])))
(is (= [[1] [] [2]] (sut/splits-by keyword? [1 :here :here 2]))))
(ns my.company.inout
"Missing functions in `clojure.java.io`."
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]))
(defn read-bytes [streamable]
(with-open [is (io/input-stream streamable)]
(.readAllBytes is)))
(defn read-edn [readable]
(with-open [r (io/reader readable)]
(edn/read-string (slurp r))))
(ns my.company.kw
"Missing functions in `clojure.core` that accept a keyword."
(:refer-clojure :exclude [str]))
(defn str
"Makes a string out of `k` so that it can be shared to non-clojure world.
Preserves namespace info if present."
[k]
(if (qualified-keyword? k)
(format "%s/%s" (namespace k) (name k))
(name k)))
(ns my.company.kw-test
(:require
[clojure.test :refer [deftest is]]
[my.company.kw :as sut]))
(deftest str-test
(is (= "a" (sut/str :a)))
(is (= "test/a" (sut/str :test/a))))
(ns my.company.map
"Missing functions in `clojure.core` that accept persistent map.
Argument names and expected positions use the same convention as `clojure.core`.")
(defn map-key
"Transform by calling `f` on each key of `m`.
Always returns a new map of the same category as `m`."
[m f]
(->> m
(map (fn [[k v]] [(f k) v]))
(into (empty m))))
(defn map-val
"Transform by calling `f` on each value of `m`.
Always returns a new map of the same category as `m`."
[m f]
(->> m
(map (fn [[k v]] [k (f v)]))
(into (empty m))))
(defn filter-key
"Filter entries of `m` satisfying `(pred k)`.
Always returns a new map of the same category as `m`."
[m pred]
(->> m
(filter (comp pred key))
(into (empty m))))
(defn filter-val
"Filter entries of `m` satisfying `(pred v)`.
Always returns a new map of the same category as `m`."
[m pred]
(->> m
(filter (comp pred val))
(into (empty m))))
(defn remove-key
"Removes entries of `m` satisfying `(pred k)`.
Always returns a new map of the same category as `m`."
[m pred]
(filter-key m (comp not pred)))
(defn remove-val
"Removes entries of `m` satisfying `(pred v)`.
Always returns a new map of the same category as `m`."
[m pred]
(filter-val m (comp not pred)))
(defn remove-keys
"Opposite of `clojure.core/select-keys`."
[m ks]
(apply dissoc m ks))
(defn dissoc-in
"Dissociate a key with path `ks` in a nested associated structure `m`."
[m ks]
(cond
(zero? (count ks)) m
(= 1 (count ks)) (dissoc m (first ks))
:else
(let [[k & rks] ks]
(if (contains? m k)
(update m k #(dissoc-in % rks))
m))))
(defn update-when
"Update the value of `k` in `m` with `f` only when `(pred v)`."
[m k pred f]
(if (pred (get m k))
(update m k f)
m))
(defn update-if
"Update the value of `k` in `m` with `f` only if `k` exists."
[m k f]
(if (contains? m k)
(update m k f)
m))
(defn assoc-if
"Override the value of `k` in `m` with `v` only if `k` exists."
[m k v]
(if (contains? m k)
(assoc m k v)
m))
(defn submap?
"Does `m` contain all keys and their vals of `n`?"
[m n]
(= n (select-keys m (keys n))))
(defn unqualified
"Convert map keys to unqualified keywords.
Assumes map keys are keywords."
[m]
(map-key m (comp keyword name)))
(ns my.company.map-test
(:require
[clojure.test :refer [deftest is]]
[clojure.test.check.clojure-test :refer [defspec]]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop]
[my.company.map :as sut]))
(defspec map-key-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/map-key m identity)))))
(deftest map-key
(is (= {} (sut/map-key {} inc)))
(is (= {1 :b} (sut/map-key {1 :a 2 :b} (constantly 1))))
(is (= {2 :a 3 :b} (sut/map-key {1 :a 2 :b} inc)))
(is (sorted? (sut/map-key (sorted-map :a 1 :b 2) identity))))
(defspec map-val-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/map-val m identity)))))
(deftest map-val
(is (= {} (sut/map-val {} inc)))
(is (= {1 :b} (sut/map-key {1 :a 2 :b} (constantly 1))))
(is (= {:a 2 :b 3} (sut/map-val {:a 1 :b 2} inc)))
(is (sorted? (sut/map-val (sorted-map :a 1 :b 2) identity))))
(defspec filter-key-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/filter-key m some?)))))
(deftest filter-key
(is (= {} (sut/filter-key {} pos-int?)))
(is (= {1 :a} (sut/filter-key {1 :a -1 :b} pos-int?)))
(is (sorted? (sut/filter-key (sorted-map :a 1 :b 2) some?))))
(defspec filter-val-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/filter-val m some?)))))
(deftest filter-val
(is (= {} (sut/filter-val {} pos-int?)))
(is (= {:a 1} (sut/filter-val {:a 1 :b -1} pos-int?)))
(is (sorted? (sut/filter-val (sorted-map :a 1 :b 2) some?))))
(defspec remove-key-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/remove-key m some?)))))
(deftest remove-key
(is (= {} (sut/remove-key {} pos-int?)))
(is (= {-1 :b} (sut/remove-key {1 :a -1 :b} pos-int?)))
(is (sorted? (sut/remove-key (sorted-map :a 1 :b 2) some?))))
(defspec remove-val-works-on-any-map-category
(prop/for-all
[m (gen/hash-map gen/any gen/any)]
(= (type m) (type (sut/remove-val m some?)))))
(deftest remove-val
(is (= {} (sut/remove-val {} pos-int?)))
(is (= {:b -1} (sut/remove-val {:a 1 :b -1} pos-int?)))
(is (sorted? (sut/remove-val (sorted-map :a 1 :b 2) some?))))
(deftest remove-keys
(is (= {} (sut/remove-keys {} #{})))
(is (= {} (sut/remove-keys {:a 1} #{:a})))
(is (= {:a 1} (sut/remove-keys {:a 1} #{})))
(is (= {:a 1} (sut/remove-keys {:a 1} #{:b}))))
(deftest dissoc-in
(is (= {:a 1} (sut/dissoc-in {:a 1} [])))
(is (= {:a 1} (sut/dissoc-in {:a 1} [:z])))
(is (= {:a {:b 2}} (sut/dissoc-in {:a {:b 2}} [:a :z])))
(is (= {:a {:b 2}} (sut/dissoc-in {:a {:b 2}} [:y :z])))
(is (= {:a {:b {:c 3}}} (sut/dissoc-in {:a {:b {:c 3}}} [:a :y :z])))
(is (= {} (sut/dissoc-in {:a 1} [:a])))
(is (= {:b 2} (sut/dissoc-in {:a 1 :b 2} [:a])))
(is (= {:a {}} (sut/dissoc-in {:a {:b 2}} [:a :b])))
(is (= {:a {}} (sut/dissoc-in {:a {:b {:c 3}}} [:a :b])))
(is (= {:a {:b {}}} (sut/dissoc-in {:a {:b {:c 3}}} [:a :b :c]))))
(deftest update-when
(is (= {} (sut/update-when {} :a string? #(str % "abc"))))
(is (= {:a nil} (sut/update-when {:a nil} :a string? #(str % "abc"))))
(is (= {:a 1} (sut/update-when {:a 1} :a string? #(str % "abc"))))
(is (= {:a "Xabc"} (sut/update-when {:a "X"} :a string? #(str % "abc")))))
(deftest update-if
(is (= {} (sut/update-if {} :a inc)))
(is (= {:a 2} (sut/update-if {:a 1} :a inc)))
(is (= {:a "abc"} (sut/update-if {:a nil} :a #(str % "abc")))))
(deftest assoc-if
(is (= {} (sut/assoc-if {} :a 1)))
(is (= {:a 2} (sut/assoc-if {:a 1} :a 2)))
(is (= {:a 1} (sut/assoc-if {:a nil} :a 1))))
(deftest submap?
(is (true? (sut/submap? {} {})))
(is (true? (sut/submap? {:a 1} {})))
(is (true? (sut/submap? {:a 1 :b 2} {:a 1})))
(is (true? (sut/submap? {:a 1 :b 2} {:a 1 :b 2})))
(is (false? (sut/submap? {} {:a 1})))
(is (false? (sut/submap? {:a 1} {:a 2})))
(is (false? (sut/submap? {:a 1} {:b 1}))))
(deftest unqualified
(is (= {} (sut/unqualified {})))
(is (= {:a 1} (sut/unqualified {:a 1})))
(is (= {:a 1} (sut/unqualified {:user/a 1}))))
(ns my.company.number
"Missing functions in `clojure.core` that accept a number."
(:import
(java.math RoundingMode)))
(defn constrain
"Make `x` fit the bounds `minimum` and `maximum`."
[minimum maximum x]
(max minimum (min maximum x)))
(defn set-scale
"Sets the scale of `x` to `n`.
`n` must be positive integer. Always returns a `double`.
See also `BigDecimal/setScale`."
^double [^Integer n x]
(-> x
(double)
(bigdec)
(.setScale n RoundingMode/HALF_UP)
(.doubleValue)))
;; https://en.wikipedia.org/wiki/Foot_(unit)
(def ^:private conversion-factor-ft->cm 30.48)
;; https://en.wikipedia.org/wiki/Pound_(mass)
(def ^:private conversion-factor-lb->kg 0.45359237)
(defn cm->ft [x]
(/ x conversion-factor-ft->cm))
(defn ft->cm [x]
(* x conversion-factor-ft->cm))
(defn kg->lb [x]
(/ x conversion-factor-lb->kg))
(defn lb->kg [x]
(* x conversion-factor-lb->kg))
(ns my.company.number-test
(:require
[clojure.test :refer [deftest is testing]]
[my.company.number :as sut]))
(deftest constrain
(is (= 1 (sut/constrain 1 9 1)))
(is (= 3 (sut/constrain 1 9 3)))
(is (= 9 (sut/constrain 1 9 9)))
(is (= -5 (sut/constrain -9 -1 -5)))
(is (= 0 (sut/constrain -9 9 0)))
(is (= 1 (sut/constrain 1 9 -1)))
(is (= 1 (sut/constrain 1 9 0)))
(is (= 9 (sut/constrain 1 9 10))))
(deftest set-scale
(testing "doubles"
(is (= 0.0 (sut/set-scale 1 0.001)))
(is (= 0.3 (sut/set-scale 1 0.345)))
(is (= 0.5 (sut/set-scale 1 0.456)))
(is (= 0.6 (sut/set-scale 1 0.567)))
(is (= 0.57 (sut/set-scale 2 0.567)))
(is (= 1.35 (sut/set-scale 7 1.35))))
(testing "non doubles"
(is (= 0.0 (sut/set-scale 1 0)))
(is (= 7.0 (sut/set-scale 1 7)))
(is (= 0.33 (sut/set-scale 2 (/ 1 3))))))
(defn- close-to? [x y]
(<= (abs (- x y)) 1e-6))
(deftest convert-cm-and-ft
(doseq [x [0 1 5 10 100 1e6 1e9 133.59 1/2 1/3 1/9 0.5123512]]
(is (close-to? x (sut/ft->cm (sut/cm->ft x))))
(is (close-to? x (sut/cm->ft (sut/ft->cm x))))))
(deftest convert-kg-and-lb
(doseq [x [0 1 5 10 100 1e6 1e9 133.59 1/2 1/3 1/9 0.5123512]]
(is (close-to? x (sut/lb->kg (sut/kg->lb x))))
(is (close-to? x (sut/kg->lb (sut/lb->kg x))))))
(ns my.company.pair
"A pair is a vector of size 2.
Functions here preserve the vector form.")
(defn map-1
"Transform first element of `p` using `f`."
[[a b] f]
[(f a) b])
(defn map-2
"Transform second element of `p` using `f`."
[[a b] f]
[a (f b)])
(defn map-3
"Transform first element of `p` using `f1` and second element of `p` using `f2`."
[[a b] f1 f2]
[(f1 a) (f2 b)])
(ns my.company.pair-test
(:require
[clojure.test :refer [deftest is]]
[my.company.pair :as sut]))
(deftest map-1
(is (= [2 2] (sut/map-1 [1 2] inc))))
(deftest map-2
(is (= [1 3] (sut/map-2 [1 2] inc))))
(ns my.company.pairs
"Pairs is a collection of `pair`."
(:require
[my.company.pair :as pair]))
(defn map-1
"Transform first element of `ps` using `f`."
[f ps]
(map #(pair/map-1 % f) ps))
(defn map-2
"Transform second element of `ps` using `f`."
[f ps]
(map #(pair/map-2 % f) ps))
(defn filter-1
"Filter `ps` where first element satisfies `pred`."
[pred ps]
(filter (fn [[a _]] (pred a)) ps))
(defn filter-2
"Filter `ps` where second element satisfies `pred`."
[pred ps]
(filter (fn [[_ b]] (pred b)) ps))
(defn firsts
"Take the first elements of `ps`."
[ps]
(map first ps))
(defn seconds
"Take the second elements of `ps`."
[ps]
(map second ps))
(ns my.company.pairs-test
(:require
[clojure.test :refer [deftest is]]
[my.company.pairs :as sut]))
(deftest map-1
(is (= [[:a 1] [:b 2]]
(sut/map-1 keyword [["a" 1] ["b" 2]]))))
(deftest map-2
(is (= [[1 :a] [2 :b]]
(sut/map-2 keyword [[1 "a"] [2 "b"]]))))
(deftest filter-1
(is (= [[:a 1]]
(sut/filter-1 #(= :a %) [[:a 1] [:b 2]]))))
(deftest filter-2
(is (= [[:a 1]]
(sut/filter-2 #(= 1 %) [[:a 1] [:b 2]]))))
(deftest firsts
(is (= [:a :b] (sut/firsts [[:a 1] [:b 2]]))))
(deftest seconds
(is (= [1 2] (sut/seconds [[:a 1] [:b 2]]))))
(ns my.company.text
"Missing functions in `clojure.string`.
This namespace deals with Java and Clojure strings, but is not named `string`
in order to avoid the clash with `clojure.string` namespace.
Argument names and expected positions use the same convention as `clojure.string`."
(:refer-clojure :exclude [double? int?])
(:require
[clojure.string :as string])
(:import
(java.net MalformedURLException URL)
(java.time DateTimeException LocalDate Year YearMonth)))
(defn try-as-int
"Try parsing `text` as an integer.
Returns `nil` on failure."
[text]
(try
(Integer/parseInt text)
(catch NumberFormatException _
nil)))
(defn try-as-double
"Try parsing `text` as a double.
Returns `nil` on failure."
[text]
(when text
(try
(Double/parseDouble text)
(catch NumberFormatException _
nil))))
(defn try-as-finite-double
"Try parsing `text` as a finite double.
Returns `nil` on failure. Rejects infinities and not-a-number."
[text]
(when-not (#{"NaN" "Infinity" "-Infinity"} (some-> text string/trim))
(try-as-double text)))
(defn try-as-boolean
"Try parsing `text` as a boolean.
Returns `nil` on failure."
[text]
(let [t (some-> text string/trim)]
(cond
(= "true" t) true
(= "false" t) false
:else nil)))
(defn try-as-year
"Try parsing `text` as a `java.time.Year`.
Returns `nil` on failure."
[text]
(when text
(try
(Year/parse text)
(catch DateTimeException _
nil))))
(defn try-as-year-month
"Try parsing `text` as a `java.time.YearMonth`.
Returns `nil` on failure."
[text]
(when text
(try
(YearMonth/parse text)
(catch DateTimeException _
nil))))
(defn try-as-date
"Try parsing `text` as a `java.time.LocalDate`.
Returns `nil` on failure."
[text]
(when text
(try
(LocalDate/parse text)
(catch DateTimeException _
nil))))
(defn int? [text]
(boolean (try-as-int text)))
(defn double? [text]
(boolean (try-as-finite-double text)))
(defn positive-double? [text]
(let [number (try-as-finite-double text)]
(boolean (and number (pos? number)))))
(defn year? [text]
(boolean (try-as-year text)))
(defn year-month? [text]
(boolean (try-as-year-month text)))
(defn date? [text]
(boolean (try-as-date text)))
(defn url? [text]
(try
(URL. text)
true
(catch MalformedURLException _
false)))
(defn as-number-if-possible
"Best effort coerse `text` into a number or simply return `text` if not possible.
Will try to return the most restrictive number type possible. E.g. an integer
instead of returning a double. Infinities and not-a-number are not considered
as numbers."
[text]
(or (try-as-int text)
(try-as-finite-double text)
text))
(defn ellipsis
"Truncate extra characters from `text` with ..."
[text max-length]
(if (<= (count text) max-length)
text
(let [ellipsis "..."
up-to (max 0 (- max-length (count ellipsis)))]
(-> text
(subs 0 up-to)
(str ellipsis)
(subs 0 max-length)))))
(defn blank->empty [text]
(if (string/blank? text)
""
text))
(ns my.company.text-test
(:refer-clojure :exclude [double? int?])
(:require
[clojure.test :refer [deftest is]]
[my.company.text :as sut])
(:import
(java.time LocalDate Year YearMonth)))
(deftest try-as-int
(is (= -1 (sut/try-as-int "-1")))
(is (= 0 (sut/try-as-int "0")))
(is (= 1 (sut/try-as-int "1")))
(is (= 1 (sut/try-as-int "+1")))
(is (nil? (sut/try-as-int "1.1")))
(is (nil? (sut/try-as-int "1e3")))
(is (nil? (sut/try-as-int "0xFF")))
(is (nil? (sut/try-as-int "NaN")))
(is (nil? (sut/try-as-int "Infinity")))
(is (nil? (sut/try-as-int "-Infinity")))
(is (nil? (sut/try-as-int "abc")))
(is (nil? (sut/try-as-int "abc123")))
(is (nil? (sut/try-as-int "")))
(is (nil? (sut/try-as-int nil))))
(deftest try-as-double
(is (= -1.0 (sut/try-as-double "-1")))
(is (= 0.0 (sut/try-as-double "0")))
(is (= 1.0 (sut/try-as-double "1")))
(is (= 1.0 (sut/try-as-double "+1")))
(is (= -1.1 (sut/try-as-double "-1.1")))
(is (= 1.1 (sut/try-as-double "1.1")))
(is (= 0.1 (sut/try-as-double ".1")))
(is (= -1000.0 (sut/try-as-double "-1e3")))
(is (= 1000.0 (sut/try-as-double "1e3")))
(is (= 1000.0 (sut/try-as-double "+1e3")))
(is (= 0.001 (sut/try-as-double "1e-3")))
(is (= 1.1 (sut/try-as-double " 1.1 ")))
(is (Double/isNaN (sut/try-as-double "NaN")))
(is (Double/isNaN (sut/try-as-double " NaN ")))
(is (Double/isInfinite (sut/try-as-double "Infinity")))
(is (Double/isInfinite (sut/try-as-double " Infinity ")))
(is (Double/isInfinite (sut/try-as-double "-Infinity")))
(is (Double/isInfinite (sut/try-as-double " -Infinity ")))
(is (nil? (sut/try-as-double "1e2.5")))
(is (nil? (sut/try-as-double "nan")))
(is (nil? (sut/try-as-double "infinity")))
(is (nil? (sut/try-as-double "-infinity")))
(is (nil? (sut/try-as-double "0xFF")))
(is (nil? (sut/try-as-double "0xFF.11")))
(is (nil? (sut/try-as-double "abc")))
(is (nil? (sut/try-as-double "abc123")))
(is (nil? (sut/try-as-double "")))
(is (nil? (sut/try-as-double nil))))
(deftest try-as-finite-double
(is (= -1.0 (sut/try-as-finite-double "-1")))
(is (= 0.0 (sut/try-as-finite-double "0")))
(is (= 1.0 (sut/try-as-finite-double "1")))
(is (= 1.0 (sut/try-as-finite-double "+1")))
(is (= -1.1 (sut/try-as-finite-double "-1.1")))
(is (= 1.1 (sut/try-as-finite-double "1.1")))
(is (= 0.1 (sut/try-as-finite-double ".1")))
(is (= -1000.0 (sut/try-as-finite-double "-1e3")))
(is (= 1000.0 (sut/try-as-finite-double "1e3")))
(is (= 1000.0 (sut/try-as-finite-double "+1e3")))
(is (= 0.001 (sut/try-as-finite-double "1e-3")))
(is (= 1.1 (sut/try-as-finite-double " 1.1 ")))
(is (nil? (sut/try-as-finite-double "NaN")))
(is (nil? (sut/try-as-finite-double " NaN ")))
(is (nil? (sut/try-as-finite-double "Infinity")))
(is (nil? (sut/try-as-finite-double " Infinity ")))
(is (nil? (sut/try-as-finite-double "-Infinity")))
(is (nil? (sut/try-as-finite-double " -Infinity ")))
(is (nil? (sut/try-as-finite-double "1e2.5")))
(is (nil? (sut/try-as-finite-double "nan")))
(is (nil? (sut/try-as-finite-double "infinity")))
(is (nil? (sut/try-as-finite-double "-infinity")))
(is (nil? (sut/try-as-finite-double "0xFF")))
(is (nil? (sut/try-as-finite-double "0xFF.11")))
(is (nil? (sut/try-as-finite-double "abc")))
(is (nil? (sut/try-as-finite-double "abc123")))
(is (nil? (sut/try-as-finite-double "")))
(is (nil? (sut/try-as-finite-double nil))))
(deftest try-as-boolean
(is (true? (sut/try-as-boolean "true")))
(is (true? (sut/try-as-boolean " true ")))
(is (false? (sut/try-as-boolean "false")))
(is (false? (sut/try-as-boolean " false ")))
(is (nil? (sut/try-as-boolean nil)))
(is (nil? (sut/try-as-boolean "")))
(is (nil? (sut/try-as-boolean "1")))
(is (nil? (sut/try-as-boolean "0")))
(is (nil? (sut/try-as-boolean "on")))
(is (nil? (sut/try-as-boolean "off")))
(is (nil? (sut/try-as-boolean "yes")))
(is (nil? (sut/try-as-boolean "no")))
(is (nil? (sut/try-as-boolean "abc"))))
(deftest try-as-year
(is (= (Year/of 2020) (sut/try-as-year "2020")))
(is (= (Year/of 2) (sut/try-as-year "2")))
(is (= (Year/of -1) (sut/try-as-year "-1")))
(is (nil? (sut/try-as-year nil)))
(is (nil? (sut/try-as-year "")))
(is (nil? (sut/try-as-year "abc")))
(is (nil? (sut/try-as-year "2020-12")))
(is (nil? (sut/try-as-year "2020-12-31"))))
(deftest try-as-year-month
(is (= (YearMonth/of 2020 1) (sut/try-as-year-month "2020-01")))
(is (nil? (sut/try-as-year-month nil)))
(is (nil? (sut/try-as-year-month "")))
(is (nil? (sut/try-as-year-month "abc")))
(is (nil? (sut/try-as-year-month "2020")))
(is (nil? (sut/try-as-year-month "2020-0")))
(is (nil? (sut/try-as-year-month "2020-2")))
(is (nil? (sut/try-as-year-month "2020-13")))
(is (nil? (sut/try-as-year-month "2020-12-31"))))
(deftest try-as-date
(is (= (LocalDate/of 2020 1 25) (sut/try-as-date "2020-01-25")))
(is (nil? (sut/try-as-date nil)))
(is (nil? (sut/try-as-date "")))
(is (nil? (sut/try-as-date "abc")))
(is (nil? (sut/try-as-date "2020")))
(is (nil? (sut/try-as-date "2020-12")))
(is (nil? (sut/try-as-date "2020-3-2")))
(is (nil? (sut/try-as-date "2020-12-31-4"))))
(deftest as-number-if-possible
(is (= 0 (sut/as-number-if-possible "0")))
(is (= 1 (sut/as-number-if-possible "1")))
(is (= -1 (sut/as-number-if-possible "-1")))
(is (= 1.5 (sut/as-number-if-possible "1.5")))
(is (= -5.9 (sut/as-number-if-possible "-5.9")))
(is (= Integer (type (sut/as-number-if-possible "0"))))
(is (= Double (type (sut/as-number-if-possible "1.1"))))
(is (= "" (sut/as-number-if-possible "")))
(is (= nil (sut/as-number-if-possible nil)))
(is (= "Infinity" (sut/as-number-if-possible "Infinity")))
(is (= "abc" (sut/as-number-if-possible "abc"))))
(deftest int?
(is (true? (sut/int? "1")))
(is (true? (sut/int? "0")))
(is (true? (sut/int? "-1")))
(is (false? (sut/int? "1.0")))
(is (false? (sut/int? "abc"))))
(deftest double?
(is (true? (sut/double? "-1")))
(is (true? (sut/double? "0")))
(is (true? (sut/double? "1")))
(is (true? (sut/double? "+1")))
(is (true? (sut/double? "-1.1")))
(is (true? (sut/double? "1.1")))
(is (true? (sut/double? ".1")))
(is (true? (sut/double? "-1e3")))
(is (true? (sut/double? "1e3")))
(is (true? (sut/double? "+1e3")))
(is (true? (sut/double? "1e-3")))
(is (true? (sut/double? " 1.1 ")))
(is (false? (sut/double? "NaN")))
(is (false? (sut/double? " NaN ")))
(is (false? (sut/double? "Infinity")))
(is (false? (sut/double? " Infinity ")))
(is (false? (sut/double? "-Infinity")))
(is (false? (sut/double? " -Infinity ")))
(is (false? (sut/double? "1e2.5")))
(is (false? (sut/double? "nan")))
(is (false? (sut/double? "infinity")))
(is (false? (sut/double? "-infinity")))
(is (false? (sut/double? "0xFF")))
(is (false? (sut/double? "0xFF.11")))
(is (false? (sut/double? "abc")))
(is (false? (sut/double? "abc123")))
(is (false? (sut/double? "")))
(is (false? (sut/double? nil))))
(deftest positive-double?
(is (true? (sut/positive-double? "1")))
(is (true? (sut/positive-double? "+1")))
(is (true? (sut/positive-double? "1.1")))
(is (true? (sut/positive-double? ".1")))
(is (true? (sut/positive-double? "1e3")))
(is (true? (sut/positive-double? "+1e3")))
(is (true? (sut/positive-double? "1e-3")))
(is (true? (sut/positive-double? " 1.1 ")))
(is (false? (sut/positive-double? "-1")))
(is (false? (sut/positive-double? "0")))
(is (false? (sut/positive-double? "-1.1")))
(is (false? (sut/positive-double? "-1e3")))
(is (false? (sut/positive-double? "NaN")))
(is (false? (sut/positive-double? " NaN ")))
(is (false? (sut/positive-double? "Infinity")))
(is (false? (sut/positive-double? " Infinity ")))
(is (false? (sut/positive-double? "-Infinity")))
(is (false? (sut/positive-double? " -Infinity ")))
(is (false? (sut/positive-double? "1e2.5")))
(is (false? (sut/positive-double? "nan")))
(is (false? (sut/positive-double? "infinity")))
(is (false? (sut/positive-double? "-infinity")))
(is (false? (sut/positive-double? "0xFF")))
(is (false? (sut/positive-double? "0xFF.11")))
(is (false? (sut/positive-double? "abc")))
(is (false? (sut/positive-double? "abc123")))
(is (false? (sut/positive-double? "")))
(is (false? (sut/positive-double? nil))))
(deftest year?
(is (true? (sut/year? "2020")))
(is (false? (sut/year? "abc"))))
(deftest year-month?
(is (true? (sut/year-month? "2020-12")))
(is (false? (sut/year-month? "abc"))))
(deftest date?
(is (true? (sut/date? "2020-12-31")))
(is (false? (sut/date? "abc"))))
(deftest url?
(is (true? (sut/url? "http://company.com")))
(is (true? (sut/url? "https://company.com")))
(is (true? (sut/url? "file:///home/user/Downloads")))
(is (false? (sut/url? "custom://abc")))
(is (false? (sut/url? "abc")))
(is (false? (sut/url? "a:b")))
(is (false? (sut/url? "a//b"))))
(deftest ellipsis
(is (= "" (sut/ellipsis "" 0)))
(is (= "" (sut/ellipsis "" 5)))
(is (= "" (sut/ellipsis "a" 0)))
(is (= "a" (sut/ellipsis "a" 1)))
(is (= "a" (sut/ellipsis "a" 5)))
(is (= "" (sut/ellipsis "abc" 0)))
(is (= "." (sut/ellipsis "abc" 1)))
(is (= ".." (sut/ellipsis "abc" 2)))
(is (= "abc" (sut/ellipsis "abc" 3)))
(is (= "abc" (sut/ellipsis "abc" 5)))
(is (= "" (sut/ellipsis "abcdefghijkl" 0)))
(is (= "." (sut/ellipsis "abcdefghijkl" 1)))
(is (= ".." (sut/ellipsis "abcdefghijkl" 2)))
(is (= "..." (sut/ellipsis "abcdefghijkl" 3)))
(is (= "a..." (sut/ellipsis "abcdefghijkl" 4)))
(is (= "ab..." (sut/ellipsis "abcdefghijkl" 5)))
(is (= "abc..." (sut/ellipsis "abcdefghijkl" 6)))
(is (= "abcd..." (sut/ellipsis "abcdefghijkl" 7)))
(is (= "abcdefg..." (sut/ellipsis "abcdefghijkl" 10)))
(is (= "abcdefgh..." (sut/ellipsis "abcdefghijkl" 11)))
(is (= "abcdefghijkl" (sut/ellipsis "abcdefghijkl" 12)))
(is (= "abcdefghijkl" (sut/ellipsis "abcdefghijkl" 20))))
(deftest blank->empty
(is (= "" (sut/blank->empty "")))
(is (= "" (sut/blank->empty " ")))
(is (= "" (sut/blank->empty "\n")))
(is (= "" (sut/blank->empty "\t")))
(is (= "" (sut/blank->empty " \n \t \r\n ")))
(is (= "a" (sut/blank->empty "a")))
(is (= " a " (sut/blank->empty " a "))))
(ns my.company.uuid
"Missing functions for java.util.UUID."
(:import java.util.UUID))
(defn from-string [s]
(UUID/fromString s))
(defn coerce
"Coerce `x` into java.util.UUID."
[x]
(cond
(string? x) (from-string x)
(uuid? x) x
:else (throw (ex-info "Don't know how to coerce a value into java.util.UUID"
{:value x
:type (type x)}))))
(ns my.company.uuid-test
(:require
[clojure.test :refer [deftest is]]
[my.company.uuid :as sut])
(:import
(clojure.lang ExceptionInfo)))
(deftest from-string
(is (uuid? (sut/from-string "f2fbebd7-2620-47ce-9262-506e4aaad754"))))
(deftest coerce
(is (uuid? (sut/coerce "f2fbebd7-2620-47ce-9262-506e4aaad754")))
(is (uuid? (sut/coerce #uuid "f2fbebd7-2620-47ce-9262-506e4aaad754")))
(is (thrown? IllegalArgumentException (sut/coerce "123")))
(is (thrown? ExceptionInfo (sut/coerce 123))))
@naxels
Copy link

naxels commented Nov 24, 2022

Here's another any? implementation using comp:

;; https://clojuredocs.org/clojure.core/any_q - comment
(def
  ^{:tag Boolean
    :doc "Returns true if (pred x) is logical true for any x in coll,
  else false."
    :arglists '([pred coll])
    :added "1.7"}
  any? (comp boolean some))

Passes the same tests :)

@mbezjak
Copy link
Author

mbezjak commented Nov 25, 2022

Yup (comp boolean some) will do the same thing! 👍

And thanks for mentioning clojure.test.check. I neglected to mention that detail. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment