Skip to content

Commit abd6bba

Browse files
committed
compare exact values in DoubleValidator and FloatValidator range checks
1 parent ec80353 commit abd6bba

4 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/main/java/org/apache/commons/validator/routines/DoubleValidator.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,42 @@ public boolean minValue(final Double value, final double min) {
183183
return minValue(value.doubleValue(), min);
184184
}
185185

186+
/**
187+
* Tests if the value is less than or equal to a maximum, comparing the exact values.
188+
*
189+
* <p>
190+
* This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses
191+
* precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite
192+
* {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged.
193+
* </p>
194+
*
195+
* @param value The value validation is being performed on.
196+
* @param max The maximum value.
197+
* @return {@code true} if the value is less than or equal to the maximum.
198+
*/
199+
@Override
200+
public boolean maxValue(final Number value, final Number max) {
201+
return isFinite(value) && isFinite(max) ? compareTo(value, max) <= 0 : value.doubleValue() <= max.doubleValue();
202+
}
203+
204+
/**
205+
* Tests if the value is greater than or equal to a minimum, comparing the exact values.
206+
*
207+
* <p>
208+
* This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses
209+
* precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite
210+
* {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged.
211+
* </p>
212+
*
213+
* @param value The value validation is being performed on.
214+
* @param min The minimum value.
215+
* @return {@code true} if the value is greater than or equal to the minimum.
216+
*/
217+
@Override
218+
public boolean minValue(final Number value, final Number min) {
219+
return isFinite(value) && isFinite(min) ? compareTo(value, min) >= 0 : value.doubleValue() >= min.doubleValue();
220+
}
221+
186222
/**
187223
* Convert the parsed value to a {@code Double}.
188224
*

src/main/java/org/apache/commons/validator/routines/FloatValidator.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,42 @@ public boolean minValue(final Float value, final float min) {
183183
return minValue(value.floatValue(), min);
184184
}
185185

186+
/**
187+
* Tests if the value is less than or equal to a maximum, comparing the exact values.
188+
*
189+
* <p>
190+
* This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses
191+
* precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite
192+
* {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged.
193+
* </p>
194+
*
195+
* @param value The value validation is being performed on.
196+
* @param max The maximum value.
197+
* @return {@code true} if the value is less than or equal to the maximum.
198+
*/
199+
@Override
200+
public boolean maxValue(final Number value, final Number max) {
201+
return isFinite(value) && isFinite(max) ? compareTo(value, max) <= 0 : value.doubleValue() <= max.doubleValue();
202+
}
203+
204+
/**
205+
* Tests if the value is greater than or equal to a minimum, comparing the exact values.
206+
*
207+
* <p>
208+
* This overrides the {@link Number} overload inherited from the superclass, which narrows the bound to a {@code double} before comparing and so loses
209+
* precision for a {@code BigDecimal} or {@code BigInteger} bound that carries more significant digits than a {@code double} can hold. A non-finite
210+
* {@link Double} or {@link Float} operand keeps the {@code doubleValue()} comparison so the documented infinity behaviour is unchanged.
211+
* </p>
212+
*
213+
* @param value The value validation is being performed on.
214+
* @param min The minimum value.
215+
* @return {@code true} if the value is greater than or equal to the minimum.
216+
*/
217+
@Override
218+
public boolean minValue(final Number value, final Number min) {
219+
return isFinite(value) && isFinite(min) ? compareTo(value, min) >= 0 : value.doubleValue() >= min.doubleValue();
220+
}
221+
186222
/**
187223
* Perform further validation and convert the {@code Number} to
188224
* a {@code Float}.

src/test/java/org/apache/commons/validator/routines/DoubleValidatorTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import static org.junit.jupiter.api.Assertions.assertNull;
2323
import static org.junit.jupiter.api.Assertions.assertTrue;
2424

25+
import java.math.BigDecimal;
26+
import java.math.BigInteger;
2527
import java.text.DecimalFormatSymbols;
2628
import java.util.Locale;
2729

@@ -132,6 +134,26 @@ void testDoubleRangeMinMaxNaN() {
132134
assertFalse(validator.maxValue(Double.NaN, 20), "maxValue() NaN");
133135
}
134136

137+
/**
138+
* Test the {@link Number} range checks against a bound that carries more precision than a {@code double}.
139+
* 2^53 is the largest integer with an exact {@code double} representation, so 2^53 + 1 cannot be narrowed
140+
* 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.
141+
*/
142+
@Test
143+
void testDoubleNumberRangeExactBound() {
144+
final DoubleValidator validator = (DoubleValidator) strictValidator;
145+
final long maxExactInt = 1L << 53; // 2^53
146+
final Double value = Double.valueOf(maxExactInt);
147+
final BigInteger above = BigInteger.valueOf(maxExactInt).add(BigInteger.ONE); // 2^53 + 1
148+
final BigInteger below = BigInteger.valueOf(maxExactInt).subtract(BigInteger.ONE); // 2^53 - 1
149+
final BigDecimal justBelow = BigDecimal.valueOf(maxExactInt).subtract(BigDecimal.valueOf(0.5)); // 2^53 - 0.5
150+
assertFalse(validator.minValue(value, above), "minValue() bound above value");
151+
assertTrue(validator.minValue(value, below), "minValue() bound below value");
152+
assertFalse(validator.maxValue(value, justBelow), "maxValue() bound below value");
153+
assertTrue(validator.maxValue(value, above), "maxValue() bound above value");
154+
assertFalse(validator.isInRange(value, above, above.add(BigInteger.ONE)), "isInRange() value below range");
155+
}
156+
135157
/**
136158
* Test DoubleValidator validate Methods
137159
*/

src/test/java/org/apache/commons/validator/routines/FloatValidatorTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import static org.junit.jupiter.api.Assertions.assertThrows;
2424
import static org.junit.jupiter.api.Assertions.assertTrue;
2525

26+
import java.math.BigDecimal;
27+
import java.math.BigInteger;
2628
import java.text.DecimalFormat;
2729
import java.text.DecimalFormatSymbols;
2830
import java.text.NumberFormat;
@@ -136,6 +138,28 @@ void testFloatRangeMinMaxNaN() {
136138
assertFalse(validator.maxValue(Float.NaN, 20), "maxValue() NaN");
137139
}
138140

141+
/**
142+
* Test the {@link Number} range checks against a bound that carries more precision than a {@code double}.
143+
* A {@code float} of this magnitude holds the exact decimal value 9.007199E15; integer bounds one unit
144+
* either side of it carry sixteen significant digits, more than a {@code double} can distinguish here
145+
* (its values are two apart), so the exact comparison keeps the value below 9.007199E15 + 1 and above
146+
* 9.007199E15 - 0.5 where narrowing the bound to a {@code double} would not.
147+
*/
148+
@Test
149+
void testFloatNumberRangeExactBound() {
150+
final FloatValidator validator = (FloatValidator) strictValidator;
151+
final Float value = Float.valueOf(9.007199E15f); // exact decimal value of the float
152+
final long exact = 9_007_199_000_000_000L;
153+
final BigInteger above = BigInteger.valueOf(exact).add(BigInteger.ONE); // value + 1
154+
final BigInteger below = BigInteger.valueOf(exact).subtract(BigInteger.ONE); // value - 1
155+
final BigDecimal justBelow = BigDecimal.valueOf(exact).subtract(BigDecimal.valueOf(0.5)); // value - 0.5
156+
assertFalse(validator.minValue(value, above), "minValue() bound above value");
157+
assertTrue(validator.minValue(value, below), "minValue() bound below value");
158+
assertFalse(validator.maxValue(value, justBelow), "maxValue() bound below value");
159+
assertTrue(validator.maxValue(value, above), "maxValue() bound above value");
160+
assertFalse(validator.isInRange(value, above, above.add(BigInteger.ONE)), "isInRange() value below range");
161+
}
162+
139163
/**
140164
* Test Float validation for values too small to handle. (slightly different from max/min which are the largest +ve/-ve
141165
*/

0 commit comments

Comments
 (0)