Skip to content
Open
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
5 changes: 5 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Project: Stax2 API
= Releases
------------------------------------------------------------------------

(not yet released)

#53 Fast path for BigDecimal decoding
(contributed by @winfriedgerlach)

4.3.1 (27-Sep-2026)

#41: Fix `ValueEncoderFactory.getScalarEncoder()` fails for long values
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/org/codehaus/stax2/ri/typed/ValueDecoderFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,12 @@ public void decode(char[] lexical, int start, int end) throws IllegalArgumentExc
public static final class DecimalDecoder
extends DecoderBase
{
/**
* Maximum number of digits for which values are parsed as a {@code long} unscaled value first
* (any 18-digit number fits in one)
*/
private static final int MAX_LONG_DIGITS = 18;

protected BigDecimal mValue;

public DecimalDecoder() { }
Expand All @@ -1062,6 +1068,9 @@ public DecimalDecoder() { }
@Override
public void decode(String lexical) throws IllegalArgumentException
{
if (decodeAsLong(lexical)) {
return;
}
try {
mValue = new BigDecimal(lexical);
} catch (NumberFormatException nex) {
Expand All @@ -1072,13 +1081,103 @@ public void decode(String lexical) throws IllegalArgumentException
@Override
public void decode(char[] lexical, int start, int end) throws IllegalArgumentException
{
if (decodeAsLong(lexical, start, end)) {
return;
}
int len = end-start;
try {
mValue = new BigDecimal(lexical, start, len);
} catch (NumberFormatException nex) {
throw constructInvalidValue(new String(lexical, start, len));
}
}

/**
* Fast path for the common case of an optional sign followed by at
* most {@link #MAX_LONG_DIGITS} (ASCII) digits, with an optional
* decimal point among them and no exponent: such values are built
* from a {@code long} unscaled value and a scale, which are the same
* as {@link BigDecimal} would produce. Anything else is left to
* {@link BigDecimal}, including reporting of invalid values.
*
* @return True if the value was decoded; false if not handled
*/
private boolean decodeAsLong(String lexical)
{
final int end = lexical.length();
// Longer than sign, point and all digits: can not be handled
if (end > MAX_LONG_DIGITS + 2) {
return false;
}
int ptr = 0;
boolean neg = false;
if (end > 0) {
char ch = lexical.charAt(0);
if (ch == '-' || ch == '+') {
neg = (ch == '-');
++ptr;
}
}
long value = 0L;
int digits = 0;
int pointIx = -1;
for (; ptr < end; ++ptr) {
char ch = lexical.charAt(ptr);
int digit = ch - '0';
if (digit >= 0 && digit <= 9) {
// may overflow with too many digits; value not used then
value = (value * 10) + digit;
++digits;
} else if (ch == '.' && pointIx < 0) {
pointIx = ptr;
} else {
return false;
}
}
if (digits == 0 || digits > MAX_LONG_DIGITS) {
return false;
}
int scale = (pointIx < 0) ? 0 : (end - pointIx - 1);
mValue = BigDecimal.valueOf(neg ? -value : value, scale);
return true;
}

private boolean decodeAsLong(char[] lexical, int start, final int end)
{
if ((end - start) > MAX_LONG_DIGITS + 2) {
return false;
}
int ptr = start;
boolean neg = false;
if (ptr < end) {
char ch = lexical[ptr];
if (ch == '-' || ch == '+') {
neg = (ch == '-');
++ptr;
}
}
long value = 0L;
int digits = 0;
int pointIx = -1;
for (; ptr < end; ++ptr) {
char ch = lexical[ptr];
int digit = ch - '0';
if (digit >= 0 && digit <= 9) {
value = (value * 10) + digit;
++digits;
} else if (ch == '.' && pointIx < 0) {
pointIx = ptr;
} else {
return false;
}
}
if (digits == 0 || digits > MAX_LONG_DIGITS) {
return false;
}
int scale = (pointIx < 0) ? 0 : (end - pointIx - 1);
mValue = BigDecimal.valueOf(neg ? -value : value, scale);
return true;
}
}

public static final class QNameDecoder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.codehaus.stax2.ri.typed;

import java.math.BigDecimal;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Tests for {@link ValueDecoderFactory.DecimalDecoder}: values with up to 18
* digits and no exponent take a fast path, which must give the same results
* (value and scale) as {@link BigDecimal#BigDecimal(String)}, like all other
* values.
*/
class BigDecimalDecoderTest
{

private static final String[] VALID = {
"0", "-0", "+0", "0.0", "-0.00", "5", "+5", "-5", "007", "007.100",
"1.", ".5", "-.5", "+.5", "12.50", "12345.67", "-98765432.123456",
// longest values for fast path: 18 digits
"999999999999999999", ".999999999999999999",
"-99999999999999999.9", "+123456789.123456789",
// 19 digits and more: BigDecimal only
"1234567890123456789", "9.999999999999999999", "12345678901234567890",
"-0.0000000000000000001", "12345678901234567890.123",
// exponents: BigDecimal only
"1e3", "1E-3", "1.5e+2", "-2.5E10",
// non-ASCII digits (accepted by BigDecimal): U+0663 ARABIC-INDIC DIGIT THREE
"\u0663", "1.\u0663"
};

private static final String[] INVALID = {
"", "-", "+", ".", "-.", "--1", "+-1", "1..2", "1.2.3", "1a", "a1",
"1 2", " 1", "1 ", "0x10", "1e", "e3", "1.5e", "INF", "NaN"
};

private final ValueDecoderFactory.DecimalDecoder decoder = new ValueDecoderFactory().getDecimalDecoder();

@Test
void decodeValidString()
{
for (String value : VALID) {
decoder.decode(value);
// BigDecimal.equals() also compares scale
assertEquals(new BigDecimal(value), decoder.getValue(), value);
}
}

@Test
void decodeValidCharArray()
{
for (String value : VALID) {
// Decoded range is not at the start or end of the array
char[] buf = ("ab" + value + "cd").toCharArray();
decoder.decode(buf, 2, buf.length - 2);
assertEquals(new BigDecimal(value), decoder.getValue(), value);
}
}

@Test
void decodeInvalidString()
{
for (String value : INVALID) {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> decoder.decode(value), value);
assertTrue(e.getMessage().contains("not a valid lexical representation of decimal"),
e.getMessage());
}
}

@Test
void decodeInvalidCharArray()
{
for (String value : INVALID) {
char[] buf = ("ab" + value + "cd").toCharArray();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> decoder.decode(buf, 2, buf.length - 2), value);
assertTrue(e.getMessage().contains("not a valid lexical representation of decimal"),
e.getMessage());
}
}
}
Loading