Skip to content

Compare exact values in DoubleValidator and FloatValidator range checks#410

Open
sahvx655-wq wants to merge 1 commit into
apache:masterfrom
sahvx655-wq:double-float-exact-bound
Open

Compare exact values in DoubleValidator and FloatValidator range checks#410
sahvx655-wq wants to merge 1 commit into
apache:masterfrom
sahvx655-wq:double-float-exact-bound

Conversation

@sahvx655-wq

Copy link
Copy Markdown
Contributor

The Number-typed range overloads on DoubleValidator and FloatValidator are inherited from AbstractNumberValidator, which tests value.doubleValue() against bound.doubleValue(). While reviewing the exact-comparison work already done on the BigInteger and BigDecimal validators I noticed the two floating-point validators still take that narrowing path, so a BigDecimal or BigInteger bound carrying more significant digits than a double can hold is rounded onto the value before the test. minValue(2^53, BigInteger 2^53 + 1) returns true and maxValue(2^53, 2^53 - 0.5) returns true, both wrong: a value that sits outside the bound is reported as in range, and it then slips through any isInRange guard built on these methods.

The two overrides keep the bound as a BigDecimal and compare the value's double against it, so the bound's precision survives; this follows the compareTo comparisons already used in BigIntegerValidator and BigDecimalValidator. A non-finite operand keeps the old doubleValue() comparison so the documented infinity and NaN behaviour is unchanged. Placing the check in the callee means isInRange(Number, Number, Number) picks it up through virtual dispatch rather than each caller needing its own guard.

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request. Which AI tool was used to create this pull request, and to what extent did it contribute?
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

@garydgregory garydgregory changed the title compare exact values in DoubleValidator and FloatValidator range checks Compare exact values in DoubleValidator and FloatValidator range checks Jun 27, 2026

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hello @sahvx655-wq
Please rebase on git master and see my comments.


private static final FloatValidator VALIDATOR = new FloatValidator();

private static boolean isFinite(final Number value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rebase on git master and reuse AbstractNumberValidator.isFinite(Number).


private static final DoubleValidator VALIDATOR = new DoubleValidator();

private static boolean isFinite(final Number value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rebase on git master and reuse AbstractNumberValidator.isFinite(Number).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Rebased on master and dropped the local helper in both DoubleValidator and FloatValidator so they use the inherited isFinite(Number).

final Double value = Double.valueOf(maxExactInt);
final BigInteger above = BigInteger.valueOf(maxExactInt).add(BigInteger.ONE); // 2^53 + 1
final BigInteger below = BigInteger.valueOf(maxExactInt).subtract(BigInteger.ONE); // 2^53 - 1
final BigDecimal justBelow = new BigDecimal(BigInteger.valueOf(maxExactInt)).subtract(BigDecimal.valueOf(0.5)); // 2^53 - 0.5

@garydgregory garydgregory Jun 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this not just BigDecimal.valueOf(maxExactInt) instead of new BigDecimal(BigInteger.valueOf(maxExactInt)) but since this is Double validator instead of a "Big" validator is might be OK...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified to BigDecimal.valueOf(maxExactInt) in the Double test. Same value, less noise.

*/
@Override
public boolean maxValue(final Number value, final Number max) {
return isFinite(value) && isFinite(max) ? BigDecimal.valueOf(value.doubleValue()).compareTo(toBigDecimal(max)) <= 0 : value.doubleValue() <= max.doubleValue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reuse the superclass' compareTo(Number, Number).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched over. Both minValue and maxValue in both validators now call the superclass compareTo(value, bound) instead of building the BigDecimal by hand.

*/
@Override
public boolean minValue(final Number value, final Number min) {
return isFinite(value) && isFinite(min) ? BigDecimal.valueOf(value.doubleValue()).compareTo(toBigDecimal(min)) >= 0 : value.doubleValue() >= min.doubleValue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reuse the superclass' compareTo(Number, Number).

*/
@Override
public boolean maxValue(final Number value, final Number max) {
return isFinite(value) && isFinite(max) ? BigDecimal.valueOf(value.doubleValue()).compareTo(toBigDecimal(max)) <= 0 : value.doubleValue() <= max.doubleValue();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like BigDecimal.valueOf(value.doubleValue()) looses precision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, that was the wrong basis. Going through compareTo(value, max) runs the value through toBigDecimal, which for a Float uses Float.toString, so the float is compared at its own precision (9.007199E15) rather than the extra digits doubleValue() invents when it widens the float. I adjusted the FloatValidatorTest expectations to that value.

@sahvx655-wq sahvx655-wq force-pushed the double-float-exact-bound branch from 6c209e2 to abd6bba Compare June 27, 2026 19:50
@sahvx655-wq

Copy link
Copy Markdown
Contributor Author

Rebased on master and reused the pulled-up helpers throughout: both validators drop the local isFinite copy for the inherited isFinite(Number), and minValue/maxValue now defer to the superclass compareTo(Number, Number) rather than assembling the BigDecimal locally. The test bound is BigDecimal.valueOf(...) now too.

On the float precision point: you were right that BigDecimal.valueOf(value.doubleValue()) was wrong. compareTo runs the value through toBigDecimal, and for a Float that uses Float.toString, so the comparison sits at the float's own decimal value (9.007199E15) instead of the trailing digits doubleValue() fabricates when it widens the float. That moved the FloatValidatorTest expectations onto 9.007199E15, which is the honest bound for a float at that magnitude. mvn clean verify is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants