diff --git a/src/com/gfredericks/schpec/numbers.clj b/src/com/gfredericks/schpec/numbers.clj new file mode 100644 index 0000000..5cea304 --- /dev/null +++ b/src/com/gfredericks/schpec/numbers.clj @@ -0,0 +1,119 @@ +(ns com.gfredericks.schpec.numbers + (:require [clojure.spec :as s] + [clojure.spec.gen :as gen]) + (:import [java.math BigDecimal MathContext RoundingMode])) + +(def finite? + "Returns true if the given value is an actual finite number" + (let [falsies #{Double/POSITIVE_INFINITY + Double/NEGATIVE_INFINITY + Double/NaN}] + (fn [x] + (and (number? x) + (not (contains? falsies x)))))) + +(s/def ::finite + (s/spec finite? + :gen #(gen/double* {:infinite? false :NaN? false}))) + +(defn- bigdec-pred + [precision scale] + (fn [d] + (and (or (not precision) + (>= precision (.precision d))) + (or (not scale) + (let [d-scale (.scale d)] + (and (not (neg? d-scale)) + (>= scale d-scale))))))) + +(defn bigdec-in + "Specs a bigdec number. Options: + + :precision - the number of digits in the unscaled value (default none) + :scale - the number of digits to the right of the decimal (default none) + :min - minimum value (inclusive, default none) + :max - maximum value (inclusive, default none) + + A decimal satifies this spec if its precision and scale are not greater + than the specified precision and scale, if given. + + Note that the java math definition of precision and scale may not be the + same as e.g. your database. For example, -1E-75M has a precision of 1 and a + scale of 75. For sanest results, you should specify both, though the spec + does not require both." + [& options] + (let [{:keys [precision scale min max]} options + dec-pred (bigdec-pred precision scale)] + (letfn [(pred [d] + (and (dec-pred d) + (or (not min) + (>= d min)) + (or (not max) + (>= max d)))) + (gen [] + (let [min (or min + (and precision + (-> BigDecimal/ONE + (.movePointRight precision) + dec + .negate))) + max (or max + (and precision + (-> BigDecimal/ONE + (.movePointRight precision) + dec))) + mc (when precision + (MathContext. precision RoundingMode/HALF_UP))] + (letfn [(f [d] + (cond-> (bigdec d) + scale + (.setScale scale BigDecimal/ROUND_HALF_UP) + precision + (.round mc)))] + (gen/fmap f (gen/double* {:infinite? false + :NaN? false + :min min + :max max})))))] + (s/spec pred :gen gen)))) + +(s/def :com.gfredericks.schpec.numbers.bigdec-in/precision + pos-int?) + +(s/def :com.gfredericks.schpec.numbers.bigdec-in/scale + (s/spec (fn [x] (and (int? x) (not (neg? x)))) + :gen #(gen/large-integer* {:min 0}))) + +(s/def :com.gfredericks.schpec.numbers.bigdec-in/min + (s/and bigdec? + ::finite)) + +(s/def :com.gfredericks.schpec.numbers.bigdec-in/max + (s/and bigdec? + ::finite)) + +(s/fdef bigdec-in + :args (s/and (s/keys* :opt-un [:com.gfredericks.schpec.numbers.bigdec-in/precision + :com.gfredericks.schpec.numbers.bigdec-in/scale + :com.gfredericks.schpec.numbers.bigdec-in/min + :com.gfredericks.schpec.numbers.bigdec-in/max]) + #(let [{:keys [min max precision scale]} % + dec-pred (bigdec-pred precision scale)] + (and (or (not (and min max)) + (>= max min)) + (or (not precision) + (pos? precision)) + (or (not scale) + (not (neg? scale))) + (or (not (and precision scale)) + (>= precision scale)) + (or (not min) + (dec-pred min)) + (or (not max) + (dec-pred max))))) + :ret s/spec? + :fn #(let [{:keys [ret args]} % + {:keys [min max]} args] + (and (or (not min) + (s/valid? ret min)) + (or (not max) + (s/valid? ret max))))) diff --git a/test/com/gfredericks/schpec/numbers_test.clj b/test/com/gfredericks/schpec/numbers_test.clj new file mode 100644 index 0000000..e3799ab --- /dev/null +++ b/test/com/gfredericks/schpec/numbers_test.clj @@ -0,0 +1,20 @@ +(ns com.gfredericks.schpec.numbers-test + (:require [clojure.spec :as s] + [clojure.test :refer :all] + [com.gfredericks.schpec.numbers :refer :all])) + +(deftest test-finite + (is (finite? 2)) + (is (finite? 2.0)) + (is (finite? 2M)) + (is (finite? 2N)) + (is (not (finite? Double/POSITIVE_INFINITY))) + (is (not (finite? Double/NEGATIVE_INFINITY))) + (is (not (finite? Double/NaN)))) + +(deftest test-bigdec-in + (let [spec (bigdec-in :min 0M :max 10M :scale 2 :precision 3)] + (is (s/valid? spec 0M)) + (is (s/valid? spec 0.11M)) + (is (s/valid? spec 1.11M)) + (is (not (s/valid? spec 1.111M)))))