-
Notifications
You must be signed in to change notification settings - Fork 6
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
dball
wants to merge
4
commits into
gfredericks:master
Choose a base branch
from
dball:add-number
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}))) | ||
|
||
(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))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))))) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: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 :/