Skip to content

Commit c4ee2f8

Browse files
committed
Merge branch '2.13'
2 parents 49c7cd3 + 78e2a8e commit c4ee2f8

File tree

8 files changed

+260
-17
lines changed

8 files changed

+260
-17
lines changed

release-notes/CREDITS-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,7 @@ Jendrik Johannes (jjohannes@github)
257257
Jonathan Haber (jhaber@github)
258258
* Contributed #573: More customizable TokenFilter inclusion (using `Tokenfilter.Inclusion`)
259259
(2.12.0)
260+
261+
Ferenc Csaky (ferenc-csaky@github)
262+
* Contributed #677: Introduce O(n^1.5) BigDecimal parser implementation
263+
(2.13.0)

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ JSON library.
2222
#673: Replace `JsonGenerator.writeObject()` (and related) with `writePOJO()`
2323
#674: Replace `getCurrentValue()`/`setCurrentValue()` with
2424
`currentValue()`/`assignCurrentValue()` in `JsonParser`/`JsonGenerator
25+
#677: Introduce O(n^1.5) BigDecimal parser implementation
26+
(contributed by Ferenc C)
2527

2628
2.12.1 (08-Jan-2021)
2729

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

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -307,22 +307,14 @@ public static double parseDouble(String s) throws NumberFormatException {
307307
}
308308

309309
public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
310-
try { return new BigDecimal(s); } catch (NumberFormatException e) {
311-
throw _badBD(s);
312-
}
310+
return BigDecimalParser.parse(s);
313311
}
314312

315-
public static BigDecimal parseBigDecimal(char[] b) throws NumberFormatException {
316-
return parseBigDecimal(b, 0, b.length);
317-
}
318-
319-
public static BigDecimal parseBigDecimal(char[] b, int off, int len) throws NumberFormatException {
320-
try { return new BigDecimal(b, off, len); } catch (NumberFormatException e) {
321-
throw _badBD(new String(b, off, len));
322-
}
313+
public static BigDecimal parseBigDecimal(char[] ch, int off, int len) throws NumberFormatException {
314+
return BigDecimalParser.parse(ch, off, len);
323315
}
324316

325-
private static NumberFormatException _badBD(String s) {
326-
return new NumberFormatException("Value \""+s+"\" can not be represented as BigDecimal");
317+
public static BigDecimal parseBigDecimal(char[] ch) throws NumberFormatException {
318+
return BigDecimalParser.parse(ch);
327319
}
328320
}

src/test/java/com/fasterxml/jackson/core/json/async/AsyncNaNHandlingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private void _testAllowNaN(JsonFactory f, String doc, int readBytes) throws Exce
6060
/*BigDecimal dec =*/ p.getDecimalValue();
6161
fail("Should fail when trying to access NaN as BigDecimal");
6262
} catch (NumberFormatException e) {
63-
verifyException(e, "can not be represented as BigDecimal");
63+
verifyException(e, "can not be represented as `java.math.BigDecimal`");
6464
}
6565

6666
assertToken(JsonToken.END_ARRAY, p.nextToken());

src/test/java/com/fasterxml/jackson/core/read/NonStandardJsonReadFeaturesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private void _testAllowNaN(int mode)
183183
/*BigDecimal dec =*/ p.getDecimalValue();
184184
fail("Should fail when trying to access NaN as BigDecimal");
185185
} catch (NumberFormatException e) {
186-
verifyException(e, "can not be represented as BigDecimal");
186+
verifyException(e, "can not be represented as `java.math.BigDecimal`");
187187
}
188188

189189
assertToken(JsonToken.END_ARRAY, p.nextToken());

src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,73 @@ public void testFloatBoundary146Bytes()
438438
}
439439
}
440440

441+
/*
442+
/**********************************************************************
443+
/* Tests, BigDecimal (core#677)
444+
/**********************************************************************
445+
*/
446+
447+
public void testBigBigDecimalsBytes() throws Exception
448+
{
449+
_testBigBigDecimals(MODE_INPUT_STREAM);
450+
_testBigBigDecimals(MODE_INPUT_STREAM_THROTTLED);
451+
}
452+
453+
public void testBigBigDecimalsChars() throws Exception
454+
{
455+
_testBigBigDecimals(MODE_READER);
456+
}
457+
458+
public void testBigBigDecimalsDataInput() throws Exception
459+
{
460+
_testBigBigDecimals(MODE_DATA_INPUT);
461+
}
462+
463+
private void _testBigBigDecimals(int mode) throws Exception
464+
{
465+
final String BASE_FRACTION =
466+
"01610253934481930774151441507943554511027782188707463024288149352877602369090537"
467+
+"80583522838238149455840874862907649203136651528841378405339370751798532555965157588"
468+
+"51877960056849468879933122908090021571162427934915567330612627267701300492535817858"
469+
+"36107216979078343419634586362681098115326893982589327952357032253344676618872460059"
470+
+"52652865429180458503533715200184512956356092484787210672008123556320998027133021328"
471+
+"04777044107393832707173313768807959788098545050700242134577863569636367439867566923"
472+
+"33479277494056927358573496400831024501058434838492057410330673302052539013639792877"
473+
+"76670882022964335417061758860066263335250076803973514053909274208258510365484745192"
474+
+"39425298649420795296781692303253055152441850691276044546565109657012938963181532017"
475+
+"97420631515930595954388119123373317973532146157980827838377034575940814574561703270"
476+
+"54949003909864767732479812702835339599792873405133989441135669998398892907338968744"
477+
+"39682249327621463735375868408190435590094166575473967368412983975580104741004390308"
478+
+"45302302121462601506802738854576700366634229106405188353120298347642313881766673834"
479+
+"60332729485083952142460470270121052469394888775064758246516888122459628160867190501"
480+
+"92476878886543996441778751825677213412487177484703116405390741627076678284295993334"
481+
+"23142914551517616580884277651528729927553693274406612634848943914370188078452131231"
482+
+"17351787166509190240927234853143290940647041705485514683182501795615082930770566118"
483+
+"77488417962195965319219352314664764649802231780262169742484818333055713291103286608"
484+
+"64318433253572997833038335632174050981747563310524775762280529871176578487487324067"
485+
+"90242862159403953039896125568657481354509805409457993946220531587293505986329150608"
486+
+"18702520420240989908678141379300904169936776618861221839938283876222332124814830207"
487+
+"073816864076428273177778788053613345444299361357958409716099682468768353446625063";
488+
489+
for (String asText : new String[] {
490+
"50."+BASE_FRACTION,
491+
"-37."+BASE_FRACTION,
492+
"0.00"+BASE_FRACTION,
493+
"-0.012"+BASE_FRACTION,
494+
"9999998."+BASE_FRACTION,
495+
"-8888392."+BASE_FRACTION,
496+
}) {
497+
final String DOC = "[ "+asText+" ]";
498+
499+
JsonParser p = createParser(mode, DOC);
500+
assertToken(JsonToken.START_ARRAY, p.nextToken());
501+
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
502+
final BigDecimal exp = new BigDecimal(asText);
503+
assertEquals(exp, p.getDecimalValue());
504+
p.close();
505+
}
506+
}
507+
441508
/*
442509
/**********************************************************************
443510
/* Tests, misc other

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

Lines changed: 2 additions & 2 deletions
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)