Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add schpec.number #1

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/com/gfredericks/schpec/numbers.clj
Original file line number Diff line number Diff line change
@@ -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})))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you intend this to be just Doubles or other numeric types as well? The generator implies the former while the predicate implies the latter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. If we wanted the latter, we'd need to choose the generator from, what, longs, doubles, bigdecs, bigints? Would that be sufficiently broad, or would we want shorts and ints and floats also?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ratios too probably

I realized after our conversations a couple weeks ago that a big reason I don't see a lot of utility in specs that combine exact and inexact types is that normal equality doesn't work; e.g., (not= 42 42M) and (not= 42M 42.0). So I see the natural classes of numeric types as:

  • fixed-integer < arbitrary-integer < ratio(including integers)
  • bigdec
  • doubles

where any of those five are natural classes to spec out, but combining those three categories is rather less useful.

So all that to say, I think restricting this to doubles makes a lot more sense.

Of course this gets a lot more muddled when we consider clojurescript :/


(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)))))
20 changes: 20 additions & 0 deletions test/com/gfredericks/schpec/numbers_test.clj
Original file line number Diff line number Diff line change
@@ -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)))))