Skip to content

Commit

Permalink
added O(n^1.5) BigDecimal parser implementation (#677)
Browse files Browse the repository at this point in the history
Co-authored-by: Ferenc Csaky <[email protected]>
  • Loading branch information
ferenc-csaky and Ferenc Csaky authored Feb 5, 2021
1 parent 205255f commit 905919f
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 15 deletions.
155 changes: 155 additions & 0 deletions src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
22 changes: 9 additions & 13 deletions src/main/java/com/fasterxml/jackson/core/io/NumberInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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());
}
}

Expand Down

0 comments on commit 905919f

Please sign in to comment.