Skip to content

Commit 905919f

Browse files
ferenc-csakyFerenc Csaky
and
Ferenc Csaky
authored
added O(n^1.5) BigDecimal parser implementation (#677)
Co-authored-by: Ferenc Csaky <[email protected]>
1 parent 205255f commit 905919f

File tree

3 files changed

+166
-15
lines changed

3 files changed

+166
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.fasterxml.jackson.core.io;
2+
3+
import java.math.BigDecimal;
4+
5+
// Based on a great idea of Eric Obermühlner to use a tree of smaller BigDecimals for parsing really big numbers
6+
// with O(n^1.5) complexity instead of O(n^2) when using the constructor for a decimal representation from JDK 8/11:
7+
// https://github.com/eobermuhlner/big-math/commit/7a5419aac8b2adba2aa700ccf00197f97b2ad89f
8+
public final class BigDecimalParser {
9+
10+
private final char[] chars;
11+
private final int off;
12+
private final int len;
13+
14+
BigDecimalParser(char[] chars, int off, int len) {
15+
this.chars = chars;
16+
this.off = off;
17+
this.len = len;
18+
}
19+
20+
BigDecimal parse() throws NumberFormatException {
21+
try {
22+
if (len < 500) {
23+
return new BigDecimal(chars, off, len);
24+
}
25+
26+
int splitLen = len / 10;
27+
return parseBigDecimal(splitLen);
28+
29+
} catch (NumberFormatException e) {
30+
String val = new String(chars, off, len);
31+
32+
throw new NumberFormatException("Value \"" + val + "\" can not be represented as BigDecimal."
33+
+ " Reason: " + e.getMessage());
34+
}
35+
}
36+
37+
private BigDecimal parseBigDecimal(int splitLen) {
38+
boolean numHasSign = false;
39+
boolean expHasSign = false;
40+
boolean neg = false;
41+
int numIdx = 0;
42+
int expIdx = -1;
43+
int dotIdx = -1;
44+
int scale = 0;
45+
46+
for (int i = off; i < len; i++) {
47+
char c = chars[i];
48+
switch (c) {
49+
case '+':
50+
if (expIdx >= 0) {
51+
if (expHasSign) {
52+
throw new NumberFormatException("Multiple signs in exponent");
53+
}
54+
expHasSign = true;
55+
} else {
56+
if (numHasSign) {
57+
throw new NumberFormatException("Multiple signs in number");
58+
}
59+
numHasSign = true;
60+
numIdx = i + 1;
61+
}
62+
break;
63+
case '-':
64+
if (expIdx >= 0) {
65+
if (expHasSign) {
66+
throw new NumberFormatException("Multiple signs in exponent");
67+
}
68+
expHasSign = true;
69+
} else {
70+
if (numHasSign) {
71+
throw new NumberFormatException("Multiple signs in number");
72+
}
73+
numHasSign = true;
74+
neg = true;
75+
numIdx = i + 1;
76+
}
77+
break;
78+
case 'e':
79+
case 'E':
80+
if (expIdx >= 0) {
81+
throw new NumberFormatException("Multiple exponent markers");
82+
}
83+
expIdx = i;
84+
break;
85+
case '.':
86+
if (dotIdx >= 0) {
87+
throw new NumberFormatException("Multiple decimal points");
88+
}
89+
dotIdx = i;
90+
break;
91+
default:
92+
if (dotIdx >= 0 && expIdx == -1) {
93+
scale++;
94+
}
95+
}
96+
}
97+
98+
int numEndIdx;
99+
int exp = 0;
100+
if (expIdx >= 0) {
101+
numEndIdx = expIdx;
102+
String expStr = new String(chars, expIdx + 1, len - expIdx - 1);
103+
exp = Integer.parseInt(expStr);
104+
scale = adjustScale(scale, exp);
105+
} else {
106+
numEndIdx = len;
107+
}
108+
109+
BigDecimal res;
110+
111+
if (dotIdx >= 0) {
112+
int leftLen = dotIdx - numIdx;
113+
BigDecimal left = toBigDecimalRec(numIdx, leftLen, exp, splitLen);
114+
115+
int rightLen = numEndIdx - dotIdx - 1;
116+
BigDecimal right = toBigDecimalRec(dotIdx + 1, rightLen, exp - rightLen, splitLen);
117+
118+
res = left.add(right);
119+
} else {
120+
res = toBigDecimalRec(numIdx, numEndIdx - numIdx, exp, splitLen);
121+
}
122+
123+
if (scale != 0) {
124+
res = res.setScale(scale);
125+
}
126+
127+
if (neg) {
128+
res = res.negate();
129+
}
130+
131+
return res;
132+
}
133+
134+
private int adjustScale(int scale, long exp) {
135+
long adjScale = scale - exp;
136+
if (adjScale > Integer.MAX_VALUE || adjScale < Integer.MIN_VALUE) {
137+
throw new NumberFormatException(
138+
"Scale out of range: " + adjScale + " while adjusting scale " + scale + " to exponent " + exp);
139+
}
140+
141+
return (int) adjScale;
142+
}
143+
144+
private BigDecimal toBigDecimalRec(int off, int len, int scale, int splitLen) {
145+
if (len > splitLen) {
146+
int mid = len / 2;
147+
BigDecimal left = toBigDecimalRec(off, mid, scale + len - mid, splitLen);
148+
BigDecimal right = toBigDecimalRec(off + mid, len - mid, scale, splitLen);
149+
150+
return left.add(right);
151+
}
152+
153+
return len == 0 ? BigDecimal.ZERO : new BigDecimal(chars, off, len).movePointRight(scale);
154+
}
155+
}

src/main/java/com/fasterxml/jackson/core/io/NumberInput.java

+9-13
Original file line numberDiff line numberDiff line change
@@ -311,22 +311,18 @@ public static double parseDouble(String s) throws NumberFormatException {
311311
}
312312

313313
public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
314-
try { return new BigDecimal(s); } catch (NumberFormatException e) {
315-
throw _badBD(s);
316-
}
317-
}
314+
char[] ch = s.toCharArray();
318315

319-
public static BigDecimal parseBigDecimal(char[] b) throws NumberFormatException {
320-
return parseBigDecimal(b, 0, b.length);
316+
return parseBigDecimal(ch);
321317
}
322-
323-
public static BigDecimal parseBigDecimal(char[] b, int off, int len) throws NumberFormatException {
324-
try { return new BigDecimal(b, off, len); } catch (NumberFormatException e) {
325-
throw _badBD(new String(b, off, len));
326-
}
318+
319+
public static BigDecimal parseBigDecimal(char[] ch) throws NumberFormatException {
320+
return parseBigDecimal(ch, 0, ch.length);
327321
}
328322

329-
private static NumberFormatException _badBD(String s) {
330-
return new NumberFormatException("Value \""+s+"\" can not be represented as BigDecimal");
323+
public static BigDecimal parseBigDecimal(char[] ch, int off, int len) throws NumberFormatException {
324+
BigDecimalParser parser = new BigDecimalParser(ch, off, len);
325+
326+
return parser.parse();
331327
}
332328
}

src/test/java/com/fasterxml/jackson/core/util/TestTextBuffer.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.fasterxml.jackson.core.util;
22

3-
import com.fasterxml.jackson.core.io.NumberInput;
3+
import com.fasterxml.jackson.core.io.BigDecimalParser;
44

55
public class TestTextBuffer
66
extends com.fasterxml.jackson.core.BaseTest
@@ -139,7 +139,7 @@ public void testContentsAsDecimalThrowsNumberFormatException() {
139139
textBuffer.contentsAsDecimal();
140140
fail("Expecting exception: NumberFormatException");
141141
} catch(NumberFormatException e) {
142-
assertEquals(NumberInput.class.getName(), e.getStackTrace()[0].getClassName());
142+
assertEquals(BigDecimalParser.class.getName(), e.getStackTrace()[0].getClassName());
143143
}
144144
}
145145

0 commit comments

Comments
 (0)