diff --git a/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java b/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java new file mode 100644 index 0000000000..7e27d4fefd --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java @@ -0,0 +1,155 @@ +package com.fasterxml.jackson.core.io; + +import java.math.BigDecimal; + +// Based on a great idea of Eric Obermühlner to use a tree of smaller BigDecimals for parsing really big numbers +// with O(n^1.5) complexity instead of O(n^2) when using the constructor for a decimal representation from JDK 8/11: +// https://github.com/eobermuhlner/big-math/commit/7a5419aac8b2adba2aa700ccf00197f97b2ad89f +public final class BigDecimalParser { + + private final char[] chars; + private final int off; + private final int len; + + BigDecimalParser(char[] chars, int off, int len) { + this.chars = chars; + this.off = off; + this.len = len; + } + + BigDecimal parse() throws NumberFormatException { + try { + if (len < 500) { + return new BigDecimal(chars, off, len); + } + + int splitLen = len / 10; + return parseBigDecimal(splitLen); + + } catch (NumberFormatException e) { + String val = new String(chars, off, len); + + throw new NumberFormatException("Value \"" + val + "\" can not be represented as BigDecimal." + + " Reason: " + e.getMessage()); + } + } + + private BigDecimal parseBigDecimal(int splitLen) { + boolean numHasSign = false; + boolean expHasSign = false; + boolean neg = false; + int numIdx = 0; + int expIdx = -1; + int dotIdx = -1; + int scale = 0; + + for (int i = off; i < len; i++) { + char c = chars[i]; + switch (c) { + case '+': + if (expIdx >= 0) { + if (expHasSign) { + throw new NumberFormatException("Multiple signs in exponent"); + } + expHasSign = true; + } else { + if (numHasSign) { + throw new NumberFormatException("Multiple signs in number"); + } + numHasSign = true; + numIdx = i + 1; + } + break; + case '-': + if (expIdx >= 0) { + if (expHasSign) { + throw new NumberFormatException("Multiple signs in exponent"); + } + expHasSign = true; + } else { + if (numHasSign) { + throw new NumberFormatException("Multiple signs in number"); + } + numHasSign = true; + neg = true; + numIdx = i + 1; + } + break; + case 'e': + case 'E': + if (expIdx >= 0) { + throw new NumberFormatException("Multiple exponent markers"); + } + expIdx = i; + break; + case '.': + if (dotIdx >= 0) { + throw new NumberFormatException("Multiple decimal points"); + } + dotIdx = i; + break; + default: + if (dotIdx >= 0 && expIdx == -1) { + scale++; + } + } + } + + int numEndIdx; + int exp = 0; + if (expIdx >= 0) { + numEndIdx = expIdx; + String expStr = new String(chars, expIdx + 1, len - expIdx - 1); + exp = Integer.parseInt(expStr); + scale = adjustScale(scale, exp); + } else { + numEndIdx = len; + } + + BigDecimal res; + + if (dotIdx >= 0) { + int leftLen = dotIdx - numIdx; + BigDecimal left = toBigDecimalRec(numIdx, leftLen, exp, splitLen); + + int rightLen = numEndIdx - dotIdx - 1; + BigDecimal right = toBigDecimalRec(dotIdx + 1, rightLen, exp - rightLen, splitLen); + + res = left.add(right); + } else { + res = toBigDecimalRec(numIdx, numEndIdx - numIdx, exp, splitLen); + } + + if (scale != 0) { + res = res.setScale(scale); + } + + if (neg) { + res = res.negate(); + } + + return res; + } + + private int adjustScale(int scale, long exp) { + long adjScale = scale - exp; + if (adjScale > Integer.MAX_VALUE || adjScale < Integer.MIN_VALUE) { + throw new NumberFormatException( + "Scale out of range: " + adjScale + " while adjusting scale " + scale + " to exponent " + exp); + } + + return (int) adjScale; + } + + private BigDecimal toBigDecimalRec(int off, int len, int scale, int splitLen) { + if (len > splitLen) { + int mid = len / 2; + BigDecimal left = toBigDecimalRec(off, mid, scale + len - mid, splitLen); + BigDecimal right = toBigDecimalRec(off + mid, len - mid, scale, splitLen); + + return left.add(right); + } + + return len == 0 ? BigDecimal.ZERO : new BigDecimal(chars, off, len).movePointRight(scale); + } +} diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java index a93236bd72..c929fcf2f7 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -311,22 +311,18 @@ public static double parseDouble(String s) throws NumberFormatException { } public static BigDecimal parseBigDecimal(String s) throws NumberFormatException { - try { return new BigDecimal(s); } catch (NumberFormatException e) { - throw _badBD(s); - } - } + char[] ch = s.toCharArray(); - public static BigDecimal parseBigDecimal(char[] b) throws NumberFormatException { - return parseBigDecimal(b, 0, b.length); + return parseBigDecimal(ch); } - - public static BigDecimal parseBigDecimal(char[] b, int off, int len) throws NumberFormatException { - try { return new BigDecimal(b, off, len); } catch (NumberFormatException e) { - throw _badBD(new String(b, off, len)); - } + + public static BigDecimal parseBigDecimal(char[] ch) throws NumberFormatException { + return parseBigDecimal(ch, 0, ch.length); } - private static NumberFormatException _badBD(String s) { - return new NumberFormatException("Value \""+s+"\" can not be represented as BigDecimal"); + public static BigDecimal parseBigDecimal(char[] ch, int off, int len) throws NumberFormatException { + BigDecimalParser parser = new BigDecimalParser(ch, off, len); + + return parser.parse(); } } diff --git a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java index a4e0243aec..f7eb410f02 100644 --- a/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java +++ b/src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java @@ -1,6 +1,6 @@ package com.fasterxml.jackson.core.util; -import com.fasterxml.jackson.core.io.NumberInput; +import com.fasterxml.jackson.core.io.BigDecimalParser; public class TestTextBuffer extends com.fasterxml.jackson.core.BaseTest @@ -139,7 +139,7 @@ public void testContentsAsDecimalThrowsNumberFormatException() { textBuffer.contentsAsDecimal(); fail("Expecting exception: NumberFormatException"); } catch(NumberFormatException e) { - assertEquals(NumberInput.class.getName(), e.getStackTrace()[0].getClassName()); + assertEquals(BigDecimalParser.class.getName(), e.getStackTrace()[0].getClassName()); } }