From abd6bba8b3a0ef84b1b0ae6100371938e9ae43da Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Wed, 24 Jun 2026 11:20:01 +0530 Subject: [PATCH] compare exact values in DoubleValidator and FloatValidator range checks --- .../validator/routines/DoubleValidator.java | 36 +++++++++++++++++++ .../validator/routines/FloatValidator.java | 36 +++++++++++++++++++ .../routines/DoubleValidatorTest.java | 22 ++++++++++++ .../routines/FloatValidatorTest.java | 24 +++++++++++++ 4 files changed, 118 insertions(+) diff --git a/src/main/java/org/apache/commons/validator/routines/DoubleValidator.java b/src/main/java/org/apache/commons/validator/routines/DoubleValidator.java index 1e8c5aa71..a83116006 100644 --- a/src/main/java/org/apache/commons/validator/routines/DoubleValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/DoubleValidator.java @@ -183,6 +183,42 @@ public boolean minValue(final Double value, final double min) { return minValue(value.doubleValue(), min); } + /** + * Tests if the value is less than or equal to a maximum, comparing the exact values. + * + *

+ * This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses + * precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite + * {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged. + *

+ * + * @param value The value validation is being performed on. + * @param max The maximum value. + * @return {@code true} if the value is less than or equal to the maximum. + */ + @Override + public boolean maxValue(final Number value, final Number max) { + return isFinite(value) && isFinite(max) ? compareTo(value, max) <= 0 : value.doubleValue() <= max.doubleValue(); + } + + /** + * Tests if the value is greater than or equal to a minimum, comparing the exact values. + * + *

+ * This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses + * precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite + * {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged. + *

+ * + * @param value The value validation is being performed on. + * @param min The minimum value. + * @return {@code true} if the value is greater than or equal to the minimum. + */ + @Override + public boolean minValue(final Number value, final Number min) { + return isFinite(value) && isFinite(min) ? compareTo(value, min) >= 0 : value.doubleValue() >= min.doubleValue(); + } + /** * Convert the parsed value to a {@code Double}. * diff --git a/src/main/java/org/apache/commons/validator/routines/FloatValidator.java b/src/main/java/org/apache/commons/validator/routines/FloatValidator.java index aa0be6854..850dd91e0 100644 --- a/src/main/java/org/apache/commons/validator/routines/FloatValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/FloatValidator.java @@ -183,6 +183,42 @@ public boolean minValue(final Float value, final float min) { return minValue(value.floatValue(), min); } + /** + * Tests if the value is less than or equal to a maximum, comparing the exact values. + * + *

+ * This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses + * precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite + * {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged. + *

+ * + * @param value The value validation is being performed on. + * @param max The maximum value. + * @return {@code true} if the value is less than or equal to the maximum. + */ + @Override + public boolean maxValue(final Number value, final Number max) { + return isFinite(value) && isFinite(max) ? compareTo(value, max) <= 0 : value.doubleValue() <= max.doubleValue(); + } + + /** + * Tests if the value is greater than or equal to a minimum, comparing the exact values. + * + *

+ * This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses + * precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite + * {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged. + *

+ * + * @param value The value validation is being performed on. + * @param min The minimum value. + * @return {@code true} if the value is greater than or equal to the minimum. + */ + @Override + public boolean minValue(final Number value, final Number min) { + return isFinite(value) && isFinite(min) ? compareTo(value, min) >= 0 : value.doubleValue() >= min.doubleValue(); + } + /** * Perform further validation and convert the {@code Number} to * a {@code Float}. diff --git a/src/test/java/org/apache/commons/validator/routines/DoubleValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/DoubleValidatorTest.java index b3518aa47..b0abc48f4 100644 --- a/src/test/java/org/apache/commons/validator/routines/DoubleValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/DoubleValidatorTest.java @@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.BigDecimal; +import java.math.BigInteger; import java.text.DecimalFormatSymbols; import java.util.Locale; @@ -132,6 +134,26 @@ void testDoubleRangeMinMaxNaN() { assertFalse(validator.maxValue(Double.NaN, 20), "maxValue() NaN"); } + /** + * Test the {@link Number} range checks against a bound that carries more precision than a {@code double}. + * 2^53 is the largest integer with an exact {@code double} representation, so 2^53 + 1 cannot be narrowed + * onto the value: a value of 2^53 is below a minimum of 2^53 + 1 and above a maximum of 2^53 - 0.5. + */ + @Test + void testDoubleNumberRangeExactBound() { + final DoubleValidator validator = (DoubleValidator) strictValidator; + final long maxExactInt = 1L << 53; // 2^53 + 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 = BigDecimal.valueOf(maxExactInt).subtract(BigDecimal.valueOf(0.5)); // 2^53 - 0.5 + assertFalse(validator.minValue(value, above), "minValue() bound above value"); + assertTrue(validator.minValue(value, below), "minValue() bound below value"); + assertFalse(validator.maxValue(value, justBelow), "maxValue() bound below value"); + assertTrue(validator.maxValue(value, above), "maxValue() bound above value"); + assertFalse(validator.isInRange(value, above, above.add(BigInteger.ONE)), "isInRange() value below range"); + } + /** * Test DoubleValidator validate Methods */ diff --git a/src/test/java/org/apache/commons/validator/routines/FloatValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/FloatValidatorTest.java index 29ba5e11a..5f7ad0fbb 100644 --- a/src/test/java/org/apache/commons/validator/routines/FloatValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/FloatValidatorTest.java @@ -23,6 +23,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.math.BigDecimal; +import java.math.BigInteger; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; @@ -136,6 +138,28 @@ void testFloatRangeMinMaxNaN() { assertFalse(validator.maxValue(Float.NaN, 20), "maxValue() NaN"); } + /** + * Test the {@link Number} range checks against a bound that carries more precision than a {@code double}. + * A {@code float} of this magnitude holds the exact decimal value 9.007199E15; integer bounds one unit + * either side of it carry sixteen significant digits, more than a {@code double} can distinguish here + * (its values are two apart), so the exact comparison keeps the value below 9.007199E15 + 1 and above + * 9.007199E15 - 0.5 where narrowing the bound to a {@code double} would not. + */ + @Test + void testFloatNumberRangeExactBound() { + final FloatValidator validator = (FloatValidator) strictValidator; + final Float value = Float.valueOf(9.007199E15f); // exact decimal value of the float + final long exact = 9_007_199_000_000_000L; + final BigInteger above = BigInteger.valueOf(exact).add(BigInteger.ONE); // value + 1 + final BigInteger below = BigInteger.valueOf(exact).subtract(BigInteger.ONE); // value - 1 + final BigDecimal justBelow = BigDecimal.valueOf(exact).subtract(BigDecimal.valueOf(0.5)); // value - 0.5 + assertFalse(validator.minValue(value, above), "minValue() bound above value"); + assertTrue(validator.minValue(value, below), "minValue() bound below value"); + assertFalse(validator.maxValue(value, justBelow), "maxValue() bound below value"); + assertTrue(validator.maxValue(value, above), "maxValue() bound above value"); + assertFalse(validator.isInRange(value, above, above.add(BigInteger.ONE)), "isInRange() value below range"); + } + /** * Test Float validation for values too small to handle. (slightly different from max/min which are the largest +ve/-ve */