penpot/common/src/app/common/data.cljc
2024-09-03 14:45:11 +02:00

973 lines
25 KiB
Clojure

;; 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 app.common.data
"A collection if helpers for working with data structures and other
data resources."
(:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration concat mapcat
parse-uuid max min regexp?])
#?(:cljs
(:require-macros [app.common.data]))
(:require
#?(:cljs [cljs.core :as c]
:clj [clojure.core :as c])
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
#?(:cljs [goog.array :as garray])
[app.common.math :as mth]
[clojure.set :as set]
[cuerdas.core :as str]
[linked.map :as lkm]
[linked.set :as lks])
#?(:clj
(:import
linked.set.LinkedSet
linked.map.LinkedMap
java.lang.AutoCloseable)))
(def boolean-or-nil?
(some-fn nil? boolean?))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ordered-set
([] lks/empty-linked-set)
([a] (conj lks/empty-linked-set a))
([a & xs] (apply conj lks/empty-linked-set a xs)))
(defn ordered-map
([] lkm/empty-linked-map)
([k a] (assoc lkm/empty-linked-map k a))
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
(defn ordered-set?
[o]
#?(:cljs (instance? lks/LinkedSet o)
:clj (instance? LinkedSet o)))
(defn ordered-map?
[o]
#?(:cljs (instance? lkm/LinkedMap o)
:clj (instance? LinkedMap o)))
(defn vec2
"Creates a optimized vector compatible type of length 2 backed
internally with MapEntry impl because it has faster access method
for its fields."
[o1 o2]
#?(:clj (clojure.lang.MapEntry. o1 o2)
:cljs (cljs.core/->MapEntry o1 o2 nil)))
#?(:clj
(defmethod print-method clojure.lang.PersistentQueue [q, w]
;; Overload the printer for queues so they look like fish
(print-method '<- w)
(print-method (seq q) w)
(print-method '-< w)))
(defn queue
([] #?(:clj clojure.lang.PersistentQueue/EMPTY :cljs #queue []))
([a] (into (queue) [a]))
([a & more] (into (queue) (cons a more))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures Access & Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn not-empty?
[coll]
(boolean (seq coll)))
(defn editable-collection?
[m]
#?(:clj (instance? clojure.lang.IEditableCollection m)
:cljs (implements? c/IEditableCollection m)))
(defn deep-merge
([a b]
(if (map? a)
(merge-with deep-merge a b)
b))
([a b & rest]
(reduce deep-merge a (cons b rest))))
(defn dissoc-in
[m [k & ks]]
(if ks
(if-let [nextmap (get m k)]
(let [newmap (dissoc-in nextmap ks)]
(if (seq newmap)
(assoc m k newmap)
(dissoc m k)))
m)
(dissoc m k)))
(defn concat-all
"A totally lazy implementation of concat with different call
signature. It works like a flatten with a single level of nesting."
[colls]
(lazy-seq
(let [c (seq colls)
o (first c)
r (rest c)]
(if-let [o (seq o)]
(cons (first o) (concat-all (cons (rest o) r)))
(some-> (seq r) concat-all)))))
(defn mapcat
"A fully lazy version of mapcat."
([f] (c/mapcat f))
([f & colls]
(concat-all (apply map f colls))))
(defn- transient-concat
[c1 colls]
(loop [result (transient c1)
colls colls]
(if colls
(recur (reduce conj! result (first colls))
(next colls))
(persistent! result))))
(defn concat-set
([] #{})
([c1]
(if (set? c1) c1 (into #{} c1)))
([c1 & more]
(if (set? c1)
(transient-concat c1 more)
(transient-concat #{} (cons c1 more)))))
(defn concat-vec
([] [])
([c1]
(if (vector? c1) c1 (into [] c1)))
([c1 & more]
(if (vector? c1)
(transient-concat c1 more)
(transient-concat [] (cons c1 more)))))
(defn enumerate
([items] (enumerate items 0))
([items start]
(loop [idx start
items items
res (transient [])]
(if (empty? items)
(persistent! res)
(recur (inc idx)
(rest items)
(conj! res [idx (first items)]))))))
(defn group-by
([kf coll] (group-by kf identity [] coll))
([kf vf coll] (group-by kf vf [] coll))
([kf vf iv coll]
(let [conj (fnil conj iv)]
(reduce (fn [result item]
(update result (kf item) conj (vf item)))
{}
coll))))
(defn seek
([pred coll]
(seek pred coll nil))
([pred coll not-found]
(reduce (fn [_ x]
(if (pred x)
(reduced x)
not-found))
not-found coll)))
(defn index-by
"Return a indexed map of the collection keyed by the result of
executing the getter over each element of the collection."
([kf coll] (index-by kf identity coll))
([kf vf coll]
(persistent!
(reduce #(assoc! %1 (kf %2) (vf %2)) (transient {}) coll))))
(defn index-of-pred
[coll pred]
(loop [c (first coll)
coll (rest coll)
index 0]
(if (nil? c)
nil
(if (pred c)
index
(recur (first coll)
(rest coll)
(inc index))))))
(defn index-of
[coll v]
(index-of-pred coll #(= % v)))
(defn replace-by-id
([value]
(map (fn [item]
(if (= (:id item) (:id value))
value
item))))
([coll value]
(sequence (replace-by-id value) coll)))
(defn vec-without-nils
[coll]
(into [] (remove nil?) coll))
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
([]
(remove (comp nil? val)))
([data]
(reduce-kv (fn [data k v]
(if (nil? v)
(dissoc data k)
data))
data
data)))
(defn without-qualified
([]
(remove (comp qualified-keyword? key)))
([data]
(reduce-kv (fn [data k _]
(if (qualified-keyword? k)
(dissoc data k)
data))
data
data)))
(defn without-keys
"Return a map without the keys provided
in the `keys` parameter."
[data keys]
(if (editable-collection? data)
(persistent! (reduce dissoc! (transient data) keys))
(reduce dissoc data keys)))
(defn patch-object
"Changes is some attributes that need to change in object.
When the attribute is nil it will be removed.
For example
- object: {:a 1 :b {:foo 1 :bar 2} :c 10}
- changes: {:a 2 :b {:foo nil :k 3}}
- result: {:a 2 :b {:bar 2 :k 3} :c 10}
"
([changes]
#(patch-object % changes))
([object changes]
(if object
(->> changes
(reduce-kv
(fn [object key value]
(cond
(map? value)
(let [current (get object key)]
(assoc object key (patch-object current value)))
(and (nil? value) (record? object))
(assoc object key nil)
(nil? value)
(dissoc object key value)
:else
(assoc object key value)))
object))
changes)))
(defn remove-at-index
"Takes a vector and returns a vector with an element in the
specified index removed."
[v index]
;; The subvec function returns a SubVector type that is an vector
;; but does not have transient impl, because of this, we need to
;; pass an explicit vector as first argument.
(concat-vec []
(subvec v 0 index)
(subvec v (inc index))))
(defn without-obj
"Clear collection from specified obj and without nil values."
[coll o]
(into [] (filter #(not= % o)) coll))
(defn zip [col1 col2]
(map vector col1 col2))
(defn zip-all
"Return a zip of both collections, extended to the lenght of the longest one,
and padding the shorter one with nils as needed."
[col1 col2]
(let [diff (- (count col1) (count col2))]
(cond (pos? diff) (zip col1 (c/concat col2 (repeat nil)))
(neg? diff) (zip (c/concat col1 (repeat nil)) col2)
:else (zip col1 col2))))
(defn mapm
"Map over the values of a map"
([mfn]
(map (fn [[key val]] (vec2 key (mfn key val)))))
([mfn coll]
(reduce-kv (fn [coll k v]
(assoc coll k (mfn k v)))
coll
coll)))
(defn removev
"Returns a vector of the items in coll for which (fn item) returns logical false"
[fn coll]
(filterv (comp not fn) coll))
(defn filterm
"Filter values of a map that satisfy a predicate"
[pred coll]
(into {} (filter pred) coll))
(defn removem
"Remove values of a map that satisfy a predicate"
[pred coll]
(into {} (remove pred) coll))
(defn map-perm
"Maps a function to each pair of values that can be combined inside the
function without repetition.
Optional parameters:
`pred?` A predicate that if not satisfied won't process the pair
`target?` A collection that will be used as seed to be stored
Example:
(map-perm vector [1 2 3 4]) => [[1 2] [1 3] [1 4] [2 3] [2 4] [3 4]]"
([mfn coll]
(map-perm mfn (constantly true) [] coll))
([mfn pred? coll]
(map-perm mfn pred? [] coll))
([mfn pred? target coll]
(loop [result (transient target)
current (first coll)
coll (rest coll)]
(if (not current)
(persistent! result)
(let [result
(loop [result result
other (first coll)
coll (rest coll)]
(if (not other)
result
(recur (cond-> result
(pred? current other)
(conj! (mfn current other)))
(first coll)
(rest coll))))]
(recur result
(first coll)
(rest coll)))))))
(defn join
"Returns a new collection with the cartesian product of both collections.
For example:
(join [1 2 3] [:a :b]) => ([1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b])
You can pass a function to merge the items. By default is `vector`:
(join [1 2 3] [1 10 100] *) => (1 10 100 2 20 200 3 30 300)"
([col1 col2] (join col1 col2 vector []))
([col1 col2 join-fn] (join col1 col2 join-fn []))
([col1 col2 join-fn acc]
(cond
(empty? col1) acc
(empty? col2) acc
:else (recur (rest col1) col2 join-fn
(let [other (mapv (partial join-fn (first col1)) col2)]
(c/concat acc other))))))
(def sentinel
#?(:clj (Object.)
:cljs (js/Object.)))
(defn getf
"Returns a function to access a map"
[coll]
(partial get coll))
(defn update-vals
[m f]
(reduce-kv (fn [acc k v]
(assoc acc k (f v)))
m
m))
(defn update-in-when
[m key-seq f & args]
(let [found (get-in m key-seq sentinel)]
(if-not (identical? sentinel found)
(assoc-in m key-seq (apply f found args))
m)))
(defn update-when
[m key f & args]
(let [found (get m key sentinel)]
(if-not (identical? sentinel found)
(assoc m key (apply f found args))
m)))
(defn assoc-in-when
[m key-seq v]
(let [found (get-in m key-seq sentinel)]
(if-not (identical? sentinel found)
(assoc-in m key-seq v)
m)))
(defn assoc-when
[m key v]
(let [found (get m key sentinel)]
(if-not (identical? sentinel found)
(assoc m key v)
m)))
(defn domap
"A side effect map version."
([f]
(map (fn [x] (f x) x)))
([f coll]
(map (fn [x] (f x) x) coll)))
(defn merge
[& maps]
(reduce conj (or (first maps) {}) (rest maps)))
(defn txt-merge
"Text attrs specific merge function."
[obj attrs]
(reduce-kv (fn [obj k v]
(if (nil? v)
(dissoc obj k)
(assoc obj k v)))
obj
attrs))
(defn distinct-xf
[f]
(fn [rf]
(let [seen (volatile! #{})]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [input* (f input)]
(if (contains? @seen input*)
result
(do (vswap! seen conj input*)
(rf result input)))))))))
(defn with-next
"Given a collection will return a new collection where each element
is paired with the next item in the collection
(with-next (range 5)) => [[0 1] [1 2] [2 3] [3 4] [4 nil]]"
[coll]
(map vector
coll
(c/concat (rest coll) [nil])))
(defn with-prev
"Given a collection will return a new collection where each element
is paired with the previous item in the collection
(with-prev (range 5)) => [[0 nil] [1 0] [2 1] [3 2] [4 3]]"
[coll]
(map vector
coll
(c/cons nil coll)))
(defn with-prev-next
"Given a collection will return a new collection where every item is paired
with the previous and the next item of a collection
(with-prev-next (range 5)) => [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]]"
[coll]
(map vector
coll
(c/cons nil coll)
(c/concat (rest coll) [nil])))
(defn deep-mapm
"Applies a map function to an associative map and recurses over its children
when it's a vector or a map"
[mfn m]
(let [do-map
(fn [entry]
(let [[k v] (mfn entry)]
(cond
(or (vector? v) (map? v))
[k (deep-mapm mfn v)]
:else
(mfn [k v]))))]
(cond
(map? m)
(into {} (map do-map) m)
(vector? m)
(into [] (map (partial deep-mapm mfn)) m)
:else
m)))
(defn iteration
"Creates a totally lazy seqable via repeated calls to step, a
function of some (continuation token) 'k'. The first call to step
will be passed initk, returning 'ret'. If (somef ret) is true, (vf
ret) will be included in the iteration, else iteration will
terminate and vf/kf will not be called. If (kf ret) is non-nil it
will be passed to the next step call, else iteration will terminate.
This can be used e.g. to consume APIs that return paginated or batched data.
step - (possibly impure) fn of 'k' -> 'ret'
:somef - fn of 'ret' -> logical true/false, default 'some?'
:vf - fn of 'ret' -> 'v', a value produced by the iteration, default 'identity'
:kf - fn of 'ret' -> 'next-k' or nil (signaling 'do not continue'), default 'identity'
:initk - the first value passed to step, default 'nil'
It is presumed that step with non-initk is
unreproducible/non-idempotent. If step with initk is unreproducible
it is on the consumer to not consume twice."
[& args]
(->> (apply c/iteration args)
(concat-all)))
(defn add-at-index
"Insert an element in a vector at an arbitrary index"
[coll index element]
(assert (vector? coll))
(let [[before after] (split-at index coll)]
(concat-vec [] before [element] after)))
(defn insert-at-index
"Insert a list of elements at the given index of a previous list.
Replace all existing elems."
[elems index new-elems]
(let [[before after] (split-at index elems)
p? (set new-elems)]
(concat-vec []
(remove p? before)
new-elems
(remove p? after))))
(defn addm-at-index
"Insert an element in an ordered map at an arbitrary index"
[coll index key element]
(assert (ordered-map? coll))
(-> (ordered-map)
(into (take index coll))
(assoc key element)
(into (drop index coll))))
(defn insertm-at-index
"Insert a map {k v} of elements in an ordered map at an arbitrary index"
[coll index new-elems]
(assert (ordered-map? coll))
(-> (ordered-map)
(into (take index coll))
(into new-elems)
(into (drop index coll))))
(defn adds-at-index
"Insert an element in an ordered set at an arbitrary index"
[coll index element]
(assert (ordered-set? coll))
(-> (ordered-set)
(into (take index coll))
(conj element)
(into (drop index coll))))
(defn inserts-at-index
"Insert a list of elements in an ordered set at an arbitrary index"
[coll index new-elems]
(assert (ordered-set? coll))
(-> (ordered-set)
(into (take index coll))
(into new-elems)
(into (drop index coll))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn nan?
[v]
#?(:cljs (js/isNaN v)
:clj (not= v v)))
(defn- impl-parse-integer
[v]
#?(:cljs (js/parseInt v 10)
:clj (try
(Integer/parseInt v)
(catch Throwable _
nil))))
(defn- impl-parse-double
[v]
#?(:cljs (js/parseFloat v)
:clj (try
(Double/parseDouble v)
(catch Throwable _
nil))))
(defn parse-integer
([v]
(parse-integer v nil))
([v default]
(let [v (impl-parse-integer v)]
(if (or (nil? v) (nan? v))
default
v))))
(defn parse-double
([v]
(parse-double v nil))
([v default]
(let [v (impl-parse-double v)]
(if (or (nil? v) (nan? v))
default
v))))
(defn parse-uuid
[v]
(try
(c/parse-uuid v)
(catch #?(:clj Throwable :cljs :default) _
nil)))
(defn num-string? [v]
;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
#?(:cljs (and (string? v)
(not (js/isNaN v))
(not (js/isNaN (parse-double v))))
:clj (not= (parse-double v :nan) :nan)))
(defn read-string
[v]
(r/read-string v))
(defn coalesce-str
[val default]
(if (or (nil? val) (nan? val))
default
(str val)))
(defn coalesce
[val default]
(or val default))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Utilities
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn regexp?
"Return `true` if `x` is a regexp pattern
instance."
[x]
#?(:cljs (cljs.core/regexp? x)
:clj (instance? java.util.regex.Pattern x)))
(defn nilf
"Returns a new function that if you pass nil as any argument will
return nil"
[f]
(fn [& args]
(if (some nil? args)
nil
(apply f args))))
(defn nilv
"Returns a default value if the given value is nil"
([default]
(map #(nilv % default)))
([v default]
(if (some? v) v default)))
(defn num?
"Checks if a value `val` is a number but not an Infinite or NaN"
([a]
(mth/finite? a))
([a b]
(and ^boolean (mth/finite? a)
^boolean (mth/finite? b)))
([a b c]
(and ^boolean (mth/finite? a)
^boolean (mth/finite? b)
^boolean (mth/finite? c)))
([a b c d]
(and ^boolean (mth/finite? a)
^boolean (mth/finite? b)
^boolean (mth/finite? c)
^boolean (mth/finite? d)))
([a b c d & others]
(and ^boolean (mth/finite? a)
^boolean (mth/finite? b)
^boolean (mth/finite? c)
^boolean (mth/finite? d)
^boolean (every? mth/finite? others))))
(defn safe+
[a b]
(if (mth/finite? a) (+ a b) a))
(defn max
([a] a)
([a b] (mth/max a b))
([a b c] (mth/max a b c))
([a b c d] (mth/max a b c d))
([a b c d e] (mth/max a b c d e))
([a b c d e f] (mth/max a b c d e f))
([a b c d e f & other]
(reduce max (mth/max a b c d e f) other)))
(defn min
([a] a)
([a b] (mth/min a b))
([a b c] (mth/min a b c))
([a b c d] (mth/min a b c d))
([a b c d e] (mth/min a b c d e))
([a b c d e f] (mth/min a b c d e f))
([a b c d e f & other]
(reduce min (mth/min a b c d e f) other)))
(defn check-num
"Function that checks if a number is nil or nan. Will return 0 when not
valid and the number otherwise."
([v] (mth/finite v 0))
([v default] (mth/finite v default)))
(defn any-key? [element & rest]
(some #(contains? element %) rest))
(defn name
"Improved version of name that won't fail if the input is not a keyword"
[maybe-keyword]
(cond
(nil? maybe-keyword)
nil
(keyword? maybe-keyword)
(c/name maybe-keyword)
(string? maybe-keyword)
maybe-keyword
:else
(str maybe-keyword)))
(defn prefix-keyword
"Given a keyword and a prefix will return a new keyword with the prefix attached
(prefix-keyword \"prefix\" :test) => :prefix-test"
[prefix kw]
(let [prefix (if (keyword? prefix) (name prefix) prefix)
kw (if (keyword? kw) (name kw) kw)]
(keyword (str prefix kw))))
(defn tap
"Similar to the tap in rxjs but for plain collections"
[f coll]
(f coll)
coll)
(defn tap-r
"Same but with args reversed, for -> threads"
[coll f]
(f coll)
coll)
(defn map-diff
"Given two maps returns the diff of its attributes in a map where
the keys will be the attributes that change and the values the previous
and current value. For attributes which value is a map this will be recursive.
For example:
(map-diff {:a 1 :b 2 :c { :foo 1 :var 2}
{:a 2 :c { :foo 10 } :d 10)
=> { :a [1 2]
:b [2 nil]
:c { :foo [1 10]
:var [2 nil]}
:d [nil 10] }
If both maps are identical the result will be an empty map."
[m1 m2]
(let [m1ks (set (keys m1))
m2ks (set (keys m2))
keys (set/union m1ks m2ks)
diff-attr
(fn [diff key]
(let [v1 (get m1 key)
v2 (get m2 key)]
(cond
(= v1 v2)
diff
(and (map? v1) (map? v2))
(assoc diff key (map-diff v1 v2))
:else
(assoc diff key [(get m1 key) (get m2 key)]))))]
(->> keys
(reduce diff-attr {}))))
(defn- extract-numeric-suffix
[basename]
(if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
[p1 (+ 1 (parse-integer p2))]
[basename 1]))
(defn unique-name
"A unique name generator"
([basename used]
(unique-name basename used false))
([basename used prefix-first?]
(assert (string? basename))
(assert (set? used))
(if (> (count basename) 1000)
;; We skip generating names for long strings. If the name is too long the regex can hang
basename
(let [[prefix initial] (extract-numeric-suffix basename)]
(if (and (not prefix-first?)
(not (contains? used basename)))
basename
(loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?)
(str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))))
(defn kebab-keys [m]
(->> m
(deep-mapm
(fn [[k v]]
(if (or (keyword? k) (string? k))
[(keyword (str/kebab (name k))) v]
[k v])))))
(defn toggle-selection
([set value]
(toggle-selection set value false))
([set value toggle?]
(if-not toggle?
(conj (ordered-set) value)
(if (contains? set value)
(disj set value)
(conj set value)))))
(defn lazy-map
"Creates a map with lazy values given the generator function that receives as argument
the key for the value to be generated"
[keys generator-fn]
(into {}
(map (fn [key]
[key (delay (generator-fn key))]))
keys))
(defn opacity-to-hex [opacity]
(let [opacity (* opacity 255)
value (mth/round opacity)]
(.. value
(toString 16)
(padStart 2 "0"))))
(defn unstable-sort
([items]
(unstable-sort compare items))
([comp-fn items]
#?(:cljs
(let [items (to-array items)]
(garray/sort items comp-fn)
(seq items))
:clj
(sort comp-fn items))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; String Functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:const trail-zeros-regex-1 #"\.0+$")
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
#?(:cljs
(defn format-precision
"Creates a number with predetermined precision and then removes the trailing 0.
Examples:
12.0123, 0 => 12
12.0123, 1 => 12
12.0123, 2 => 12.01"
[num precision]
(if (number? num)
(try
(let [num-str (mth/to-fixed num precision)
;; Remove all trailing zeros after the comma 100.00000
num-str (str/replace num-str trail-zeros-regex-1 "")]
;; Remove trailing zeros after a decimal number: 0.001|00|
(if-let [m (re-find trail-zeros-regex-2 num-str)]
(str/replace num-str (first m) (second m))
num-str))
(catch :default _
(str num)))
(str num))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Util protocols
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defprotocol ICloseable
:extend-via-metadata true
(close! [_] "Close the resource."))
#?(:clj
(extend-protocol ICloseable
AutoCloseable
(close! [this] (.close this))))
(defn take-until
"Returns a lazy sequence of successive items from coll until
(pred item) returns true, including that item"
([pred]
(halt-when pred (fn [r h] (conj r h))))
([pred coll]
(transduce (take-until pred) conj [] coll)))
(defn safe-subvec
"Wrapper around subvec so it doesn't throw an exception but returns nil instead"
([v start]
(when (and (some? v)
(> start 0) (< start (count v)))
(subvec v start)))
([v start end]
(let [size (count v)]
(when (and (some? v)
(>= start 0) (< start size)
(>= end 0) (<= start end) (<= end size))
(subvec v start end)))))