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 ea0bd679d..e61cedcd3 100644 --- a/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/BigDecimalValidator.java @@ -18,6 +18,7 @@ package org.apache.commons.validator.routines; import java.math.BigDecimal; +import java.text.DecimalFormat; import java.text.Format; import java.text.NumberFormat; import java.util.Locale; @@ -220,6 +221,28 @@ public boolean minValue(final Number value, final Number min) { return isFinite(value) && isFinite(min) ? compareTo(value, min) >= 0 : value.doubleValue() >= min.doubleValue(); } + /** + * Returns a {@code Format} that parses to a {@code BigDecimal} so the exact value of the input is preserved. + * + *
+ * The superclass leaves {@link DecimalFormat} in its default mode, where {@code parse} yields a {@code Double} for a + * fractional value and so rounds an input carrying more significant digits than a {@code double} can hold. Enabling + * {@link DecimalFormat#setParseBigDecimal(boolean)} keeps the value as a {@code BigDecimal} through parsing. + *
+ * + * @param pattern The pattern used to validate the value against or {@code null} to use the default for the {@link Locale}. + * @param locale The locale to use for the format, system default if null. + * @return The {@code Format} to use. + */ + @Override + protected Format getFormat(final String pattern, final Locale locale) { + final Format format = super.getFormat(pattern, locale); + if (format instanceof DecimalFormat) { + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format; + } + /** * 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 af2acbb69..e6fa5ea84 100644 --- a/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/BigIntegerValidator.java @@ -19,6 +19,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.text.DecimalFormat; import java.text.Format; import java.text.NumberFormat; import java.util.Locale; @@ -183,6 +184,29 @@ public boolean minValue(final Number value, final Number min) { return toBigInteger(value).compareTo(toBigInteger(min)) >= 0; } + /** + * Returns a {@code Format} that parses to a {@code BigDecimal} so the exact value of the input is preserved. + * + *+ * The superclass leaves {@link DecimalFormat} in its default mode, where {@code parse} yields a {@code Double} for a + * value outside the {@code long} range and so rounds an integer carrying more significant digits than a {@code double} + * can hold. Enabling {@link DecimalFormat#setParseBigDecimal(boolean)} keeps the full magnitude through parsing before + * it is converted to a {@code BigInteger}. + *
+ * + * @param pattern The pattern used to validate the value against or {@code null} to use the default for the {@link Locale}. + * @param locale The locale to use for the format, system default if null. + * @return The {@code Format} to use. + */ + @Override + protected Format getFormat(final String pattern, final Locale locale) { + final Format format = super.getFormat(pattern, locale); + if (format instanceof DecimalFormat) { + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format; + } + /** * Converts 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 688350399..4d07ad053 100644 --- a/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/BigDecimalValidatorTest.java @@ -54,7 +54,7 @@ protected void setUp() { // testValid() testNumber = new BigDecimal("1234.5"); final Number testNumber2 = new BigDecimal(".1"); - final Number testNumber3 = new BigDecimal("12345.67899"); + final Number testNumber3 = new BigDecimal("12345.678990"); testZero = new BigDecimal("0"); validStrict = new String[] { "0", "1234.5", "1,234.5", ".1", "12345.678990" }; validStrictCompare = new Number[] { testZero, testNumber, testNumber, testNumber2, testNumber3 }; @@ -205,6 +205,21 @@ void testBigDecimalRangeMinMax() { assertFalse(validator.maxValue(number21, max), "maxValue(A) > max"); } + /** + * A value carrying more significant digits than a {@code double} can hold must be converted exactly. Parsing through a + * {@code double} rounds at about 17 digits, so the result has to come straight from the {@code BigDecimal} parse. + */ + @Test + void testValueBeyondDoublePrecision() { + final BigDecimalValidator validator = BigDecimalValidator.getInstance(); + final BigDecimal expected = new BigDecimal("0.12345678901234567890"); + assertEquals(expected, validator.validate(expected.toPlainString(), Locale.US)); + + // 2^53 + 1 has 16 digits but is the smallest integer a double cannot represent + final BigDecimal unrepresentable = BigDecimal.valueOf(2).pow(53).add(BigDecimal.ONE); + assertEquals(unrepresentable, validator.validate(unrepresentable.toPlainString(), Locale.US)); + } + /** * Test BigDecimalValidator validate Methods */ 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 fe9609f3e..854339b29 100644 --- a/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/BigIntegerValidatorTest.java @@ -92,6 +92,21 @@ void testBigIntegerAboveLongMaxValue() { assertEquals(BigDecimalValidator.getInstance().validate(aboveLongStr, "#").toBigInteger(), resultAboveLong); } + /** + * A value carrying more significant digits than a {@code double} can hold must be converted exactly. Scaling + * {@link Long#MAX_VALUE} up pushes past the roughly 17 significant digits a double can represent, so a result routed + * through a double would be rounded rather than preserved. + */ + @Test + void testBigIntegerExactBeyondDoublePrecision() { + final BigInteger exact = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TEN).add(BigInteger.valueOf(7)); + final String exactStr = exact.toString(); + final BigIntegerValidator instance = BigIntegerValidator.getInstance(); + assertEquals(exact, instance.validate(exactStr, "#")); + // BigInteger and BigDecimal validators must agree on the exact value + assertEquals(BigDecimalValidator.getInstance().validate(exactStr, "#").toBigInteger(), instance.validate(exactStr, "#")); + } + /** * Test a value larger than {@link Long#MAX_VALUE} keeps its magnitude instead of being clamped to {@link Long#MAX_VALUE}. */