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 c3b4f5486..5d9a9c22d 100644 --- a/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java @@ -79,6 +79,23 @@ public static BigIntegerValidator getInstance() { return VALIDATOR; } + private static BigDecimal toBigDecimal(final Number value) { + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } + if (value instanceof Long) { + return BigDecimal.valueOf(value.longValue()); + } + if (value instanceof Double) { + // No need to roundtrip with a string. + return BigDecimal.valueOf(((Double) value).doubleValue()); + } + return new BigDecimal(value.toString()); + } + private static BigInteger toBigInteger(final Object value) { if (value instanceof Long) { return BigInteger.valueOf(((Long) value).longValue()); @@ -163,7 +180,8 @@ public boolean maxValue(final BigInteger value, final long max) { * *
* 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. + * magnitude for a {@code BigInteger} outside the long range. The operands are compared as {@code BigDecimal} so a non-integer bound keeps its fractional + * part instead of being truncated towards zero. *
* * @param value The value validation is being performed on. @@ -172,7 +190,7 @@ public boolean maxValue(final BigInteger value, final long max) { */ @Override public boolean maxValue(final Number value, final Number max) { - return toBigInteger(value).compareTo(toBigInteger(max)) <= 0; + return toBigDecimal(value).compareTo(toBigDecimal(max)) <= 0; } /** @@ -191,7 +209,8 @@ public boolean minValue(final BigInteger value, final long min) { * ** 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. + * magnitude for a {@code BigInteger} outside the long range. The operands are compared as {@code BigDecimal} so a non-integer bound keeps its fractional + * part instead of being truncated towards zero. *
* * @param value The value validation is being performed on. @@ -200,7 +219,7 @@ public boolean minValue(final BigInteger value, final long min) { */ @Override public boolean minValue(final Number value, final Number min) { - return toBigInteger(value).compareTo(toBigInteger(min)) >= 0; + return toBigDecimal(value).compareTo(toBigDecimal(min)) >= 0; } /** 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 79e27edb7..b1f1304bb 100644 --- a/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java @@ -22,6 +22,7 @@ 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.util.Locale; @@ -214,4 +215,27 @@ void testNumberRangeOutsideLongRange() { assertEquals(50L, wrapsIntoRange.longValue()); assertFalse(instance.isInRange(wrapsIntoRange, min, max)); } + + /** + * The {@link Number} overloads must compare against the exact bound. A non-integer bound was converted with BigInteger.toBigInteger(), which truncates + * towards zero, so a fractional minimum was floored (wrongly admitting a value below it) and a fractional maximum was floored too (wrongly admitting a value + * above a negative maximum). + */ + @Test + void testNumberRangeFractionalBound() { + final AbstractNumberValidator instance = BigIntegerValidator.getInstance(); + final Number five = BigInteger.valueOf(5); + // 5 >= 5.9 is false, but flooring the bound to 5 made it pass + assertFalse(instance.minValue(five, Double.valueOf(5.9))); + assertFalse(instance.minValue(five, new BigDecimal("5.9"))); + // a whole-number bound is unaffected + assertTrue(instance.minValue(five, BigInteger.valueOf(5))); + + final Number minusFive = BigInteger.valueOf(-5); + // -5 <= -5.9 is false, but flooring the bound to -5 made it pass + assertFalse(instance.maxValue(minusFive, Double.valueOf(-5.9))); + assertFalse(instance.maxValue(minusFive, new BigDecimal("-5.9"))); + // -6 <= -5.9 is true + assertTrue(instance.maxValue(BigInteger.valueOf(-6), Double.valueOf(-5.9))); + } }