Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading