Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER to enable faster BigDecimal, BigInteger parsing #851

Merged
merged 12 commits into from
Dec 3, 2022
11 changes: 11 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,17 @@ public enum Feature {
*/
USE_FAST_DOUBLE_PARSER(false),

/**
* Feature that determines whether to use the built-in Java code for parsing
* <code>BigDecimal</code>s and <code>BigIntegers</code>s or to use
* {@link com.fasterxml.jackson.core.io.doubleparser} instead.
*<p>
* This setting is disabled by default for backwards compatibility.
*
* @since 2.15
*/
USE_FAST_BIG_NUMBER_PARSER(false)

;

/**
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/com/fasterxml/jackson/core/StreamReadFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,18 @@ public enum StreamReadFeature
*
* @since 2.14
*/
USE_FAST_DOUBLE_PARSER(JsonParser.Feature.USE_FAST_DOUBLE_PARSER)
USE_FAST_DOUBLE_PARSER(JsonParser.Feature.USE_FAST_DOUBLE_PARSER),

/**
* Feature that determines whether to use the built-in Java code for parsing
* <code>BigDecimal</code>s and <code>BigIntegers</code>s or to use
* {@link com.fasterxml.jackson.core.io.doubleparser} instead.
*<p>
* This setting is disabled by default.
*
* @since 2.15
*/
USE_FAST_BIG_NUMBER_PARSER(JsonParser.Feature.USE_FAST_BIG_NUMBER_PARSER)

;

Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/fasterxml/jackson/core/base/ParserBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,9 @@ protected BigInteger _getBigInteger() {
} else if (_numberString == null) {
throw new IllegalStateException("cannot get BigInteger from current parser state");
}
_numberBigInt = NumberInput.parseBigInteger(_numberString);
_numberBigInt = NumberInput.parseBigInteger(
_numberString,
isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
_numberString = null;
return _numberBigInt;
}
Expand All @@ -1165,7 +1167,9 @@ protected BigDecimal _getBigDecimal() {
} else if (_numberString == null) {
throw new IllegalStateException("cannot get BigDecimal from current parser state");
}
_numberBigDecimal = NumberInput.parseBigDecimal(_numberString);
_numberBigDecimal = NumberInput.parseBigDecimal(
_numberString,
isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
_numberString = null;
return _numberBigDecimal;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fasterxml.jackson.core.io;

import ch.randelshofer.fastdoubleparser.JavaBigDecimalParser;

import java.math.BigDecimal;
import java.util.Arrays;

Expand All @@ -21,7 +23,7 @@
*/
public final class BigDecimalParser
{
private final static int MAX_CHARS_TO_REPORT = 1000;
final static int MAX_CHARS_TO_REPORT = 1000;

private BigDecimalParser() {}

Expand Down Expand Up @@ -60,6 +62,29 @@ public static BigDecimal parse(char[] chars) {
return parse(chars, 0, chars.length);
}

public static BigDecimal parseWithFastParser(final String valueStr) {
pjfanning marked this conversation as resolved.
Show resolved Hide resolved
try {
return JavaBigDecimalParser.parseBigDecimal(valueStr);
} catch (NumberFormatException nfe) {
final String reportNum = valueStr.length() <= MAX_CHARS_TO_REPORT ?
valueStr : valueStr.substring(0, MAX_CHARS_TO_REPORT) + " [truncated]";
throw new NumberFormatException("Value \"" + reportNum
+ "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage());
}
}

public static BigDecimal parseWithFastParser(final char[] ch, final int off, final int len) {
try {
return JavaBigDecimalParser.parseBigDecimal(ch, off, len);
} catch (NumberFormatException nfe) {
final String reportNum = len <= MAX_CHARS_TO_REPORT ?
new String(ch, off, len) : new String(ch, off, MAX_CHARS_TO_REPORT) + " [truncated]";
final int reportLen = Math.min(len, MAX_CHARS_TO_REPORT);
throw new NumberFormatException("Value \"" + new String(ch, off, reportLen)
+ "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage());
}
}

private static BigDecimal parseBigDecimal(final char[] chars, final int off, final int len, final int splitLen) {
boolean numHasSign = false;
boolean expHasSign = false;
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/io/BigIntegerParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.fasterxml.jackson.core.io;

import ch.randelshofer.fastdoubleparser.JavaBigIntegerParser;

import java.math.BigInteger;

import static com.fasterxml.jackson.core.io.BigDecimalParser.MAX_CHARS_TO_REPORT;

/**
* Helper class used to implement more optimized parsing of {@link BigInteger} for REALLY
* big values (over 500 characters).
*
* @since 2.15
*/
public final class BigIntegerParser
{
private BigIntegerParser() {}

public static BigInteger parseWithFastParser(final String valueStr) {
try {
return JavaBigIntegerParser.parseBigInteger(valueStr);
} catch (NumberFormatException nfe) {
final String reportNum = valueStr.length() <= MAX_CHARS_TO_REPORT ?
valueStr : valueStr.substring(0, MAX_CHARS_TO_REPORT) + " [truncated]";
throw new NumberFormatException("Value \"" + reportNum
+ "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage());
}
}
}
83 changes: 79 additions & 4 deletions src/main/java/com/fasterxml/jackson/core/io/NumberInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -380,28 +380,103 @@ public static float parseFloat(final String s, final boolean useFastParser) thro
return useFastParser ? JavaFloatParser.parseFloat(s) : Float.parseFloat(s);
}

public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
/**
* @param s a string representing a number to parse
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
*/
public static BigDecimal parseBigDecimal(final String s) throws NumberFormatException {
return BigDecimalParser.parse(s);
}

public static BigDecimal parseBigDecimal(char[] ch, int off, int len) throws NumberFormatException {
/**
* @param s a string representing a number to parse
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
* @since v2.15
*/
public static BigDecimal parseBigDecimal(final String s, final boolean useFastParser) throws NumberFormatException {
return useFastParser ?
BigDecimalParser.parseWithFastParser(s) :
BigDecimalParser.parse(s);
}

/**
* @param ch a char array with text that makes up a number
* @param off the offset to apply when parsing the number in the char array
* @param len the length of the number in the char array
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
*/
public static BigDecimal parseBigDecimal(final char[] ch, final int off, final int len) throws NumberFormatException {
return BigDecimalParser.parse(ch, off, len);
}

public static BigDecimal parseBigDecimal(char[] ch) throws NumberFormatException {
/**
* @param ch a char array with text that makes up a number
* @param off the offset to apply when parsing the number in the char array
* @param len the length of the number in the char array
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
* @since v2.15
*/
public static BigDecimal parseBigDecimal(final char[] ch, final int off, final int len,
final boolean useFastParser)
throws NumberFormatException {
return useFastParser ?
BigDecimalParser.parseWithFastParser(ch, off, len) :
BigDecimalParser.parse(ch, off, len);
}

/**
* @param ch a char array with text that makes up a number
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
*/
public static BigDecimal parseBigDecimal(final char[] ch) throws NumberFormatException {
return BigDecimalParser.parse(ch);
}

/**
* @param ch a char array with text that makes up a number
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
* @return a BigDecimal
* @throws NumberFormatException if the char array cannot be represented by a BigDecimal
* @since v2.15
*/
public static BigDecimal parseBigDecimal(final char[] ch, final boolean useFastParser) throws NumberFormatException {
return useFastParser ?
BigDecimalParser.parseWithFastParser(ch, 0, ch.length) :
BigDecimalParser.parse(ch);
}

/**
* @param s a string representing a number to parse
* @return a BigInteger
* @throws NumberFormatException if string cannot be represented by a BigInteger
* @since v2.14
*/
public static BigInteger parseBigInteger(String s) throws NumberFormatException {
public static BigInteger parseBigInteger(final String s) throws NumberFormatException {
if (s.length() > LARGE_INT_SIZE) {
return BigDecimalParser.parse(s).toBigInteger();
}
return new BigInteger(s);
}

/**
* @param s a string representing a number to parse
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
* @return a BigInteger
* @throws NumberFormatException if string cannot be represented by a BigInteger
* @since v2.15
*/
public static BigInteger parseBigInteger(final String s, final boolean useFastParser) throws NumberFormatException {
if (useFastParser) {
return BigIntegerParser.parseWithFastParser(s);
} else {
return parseBigInteger(s);
}
}
}
15 changes: 9 additions & 6 deletions src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -484,38 +484,41 @@ public char[] contentsAsArray() {
* Convenience method for converting contents of the buffer
* into a {@link BigDecimal}.
*
* @param constraints constraints for stream reading
* @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser}
* @return Buffered text value parsed as a {@link BigDecimal}, if possible
*
* @throws NumberFormatException if contents are not a valid Java number
*
* @since 2.15
*/
public BigDecimal contentsAsDecimal(StreamReadConstraints constraints) throws NumberFormatException
public BigDecimal contentsAsDecimal(final StreamReadConstraints constraints,
final boolean useFastParser) throws NumberFormatException
{
// Already got a pre-cut array?
if (_resultArray != null) {
constraints.validateFPLength(_resultArray.length);
return NumberInput.parseBigDecimal(_resultArray);
return NumberInput.parseBigDecimal(_resultArray, useFastParser);
}
// Or a shared buffer?
if ((_inputStart >= 0) && (_inputBuffer != null)) {
constraints.validateFPLength(_inputLen);
return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen);
return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen, useFastParser);
}
// Or if not, just a single buffer (the usual case)
if ((_segmentSize == 0) && (_currentSegment != null)) {
constraints.validateFPLength(_currentSize);
return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize);
return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize, useFastParser);
}
// If not, let's just get it aggregated...
final char[] numArray = contentsAsArray();
constraints.validateFPLength(numArray.length);
return NumberInput.parseBigDecimal(numArray);
return NumberInput.parseBigDecimal(numArray, useFastParser);
}

@Deprecated // @since 2.15
public BigDecimal contentsAsDecimal() throws NumberFormatException {
return contentsAsDecimal(StreamReadConstraints.defaults());
return contentsAsDecimal(StreamReadConstraints.defaults(), false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,43 @@

public class BigDecimalParserTest extends com.fasterxml.jackson.core.BaseTest {
public void testLongStringParse() {
final int len = 1500;
final StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append("A");
try {
BigDecimalParser.parse(genLongString());
fail("expected NumberFormatException");
} catch (NumberFormatException nfe) {
assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA"));
assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated"));
}
}

public void testLongStringFastParse() {
try {
BigDecimalParser.parseWithFastParser(genLongString());
fail("expected NumberFormatException");
} catch (NumberFormatException nfe) {
assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA"));
assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated"));
}
}

/* there is an open issue at https://github.com/wrandelshofer/FastDoubleParser/issues/24 about this
public void testLongStringFastParseBigInteger() {
try {
BigDecimalParser.parse(sb.toString());
BigDecimalParser.parseBigIntegerWithFastParser(genLongString());
fail("expected NumberFormatException");
} catch (NumberFormatException nfe) {
assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA"));
assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated"));
}
}
*/

private String genLongString() {
final int len = 1500;
final StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append("A");
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ public void testParseLongBigInteger()
}
String test1000 = stringBuilder.toString();
assertEquals(new BigInteger(test1000), NumberInput.parseBigInteger(test1000));
assertEquals(new BigInteger(test1000), NumberInput.parseBigInteger(test1000, true));
for (int i = 0; i < 1000; i++) {
stringBuilder.append(7);
}
String test2000 = stringBuilder.toString();
assertEquals(new BigInteger(test2000), NumberInput.parseBigInteger(test2000));
assertEquals(new BigInteger(test2000), NumberInput.parseBigInteger(test2000, true));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class FastParserNonStandardNumberParsingTest
.enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS)
.enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER)
.enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)
.build();

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class FastParserNumberParsingTest extends NumberParsingTest
{
private final JsonFactory fastFactory = JsonFactory.builder()
.enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER)
.enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)
.build();

@Override
Expand Down