Compare commits

..

1 Commits

Author SHA1 Message Date
Lukasz Lenart b7d66e1191 WW-5711 fix(conversion): bound fraction digits when formatting BigDecimal
StringConverter formatted BigDecimal, Double and Float with
maximumFractionDigits set to Integer.MAX_VALUE. That constant arrived
with WW-4871, which fixed round-trip precision loss for double and
float; both of those types are naturally bounded, the widest being
Double.MIN_VALUE at 325 fraction digits.

BigDecimal has no such bound. DecimalFormat honours
maximumFractionDigits literally and pads the fraction out to the
value's full scale, so the length of the formatted output followed the
scale of the value rather than its precision.

Bound the setting to 340. Every double and float value still formats in
full, as does every BigDecimal within that range; beyond it the value is
rounded to the bound. The existing round-trip assertions for
Double.MIN_VALUE (325 fraction digits) and for a BigDecimal slightly
wider than double (326) are untouched and still pass.

Fixes: https://issues.apache.org/jira/browse/WW-5711

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwgeV4TN78ke2hHKTVWAUP
2026-08-31 20:35:24 +02:00
2 changed files with 26 additions and 1 deletions
@@ -36,6 +36,18 @@ import java.util.Objects;
public class StringConverter extends DefaultTypeConverter {
/**
* Upper bound on the number of fraction digits emitted when formatting a number.
* <p>
* Covers every {@code double} and {@code float} value in full - the widest is
* {@link Double#MIN_VALUE} at 325 fraction digits - so the round-trip precision
* introduced by WW-4871 is preserved. Beyond that bound the length of the output
* would follow the scale of the value rather than its precision, so a
* {@link BigDecimal} scaled past this limit is rounded to it.
*/
private static final int MAX_FRACTION_DIGITS = 340;
@Override
public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType) {
String result;
@@ -86,7 +98,7 @@ public class StringConverter extends DefaultTypeConverter {
// TODO: delete this variable and corresponding if statement when jdk fixed java.text.NumberFormat.format's behavior with Float
Object fixedValue = value;
if (value instanceof BigDecimal || value instanceof Double || value instanceof Float) {
format.setMaximumFractionDigits(Integer.MAX_VALUE);
format.setMaximumFractionDigits(MAX_FRACTION_DIGITS);
if (value instanceof Float) {
fixedValue = Double.valueOf(value.toString());
}
@@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.StrutsInternalTestCase;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Locale;
import java.util.Map;
@@ -102,6 +103,18 @@ public class StringConverterTest extends StrutsInternalTestCase {
assertEquals(aBitBiggerThanDouble.substring(0, 309) + "," + aBitBiggerThanDouble.substring(310), value);
}
public void testBigDecimalFractionDigitsAreBounded() throws Exception {
// given
StringConverter converter = new StringConverter();
Map<String, Object> context = createContextWithLocale(new Locale("pl", "PL"));
// when the scale of the value exceeds the supported number of fraction digits
Object value = converter.convertValue(context, null, null, null, new BigDecimal(BigInteger.ONE, 100_000), null);
// then the length of the output is bounded by the converter, not by the scale of the value
assertEquals("0", value);
}
public void testStringArrayToStringConversion() {
// given
StringConverter converter = new StringConverter();