Skip to content

Commit

Permalink
Fixes many error cases reported in issue #9.
Browse files Browse the repository at this point in the history
  • Loading branch information
wrandelshofer committed Mar 23, 2021
1 parent 0e2e5c3 commit 345820b
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FastDoubleParser

A straight forward C++ to Java port of Daniel Lemires fast_double_parser.
A straight forward C++ to Java port of Daniel Lemire's fast_double_parser.

https://github.com/lemire/fast_double_parser

Expand Down
3 changes: 3 additions & 0 deletions data/FastDoubleParser_errorcases.txt

Large diffs are not rendered by default.

50 changes: 34 additions & 16 deletions src/ch/randelshofer/math/FastDoubleParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package ch.randelshofer.math;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
* This is a straightforward C++ to Java port of fast_double_parser
Expand Down Expand Up @@ -780,7 +781,7 @@ private static Double computeFloat64(int power, long digits, boolean negative) {
// It was described in
// Clinger WD. How to read floating point numbers accurately.
// ACM SIGPLAN Notices. 1990
if (-22 <= power && power <= 22 && digits <= 9007199254740991L) {
if (-22 <= power && power <= 22 && digits >= 0 && digits <= 9007199254740991L) {
// convert the integer into a double. This is lossless since
// 0 <= i <= 2^53 - 1.
double d = (double) digits;
Expand Down Expand Up @@ -867,7 +868,7 @@ private static Double computeFloat64(int power, long digits, boolean negative) {
// We expect this next branch to be rarely taken (say 1% of the time).
// When (upper & 0x1FF) == 0x1FF, it can be common for
// lower + i < lower to be true (proba. much higher than 1%).
if ((upper & 0x1FF) == 0x1FF && (lower + digits < lower)) {
if ((upper & 0x1FF) == 0x1FF && Long.compareUnsigned(lower + digits, lower) < 0) {
long factor_mantissa_low =
MANTISSA_128[power - FASTFLOAT_SMALLEST_POWER];
// next, we compute the 64-bit x 128-bit multiplication, getting a 192-bit
Expand Down Expand Up @@ -959,6 +960,8 @@ private static Double computeFloat64(int power, long digits, boolean negative) {
* <p>
* FIXME
* <ul>
* <li>The method currently only supports a small subset of the grammar
* supported by {@link Double#valueOf(String)}.</li>
* <li>The method currently does not handle leading and trailing whitespace.</li>
* <li>The method currently does not throw an exception when non-whitespace
* character follow after the number.</li>
Expand Down Expand Up @@ -1022,9 +1025,6 @@ public static double parseDouble(CharSequence str) throws NumberFormatException
}
if (ch == '.') {
ch = ++index < strlen ? str.charAt(index) : 0;
if (!isInteger(ch)) {
throw new NumberFormatException("decimal point must be followed by an integer");
}
while (isInteger(ch)) {
digits = 10 * digits + ch - '0';// This might overflow, we deal with it later.
exponent--;
Expand All @@ -1039,7 +1039,7 @@ public static double parseDouble(CharSequence str) throws NumberFormatException
exponent = 0;
digits = 0;
while (isInteger(ch)) {
if (digits < MINIMAL_NINETEEN_DIGIT_INTEGER) {
if (digits >= 0 && digits < MINIMAL_NINETEEN_DIGIT_INTEGER) {
// We avoid overflow by only considering up to 19 digits.
digits = 10 * digits + ch - '0';
} else {
Expand All @@ -1051,7 +1051,7 @@ public static double parseDouble(CharSequence str) throws NumberFormatException
if (ch == '.') {
ch = ++index < strlen ? str.charAt(index) : 0;
while (isInteger(ch)) {
if (digits < MINIMAL_NINETEEN_DIGIT_INTEGER) {
if (digits >= 0 && digits < MINIMAL_NINETEEN_DIGIT_INTEGER) {
// We avoid overflow by only considering up to 19 digits.
digits = 10 * digits + ch - '0';
exponent--;
Expand Down Expand Up @@ -1083,22 +1083,40 @@ public static double parseDouble(CharSequence str) throws NumberFormatException
exponent += (neg_exp ? -exp_number : exp_number);
}

if (digits==0 || exponent < FASTFLOAT_SMALLEST_POWER) {
if (digits == 0) {
return negative ? -0.0 : 0.0;
}
if (exponent > FASTFLOAT_LARGEST_POWER) {
return negative ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
Double outDouble;
if (FASTFLOAT_SMALLEST_POWER <= exponent && exponent <= FASTFLOAT_LARGEST_POWER) {
outDouble = computeFloat64((int) exponent, digits, negative);
} else {
outDouble = null;
}
// from this point forward, exponent >= FASTFLOAT_SMALLEST_POWER and
// exponent <= FASTFLOAT_LARGEST_POWER

Double outDouble = computeFloat64((int) exponent, digits, negative);
if (outDouble == null) {
// we are almost never going to get here.
BigDecimal bigDecimal = BigDecimal.valueOf(negative ? -digits : digits)
.scaleByPowerOfTen((int) exponent);
BigDecimal bigDecimal = toBigDecimal(negative, (int) exponent, digits);
return bigDecimal.doubleValue();
}
return outDouble;
}

private static BigDecimal toBigDecimal(boolean negative, int exponent, long digits) {
BigDecimal bigDecimal;
if (digits < 0) {// digits is a uint64
int upper = (int) (digits >>> 32);
int lower = (int) digits;
bigDecimal =
new BigDecimal((BigInteger.valueOf(Integer.toUnsignedLong(upper))).shiftLeft(32).
add(BigInteger.valueOf(Integer.toUnsignedLong(lower))))
.scaleByPowerOfTen(exponent);
if (negative) {
bigDecimal = bigDecimal.negate();
}

} else {
bigDecimal = BigDecimal.valueOf(negative ? -digits : digits)
.scaleByPowerOfTen(exponent);
}
return bigDecimal;
}
}
15 changes: 15 additions & 0 deletions test/ch/randelshofer/math/FastDoubleParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.TestFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -46,6 +50,8 @@ List<DynamicNode> dynamicTestsLegalInputs() {
dynamicTest("1e1", () -> testLegalInput("1e1", 1e1)),
dynamicTest("1e+1", () -> testLegalInput("1e+1", 1e+1)),
dynamicTest("1e-1", () -> testLegalInput("1e-1", 1e-1)),
dynamicTest("3.7587182468424695418288325e-309", () -> testLegalInput("3.7587182468424695418288325e-309", 3.7587182468424695418288325e-309)),
dynamicTest("9007199254740992.e-256", () -> testLegalInput("9007199254740992.e-256", 9007199254740992.e-256)),
dynamicTest("0.00000000000000000000000000000000000000000001e+46",
() -> testLegalInput("0.00000000000000000000000000000000000000000001e+46",
100.0)),
Expand Down Expand Up @@ -101,6 +107,15 @@ List<DynamicNode> dynamicTestsLegalInputs() {
);
}

@TestFactory
Stream<DynamicNode> testErrorCases() throws IOException {
return Files.lines(Path.of("data/FastDoubleParser_errorcases.txt"))
.flatMap(line->Arrays.stream(line.split(",")))
.map(str->dynamicTest(str,()->testLegalInput(str,Double.parseDouble(str))));


}

@TestFactory
Stream<DynamicNode> dynamicTestsRandomInputs() {
Random r=new Random(0);
Expand Down

0 comments on commit 345820b

Please sign in to comment.