Fixed parsing of float literals.

Float literals were not parsed properly when their fractional part
exceeded 53 significant bits. This affected in particular math.PI
and math.E. Rather than reinventing the wheel, this patch reuses
C's strtod() implementation, which already does the heavy lifting
with respect to correctness, though some caution is necessary to
keep float parsing locale-independent.
This commit is contained in:
Reimer Behrends
2014-05-23 11:27:39 +02:00
parent b54f66eeff
commit 8b8a21cb59
2 changed files with 92 additions and 44 deletions

View File

@@ -231,33 +231,43 @@ proc parseInt*(s: string, number: var int, start = 0): int {.
else:
number = int(res)
proc tenToThePowerOf(b: int): BiggestFloat =
var b = b
var a = 10.0
result = 1.0
while true:
if (b and 1) == 1:
result *= a
b = b shr 1
if b == 0: break
a *= a
proc parseBiggestFloat*(s: string, number: var BiggestFloat, start = 0): int {.
rtl, extern: "npuParseBiggestFloat", noSideEffect.} =
## parses a float starting at `start` and stores the value into `number`.
## Result is the number of processed chars or 0 if there occured a parsing
## error.
## Result is the number of processed chars or 0 if a parsing error
## occurred.
type struct_lconv {.importc: "struct lconv",header:"<locale.h>".} =
object
# Unneeded fields have been omitted.
decimal_point: cstring
proc localeconv(): ptr struct_lconv {.importc, header: "<locale.h>",
noSideEffect.}
proc strtod(buf: cstring, endptr: ptr cstring): float64 {.importc,
header: "<stdlib.h>", noSideEffect.}
# This routine leverages `strtod()` for the non-trivial task of
# parsing floating point numbers correctly. Because `strtod()` is
# locale-dependent with respect to the radix character, we create
# a copy where the decimal point is replaced with the locale's
# radix character.
var
esign = 1.0
sign = 1.0
i = start
exponent: int
flags: int
number = 0.0
if s[i] == '+': inc(i)
elif s[i] == '-':
sign = -1.0
sign = 1.0
t = ""
hasdigits = false
# Sign?
if s[i] == '+' or s[i] == '-':
if s[i] == '-':
sign = -1.0
add(t, s[i])
inc(i)
# NaN?
if s[i] == 'N' or s[i] == 'n':
if s[i+1] == 'A' or s[i+1] == 'a':
if s[i+2] == 'N' or s[i+2] == 'n':
@@ -265,6 +275,8 @@ proc parseBiggestFloat*(s: string, number: var BiggestFloat, start = 0): int {.
number = NaN
return i+3 - start
return 0
# Inf?
if s[i] == 'I' or s[i] == 'i':
if s[i+1] == 'N' or s[i+1] == 'n':
if s[i+2] == 'F' or s[i+2] == 'f':
@@ -272,46 +284,40 @@ proc parseBiggestFloat*(s: string, number: var BiggestFloat, start = 0): int {.
number = Inf*sign
return i+3 - start
return 0
# Integer part?
while s[i] in {'0'..'9'}:
# Read integer part
flags = flags or 1
number = number * 10.0 + toFloat(ord(s[i]) - ord('0'))
hasdigits = true
add(t, s[i])
inc(i)
while s[i] == '_': inc(i)
# Decimal?
# Fractional part?
if s[i] == '.':
var hd = 1.0
add(t, localeconv().decimal_point)
inc(i)
while s[i] in {'0'..'9'}:
# Read fractional part
flags = flags or 2
number = number * 10.0 + toFloat(ord(s[i]) - ord('0'))
hd = hd * 10.0
hasdigits = true
add(t, s[i])
inc(i)
while s[i] == '_': inc(i)
number = number / hd # this complicated way preserves precision
# Again, read integer and fractional part
if flags == 0: return 0
if not hasdigits:
return 0
# Exponent?
if s[i] in {'e', 'E'}:
add(t, s[i])
inc(i)
if s[i] == '+':
inc(i)
elif s[i] == '-':
esign = -1.0
if s[i] in {'+', '-'}:
add(t, s[i])
inc(i)
if s[i] notin {'0'..'9'}:
return 0
while s[i] in {'0'..'9'}:
exponent = exponent * 10 + ord(s[i]) - ord('0')
add(t, s[i])
inc(i)
while s[i] == '_': inc(i)
# Calculate Exponent
let hd = tenToThePowerOf(exponent)
if esign > 0.0: number = number * hd
else: number = number / hd
# evaluate sign
number = number * sign
number = strtod(t, nil)
result = i - start
proc parseFloat*(s: string, number: var float, start = 0): int {.

42
tests/float/tfloat4.nim Normal file
View File

@@ -0,0 +1,42 @@
import math, strutils
proc c_sprintf(buf, fmt: cstring) {.importc:"sprintf", header: "<stdio.h>", varargs.}
proc floatToStr(f: float64): string =
var buffer: array[128, char]
c_sprintf(buffer, "%.16e", f)
result = ""
for ch in buffer:
if ch == '\0':
return
add(result, ch)
let testFloats = [
"0", "-1", "1", "1.", ".3", "3.3", "-.3", "-99.99",
"1.1e10", "-2e100", "1.234e-10", "1.234e+10",
"-inf", "inf", "+inf",
"3.14159265358979323846264338327950288",
"1.57079632679489661923132169163975144",
"0.785398163397448309615660845819875721",
"1.41421356237309504880168872420969808",
"0.707106781186547524400844362104849039",
"2.71828182845904523536028747135266250",
"0.00097656250000000021684043449710088680149056017398834228515625"
]
for num in testFloats:
assert num.parseFloat.floatToStr.parseFloat == num.parseFloat
assert "0".parseFloat == 0.0
assert "-.1".parseFloat == -0.1
assert "2.5e1".parseFloat == 25.0
assert "1e10".parseFloat == 10_000_000_000.0
assert "0.000_005".parseFloat == 5.000_000e-6
assert "1.234_567e+2".parseFloat == 123.4567
assert "1e1_00".parseFloat == "1e100".parseFloat
assert "3.1415926535897932384626433".parseFloat ==
3.1415926535897932384626433
assert "2.71828182845904523536028747".parseFloat ==
2.71828182845904523536028747
assert 0.00097656250000000021684043449710088680149056017398834228515625 ==
"0.00097656250000000021684043449710088680149056017398834228515625".parseFloat