Skip to content

Fix printDecimal regression for decimal values wider than a long - #96

Merged
pjfanning merged 1 commit into
apache:trunkfrom
czpilar:patches/printdecimal-wider-than-long
Aug 26, 2026
Merged

Fix printDecimal regression for decimal values wider than a long#96
pjfanning merged 1 commit into
apache:trunkfrom
czpilar:patches/printdecimal-wider-than-long

Conversation

@czpilar

@czpilar czpilar commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Regression introduced in 5.4.0: printing any xsd:decimal that has a non-zero scale and does not fit into a long throws IllegalArgumentException("Value can't be converted to long"). 5.3.0 prints the same value correctly.

XsTypeConverter.printDecimal calls MathUtil.toLong(value) as the left operand of an && whose right operand is the "scale < 0" guard. Java evaluates && left to right, so toLong() runs for every value with a non-zero scale. Its result only matters when the scale is negative - in which case the value is integral and always fits into a long - but by then the exception has already been thrown.

5.3.0 used BigDecimal.longValue() on that line, which truncates silently instead of throwing, so the condition was simply false and printing proceeded. MathUtil was added in 5.4.0 and toLong() replaced longValue() here.

Swapping the operands restores the old behaviour without changing the semantics.

Seen in the wild as a WSDL2Java code generation failure: xmlbeans could no longer save a compiled schema type system containing a high-precision decimal.

@pjfanning

Copy link
Copy Markdown
Member

Thanks for the patch and for the clear write-up - the diagnosis is right, and the operand swap is a pure short-circuit reordering with no semantic change (under scale < 0 the value is integral, so toLong(v) == 0 and signum() == 0 agree whenever toLong doesn't throw). It does fix the reported case.

However, I think it's an incomplete fix: the scale < 0 guard doesn't stop toLong() from throwing, it only narrows which values reach it. Negative-scale decimals wider than a long still fail the same way after the patch:

1E+20    scale=-20   -> IllegalArgumentException: Value can't be converted to long
-1E+20   scale=-20   -> IllegalArgumentException: Value can't be converted to long
1E+2000  scale=-2000 -> IllegalArgumentException: BigDecimal magnitude too large to convert safely (limit 1024)

That shape isn't hypothetical - new BigDecimal("100000000000000000000").stripTrailingZeros() is exactly 1E+20, and BigDecimal.valueOf(unscaled, negativeScale) gives the same. Values set programmatically via setBigDecimalValue reach printDecimal that way (the lexer path can't, since lexDecimal rejects exponents).

MathUtil.toLong(value) == 0 is really standing in for "is this value zero" - Harmony's original line was if (scale == 0 || (isZero() && scale < 0)). So I'd suggest using signum() directly instead: same semantics, can't throw, O(1), and it takes MathUtil off the serialization path entirely:

if (scale == 0 || (value.signum() == 0 && scale < 0)) {
    return intStr;
}

I ran the full printDecimal body with that condition:

1E+20      -> 100000000000000000000
-1E+20     -> -100000000000000000000
0E+5       -> 0
0E-5       -> 0.00000
100 / 1E+2 -> 100
123456789012345678901234567890.5 -> 123456789012345678901234567890.5   (both signs)

all correct, including the zero-with-negative-scale case the branch exists for.

On the test: printDecimalHandlesValuesWiderThanLong only covers scale > 0. Worth adding the negative-scale cases, since those are the ones left broken today:

assertEquals("100000000000000000000", XsTypeConverter.printDecimal(new BigDecimal("1E+20")));
assertEquals("-100000000000000000000", XsTypeConverter.printDecimal(new BigDecimal("-1E+20")));
assertEquals("0", XsTypeConverter.printDecimal(new BigDecimal("0E+5")));

Minor: with the signum() version the inline comment can be simplified to just say the branch only needs a zero check, which is what Harmony did.

Regression introduced in 5.4.0: printing an xsd:decimal that does not fit into a
long throws IllegalArgumentException("Value can't be converted to long"). 5.3.0
prints the same value correctly.

XsTypeConverter.printDecimal used MathUtil.toLong(value) == 0 to test whether the
value is zero. toLong() throws for anything wider than a long, so the check blew
up on values it was only ever meant to classify. 5.3.0 used BigDecimal.longValue()
here, which truncates silently; MathUtil was added in 5.4.0 and toLong() replaced
longValue() on this line.

Use signum() instead, as in the original Harmony code this method is derived from:
same semantics, cannot throw, O(1), and it keeps MathUtil off the serialization
path entirely.

Seen in the wild as a WSDL2Java code generation failure: xmlbeans could no longer
save a compiled schema type system containing a high-precision decimal.
@czpilar
czpilar force-pushed the patches/printdecimal-wider-than-long branch from 8dbf28b to 976aec5 Compare August 26, 2026 22:29
@czpilar

czpilar commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

The negative-scale case reproduces exactly as described - I added the three cases as a test against the operand-swap version first, and 1E+20 failed with the same "Value can't be converted to long". So the swap only narrowed the window rather than closing it.

Switched to signum(). Agreed it is the right level to fix this at: the branch is a zero test, signum() states that directly, and it takes MathUtil off the serialization path. Also dropped the && evaluation-order comment, which no longer applies.

Tests now cover positive scale, negative scale and zero with both signs of scale. XsTypeConverterTest (32), TestMathUtil (14), SchemaTypesTests and NumeralsTests all pass.

@pjfanning
pjfanning merged commit e143eb9 into apache:trunk Aug 26, 2026
@pjfanning

Copy link
Copy Markdown
Member

thanks - merged

It looks like we might have a few similar issues. I have a Claude Opus task looking into it.

@czpilar
czpilar deleted the patches/printdecimal-wider-than-long branch August 27, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants