Skip to content
Merged
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 @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
* </p>
*
* @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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* 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}.
* </p>
*
* @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}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down
Loading