From b7d66e1191ed78644eb0bbda16f2b4c23b3ff242 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 31 Aug 2026 20:35:24 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01LwgeV4TN78ke2hHKTVWAUP --- .../struts2/conversion/impl/StringConverter.java | 14 +++++++++++++- .../conversion/impl/StringConverterTest.java | 13 +++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/struts2/conversion/impl/StringConverter.java b/core/src/main/java/org/apache/struts2/conversion/impl/StringConverter.java index 807c82f87c..e79fcf7c41 100644 --- a/core/src/main/java/org/apache/struts2/conversion/impl/StringConverter.java +++ b/core/src/main/java/org/apache/struts2/conversion/impl/StringConverter.java @@ -36,6 +36,18 @@ public class StringConverter extends DefaultTypeConverter { + /** + * Upper bound on the number of fraction digits emitted when formatting a number. + *

+ * 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 context, Object target, Member member, String propertyName, Object value, Class toType) { String result; @@ -86,7 +98,7 @@ protected String convertToString(Locale locale, Object value) { // 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()); } diff --git a/core/src/test/java/org/apache/struts2/conversion/impl/StringConverterTest.java b/core/src/test/java/org/apache/struts2/conversion/impl/StringConverterTest.java index a0310de191..fd3f673ab3 100644 --- a/core/src/test/java/org/apache/struts2/conversion/impl/StringConverterTest.java +++ b/core/src/test/java/org/apache/struts2/conversion/impl/StringConverterTest.java @@ -22,6 +22,7 @@ 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 void testBigDecimalToStringConversionPL() throws Exception { assertEquals(aBitBiggerThanDouble.substring(0, 309) + "," + aBitBiggerThanDouble.substring(310), value); } + public void testBigDecimalFractionDigitsAreBounded() throws Exception { + // given + StringConverter converter = new StringConverter(); + Map 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();