From 29a9a8e1299d07846018fdb90cb66873513b3f8c Mon Sep 17 00:00:00 2001 From: Donald Ball Date: Wed, 20 Jul 2016 15:36:20 -0400 Subject: [PATCH 1/4] Add schpec.number --- src/com/gfredericks/schpec/number.clj | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/com/gfredericks/schpec/number.clj diff --git a/src/com/gfredericks/schpec/number.clj b/src/com/gfredericks/schpec/number.clj new file mode 100644 index 0000000..fba5890 --- /dev/null +++ b/src/com/gfredericks/schpec/number.clj @@ -0,0 +1,119 @@ +(ns com.gfredericks.schpec.number + (:require [clojure.spec :as s] + [clojure.spec.gen :as gen]) + (:import [java.math BigDecimal MathContext RoundingMode])) + +(defn real? + [x] + (and (number? x) + (case x + Double/POSITIVE_INFINITY false + Double/NEGATIVE_INFINITY false + Double/NaN false + true))) + +(s/def ::real + (s/spec real? + :gen #(gen/double* {:infinite? false :NaN? false}))) + +(defn decimal-pred + [precision scale] + (fn [d] + (let [d (bigdec 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 decimal-in + "Specs a decimal number. The number type may be anything that bigdec + accepts. 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 (decimal-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 :specs.number.decimal/precision + pos-int?) + +(s/def :specs.number.decimal/scale + (s/spec (fn [x] (and (int? x) (not (neg? x)))) + :gen #(gen/large-integer* {:min 0}))) + +(s/def :specs.number.decimal/min + ::real) + +(s/def :specs.number.decimal/max + ::real) + +(s/fdef decimal-in + :args (s/and (s/keys* :opt-un [:specs.number.decimal/precision + :specs.number.decimal/scale + :specs.number.decimal/min + :specs.number.decimal/max]) + #(let [{:keys [min max precision scale]} % + dec-pred (decimal-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))))) From 9b4096f665b827eac7843e8fb77e41f9c3705d4c Mon Sep 17 00:00:00 2001 From: Donald Ball Date: Sun, 7 Aug 2016 12:26:31 -0400 Subject: [PATCH 2/4] Revise names and semantics --- src/com/gfredericks/schpec/number.clj | 55 ++++++++++++++------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/com/gfredericks/schpec/number.clj b/src/com/gfredericks/schpec/number.clj index fba5890..1d80a38 100644 --- a/src/com/gfredericks/schpec/number.clj +++ b/src/com/gfredericks/schpec/number.clj @@ -3,7 +3,8 @@ [clojure.spec.gen :as gen]) (:import [java.math BigDecimal MathContext RoundingMode])) -(defn real? +(defn finite? + "Returns true if the given value is an actual finite number" [x] (and (number? x) (case x @@ -12,24 +13,22 @@ Double/NaN false true))) -(s/def ::real - (s/spec real? +(s/def ::finite + (s/spec finite? :gen #(gen/double* {:infinite? false :NaN? false}))) -(defn decimal-pred +(defn- bigdec-pred [precision scale] (fn [d] - (let [d (bigdec d)] - (and (or (not precision) - (>= precision (.precision d))) - (or (not scale) - (let [d-scale (.scale d)] - (and (not (neg? d-scale)) - (>= scale d-scale)))))))) + (and (or (not precision) + (>= precision (.precision d))) + (or (not scale) + (let [d-scale (.scale d)] + (and (not (neg? d-scale)) + (>= scale d-scale))))))) -(defn decimal-in - "Specs a decimal number. The number type may be anything that bigdec - accepts. Options: +(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) @@ -45,7 +44,7 @@ does not require both." [& options] (let [{:keys [precision scale min max]} options - dec-pred (decimal-pred precision scale)] + dec-pred (bigdec-pred precision scale)] (letfn [(pred [d] (and (dec-pred d) (or (not min) @@ -78,26 +77,28 @@ :max max})))))] (s/spec pred :gen gen)))) -(s/def :specs.number.decimal/precision +(s/def :specs.number.bigdec/precision pos-int?) -(s/def :specs.number.decimal/scale +(s/def :specs.number.bigdec/scale (s/spec (fn [x] (and (int? x) (not (neg? x)))) :gen #(gen/large-integer* {:min 0}))) -(s/def :specs.number.decimal/min - ::real) +(s/def :specs.number.bigdec/min + (s/and bigdec? + ::finite)) -(s/def :specs.number.decimal/max - ::real) +(s/def :specs.number.bigdec/max + (s/and bigdec? + ::finite)) -(s/fdef decimal-in - :args (s/and (s/keys* :opt-un [:specs.number.decimal/precision - :specs.number.decimal/scale - :specs.number.decimal/min - :specs.number.decimal/max]) +(s/fdef bigdec-in + :args (s/and (s/keys* :opt-un [:specs.number.bigdec/precision + :specs.number.bigdec/scale + :specs.number.bigdec/min + :specs.number.bigdec/max]) #(let [{:keys [min max precision scale]} % - dec-pred (decimal-pred precision scale)] + dec-pred (bigdec-pred precision scale)] (and (or (not (and min max)) (>= max min)) (or (not precision) From 6b0a3fcda92f07080d840892307918866a5dea38 Mon Sep 17 00:00:00 2001 From: Donald Ball Date: Sun, 7 Aug 2016 12:36:56 -0400 Subject: [PATCH 3/4] Fix finite? cases, add example tests --- .../schpec/{number.clj => numbers.clj} | 17 ++++++++--------- test/com/gfredericks/schpec/numbers_test.clj | 12 ++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) rename src/com/gfredericks/schpec/{number.clj => numbers.clj} (94%) create mode 100644 test/com/gfredericks/schpec/numbers_test.clj diff --git a/src/com/gfredericks/schpec/number.clj b/src/com/gfredericks/schpec/numbers.clj similarity index 94% rename from src/com/gfredericks/schpec/number.clj rename to src/com/gfredericks/schpec/numbers.clj index 1d80a38..510f1ba 100644 --- a/src/com/gfredericks/schpec/number.clj +++ b/src/com/gfredericks/schpec/numbers.clj @@ -1,17 +1,16 @@ -(ns com.gfredericks.schpec.number +(ns com.gfredericks.schpec.numbers (:require [clojure.spec :as s] [clojure.spec.gen :as gen]) (:import [java.math BigDecimal MathContext RoundingMode])) -(defn finite? +(def finite? "Returns true if the given value is an actual finite number" - [x] - (and (number? x) - (case x - Double/POSITIVE_INFINITY false - Double/NEGATIVE_INFINITY false - Double/NaN false - true))) + (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? diff --git a/test/com/gfredericks/schpec/numbers_test.clj b/test/com/gfredericks/schpec/numbers_test.clj new file mode 100644 index 0000000..cbd7042 --- /dev/null +++ b/test/com/gfredericks/schpec/numbers_test.clj @@ -0,0 +1,12 @@ +(ns com.gfredericks.schpec.numbers-test + (:require [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)))) From 2a8011967d26bcc2fb06c96f269ba02010efcce0 Mon Sep 17 00:00:00 2001 From: Donald Ball Date: Sun, 7 Aug 2016 14:26:09 -0400 Subject: [PATCH 4/4] Fix keyword ns, add trivial test for bigdec-in --- src/com/gfredericks/schpec/numbers.clj | 16 ++++++++-------- test/com/gfredericks/schpec/numbers_test.clj | 10 +++++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/com/gfredericks/schpec/numbers.clj b/src/com/gfredericks/schpec/numbers.clj index 510f1ba..5cea304 100644 --- a/src/com/gfredericks/schpec/numbers.clj +++ b/src/com/gfredericks/schpec/numbers.clj @@ -76,26 +76,26 @@ :max max})))))] (s/spec pred :gen gen)))) -(s/def :specs.number.bigdec/precision +(s/def :com.gfredericks.schpec.numbers.bigdec-in/precision pos-int?) -(s/def :specs.number.bigdec/scale +(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 :specs.number.bigdec/min +(s/def :com.gfredericks.schpec.numbers.bigdec-in/min (s/and bigdec? ::finite)) -(s/def :specs.number.bigdec/max +(s/def :com.gfredericks.schpec.numbers.bigdec-in/max (s/and bigdec? ::finite)) (s/fdef bigdec-in - :args (s/and (s/keys* :opt-un [:specs.number.bigdec/precision - :specs.number.bigdec/scale - :specs.number.bigdec/min - :specs.number.bigdec/max]) + :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)) diff --git a/test/com/gfredericks/schpec/numbers_test.clj b/test/com/gfredericks/schpec/numbers_test.clj index cbd7042..e3799ab 100644 --- a/test/com/gfredericks/schpec/numbers_test.clj +++ b/test/com/gfredericks/schpec/numbers_test.clj @@ -1,5 +1,6 @@ (ns com.gfredericks.schpec.numbers-test - (:require [clojure.test :refer :all] + (:require [clojure.spec :as s] + [clojure.test :refer :all] [com.gfredericks.schpec.numbers :refer :all])) (deftest test-finite @@ -10,3 +11,10 @@ (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)))))