From 790971122661d97b442bb97943a2ff48061f8d25 Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Sat, 20 Jun 2026 13:13:19 +0530 Subject: [PATCH] compare exact values in BigInteger and BigDecimal Number range checks --- .../routines/BigDecimalValidator.java | 50 +++++++++++++++++++ .../routines/BigIntegerValidator.java | 36 +++++++++++++ .../routines/BigDecimalValidatorTest.java | 18 +++++++ .../routines/BigIntegerValidatorTest.java | 19 +++++++ 4 files changed, 123 insertions(+) diff --git a/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java b/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java index c339e6f5e..1b5ac9253 100644 --- a/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java @@ -157,6 +157,56 @@ public boolean minValue(final BigDecimal value, final double min) { return Double.isFinite(min) ? compareTo(value, min) >= 0 : 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 value to a {@code double} before comparing and so loses precision for a {@code BigDecimal} + * that differs from the bound only beyond double precision. 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) ? toBigDecimal(value).compareTo(toBigDecimal(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 value to a {@code double} before comparing and so loses precision for a {@code BigDecimal} + * that differs from the bound only beyond double precision. 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) ? toBigDecimal(value).compareTo(toBigDecimal(min)) >= 0 : value.doubleValue() >= min.doubleValue(); + } + + private static boolean isFinite(final Number value) { + if (value instanceof Double) { + return Double.isFinite((Double) value); + } + if (value instanceof Float) { + return Float.isFinite((Float) value); + } + return true; + } + + private static BigDecimal toBigDecimal(final Number value) { + return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString()); + } + /** * Converts the parsed value to a {@code BigDecimal}. * diff --git a/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java b/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java index 3535f4e95..c812aa9de 100644 --- a/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java @@ -148,6 +148,42 @@ public boolean minValue(final BigInteger value, final long min) { return value.compareTo(BigInteger.valueOf(min)) >= 0; } + /** + * Check 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 value to a {@code long} before comparing and so loses magnitude for a {@code BigInteger} + * outside the long range.

+ * + * @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 toBigInteger(value).compareTo(toBigInteger(max)) <= 0; + } + + /** + * Check 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 value to a {@code long} before comparing and so loses magnitude for a {@code BigInteger} + * outside the long range.

+ * + * @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 toBigInteger(value).compareTo(toBigInteger(min)) >= 0; + } + + private static BigInteger toBigInteger(final Number value) { + return value instanceof BigInteger ? (BigInteger) value : new BigDecimal(value.toString()).toBigInteger(); + } + /** * Convert the parsed value to a {@code BigInteger}. * diff --git a/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java index 6d47a82c9..db3807a1c 100644 --- a/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java @@ -142,6 +142,24 @@ void testBigDecimalCompareWithinDoubleRange() { assertFalse(validator.isInRange(belowBound, higherBound, higherBound), "isInRange: 2^53 + 3 is below [2^53 + 4, 2^53 + 4]"); } + /** + * The {@link Number} overloads inherited from the superclass must compare the exact value against a + * {@code BigDecimal} bound, not values narrowed to a double, so a difference smaller than double precision + * is not lost. + */ + @Test + void testNumberRangeBeyondDoublePrecision() { + final BigDecimalValidator validator = BigDecimalValidator.getInstance(); + final BigDecimal twoPow53 = BigDecimal.valueOf(2).pow(53); + // 2^53 + 1 collapses onto 2^53 as a double, so the double-based comparison cannot tell them apart + final Number value = twoPow53.add(BigDecimal.ONE); + final Number bound = twoPow53; + assertEquals(value.doubleValue(), bound.doubleValue()); + assertFalse(validator.maxValue(value, bound)); + assertTrue(validator.minValue(value, bound)); + assertFalse(validator.isInRange(value, twoPow53.subtract(BigDecimal.ONE), bound)); + } + /** * Tests isInRange(), minValue(), and maxValue() when a bound is {@link Double#NaN}, {@link Double#POSITIVE_INFINITY} or * {@link Double#NEGATIVE_INFINITY}. diff --git a/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java index b3f0bc8d3..fe9609f3e 100644 --- a/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java @@ -194,4 +194,23 @@ void testMinValueOutsideLongRange() { assertFalse(instance.minValue(belowMin, Long.MIN_VALUE)); assertFalse(instance.minValue(belowMin, Long.MAX_VALUE)); } + + /** + * The {@link Number} overloads inherited from the superclass must compare the exact value, not a value + * narrowed to a long, for BigIntegers outside the long range. + */ + @Test + void testNumberRangeOutsideLongRange() { + final BigIntegerValidator instance = BigIntegerValidator.getInstance(); + final Number min = BigInteger.valueOf(5); + final Number max = BigInteger.valueOf(100); + // 2^63 narrows to Long.MIN_VALUE, which the long-based comparison wrongly reports as below the range + final Number aboveMax = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + assertTrue(instance.minValue(aboveMax, min)); + assertFalse(instance.maxValue(aboveMax, max)); + // 2^64 + 50 narrows to 50, which the long-based comparison wrongly reports as in range + final Number wrapsIntoRange = BigInteger.ONE.shiftLeft(Long.SIZE).add(BigInteger.valueOf(50)); + assertEquals(50L, wrapsIntoRange.longValue()); + assertFalse(instance.isInRange(wrapsIntoRange, min, max)); + } }