Skip to content

Commit 1bf4b80

Browse files
committed
Floats formatted with "correct" precision
Conversion float->string ensures that, for any float f, tonumber(tostring(f)) == f, but still avoiding noise like 1.1 converting to "1.1000000000000001".
1 parent 4c6afbc commit 1bf4b80

File tree

3 files changed

+153
-21
lines changed

3 files changed

+153
-21
lines changed

lobject.c

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "lprefix.h"
1111

1212

13+
#include <float.h>
1314
#include <locale.h>
1415
#include <math.h>
1516
#include <stdarg.h>
@@ -401,29 +402,54 @@ int luaO_utf8esc (char *buff, unsigned long x) {
401402
/*
402403
** Maximum length of the conversion of a number to a string. Must be
403404
** enough to accommodate both LUA_INTEGER_FMT and LUA_NUMBER_FMT.
404-
** (For a long long int, this is 19 digits plus a sign and a final '\0',
405-
** adding to 21. For a long double, it can go to a sign, 33 digits,
406-
** the dot, an exponent letter, an exponent sign, 5 exponent digits,
407-
** and a final '\0', adding to 43.)
405+
** For a long long int, this is 19 digits plus a sign and a final '\0',
406+
** adding to 21. For a long double, it can go to a sign, the dot, an
407+
** exponent letter, an exponent sign, 4 exponent digits, the final
408+
** '\0', plus the significant digits, which are approximately the *_DIG
409+
** attribute.
408410
*/
409-
#define MAXNUMBER2STR 44
411+
#define MAXNUMBER2STR (20 + l_floatatt(DIG))
410412

411413

412414
/*
413-
** Convert a number object to a string, adding it to a buffer
415+
** Convert a float to a string, adding it to a buffer. First try with
416+
** a not too large number of digits, to avoid noise (for instance,
417+
** 1.1 going to "1.1000000000000001"). If that lose precision, so
418+
** that reading the result back gives a different number, then do the
419+
** conversion again with extra precision. Moreover, if the numeral looks
420+
** like an integer (without a decimal point or an exponent), add ".0" to
421+
** its end.
422+
*/
423+
static int tostringbuffFloat (lua_Number n, char *buff) {
424+
/* first conversion */
425+
int len = l_sprintf(buff, MAXNUMBER2STR, LUA_NUMBER_FMT,
426+
(LUAI_UACNUMBER)n);
427+
lua_Number check = lua_str2number(buff, NULL); /* read it back */
428+
if (check != n) { /* not enough precision? */
429+
/* convert again with more precision */
430+
len = l_sprintf(buff, MAXNUMBER2STR, LUA_NUMBER_FMT_N,
431+
(LUAI_UACNUMBER)n);
432+
}
433+
/* looks like an integer? */
434+
if (buff[strspn(buff, "-0123456789")] == '\0') {
435+
buff[len++] = lua_getlocaledecpoint();
436+
buff[len++] = '0'; /* adds '.0' to result */
437+
}
438+
return len;
439+
}
440+
441+
442+
/*
443+
** Convert a number object to a string, adding it to a buffer.
414444
*/
415445
static unsigned tostringbuff (TValue *obj, char *buff) {
416446
int len;
417447
lua_assert(ttisnumber(obj));
418448
if (ttisinteger(obj))
419449
len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
420-
else {
421-
len = lua_number2str(buff, MAXNUMBER2STR, fltvalue(obj));
422-
if (buff[strspn(buff, "-0123456789")] == '\0') { /* looks like an int? */
423-
buff[len++] = lua_getlocaledecpoint();
424-
buff[len++] = '0'; /* adds '.0' to result */
425-
}
426-
}
450+
else
451+
len = tostringbuffFloat(fltvalue(obj), buff);
452+
lua_assert(len < MAXNUMBER2STR);
427453
return cast_uint(len);
428454
}
429455

luaconf.h

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,13 @@
416416
@@ l_floatatt(x) corrects float attribute 'x' to the proper float type
417417
** by prefixing it with one of FLT/DBL/LDBL.
418418
@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats.
419-
@@ LUA_NUMBER_FMT is the format for writing floats.
420-
@@ lua_number2str converts a float to a string.
419+
@@ LUA_NUMBER_FMT is the format for writing floats with the maximum
420+
** number of digits that respects tostring(tonumber(numeral)) == numeral.
421+
** (That would be floor(log10(2^n)), where n is the number of bits in
422+
** the float mantissa.)
423+
@@ LUA_NUMBER_FMT_N is the format for writing floats with the minimum
424+
** number of digits that ensures tonumber(tostring(number)) == number.
425+
** (That would be LUA_NUMBER_FMT+2.)
421426
@@ l_mathop allows the addition of an 'l' or 'f' to all math operations.
422427
@@ l_floor takes the floor of a float.
423428
@@ lua_str2number converts a decimal numeral to a number.
@@ -428,8 +433,6 @@
428433

429434
#define l_floor(x) (l_mathop(floor)(x))
430435

431-
#define lua_number2str(s,sz,n) \
432-
l_sprintf((s), sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)(n))
433436

434437
/*
435438
@@ lua_numbertointeger converts a float number with an integral value
@@ -458,6 +461,7 @@
458461

459462
#define LUA_NUMBER_FRMLEN ""
460463
#define LUA_NUMBER_FMT "%.7g"
464+
#define LUA_NUMBER_FMT_N "%.9g"
461465

462466
#define l_mathop(op) op##f
463467

@@ -474,6 +478,7 @@
474478

475479
#define LUA_NUMBER_FRMLEN "L"
476480
#define LUA_NUMBER_FMT "%.19Lg"
481+
#define LUA_NUMBER_FMT_N "%.21Lg"
477482

478483
#define l_mathop(op) op##l
479484

@@ -488,7 +493,8 @@
488493
#define LUAI_UACNUMBER double
489494

490495
#define LUA_NUMBER_FRMLEN ""
491-
#define LUA_NUMBER_FMT "%.14g"
496+
#define LUA_NUMBER_FMT "%.15g"
497+
#define LUA_NUMBER_FMT_N "%.17g"
492498

493499
#define l_mathop(op) op
494500

testes/math.lua

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ do
2222
end
2323
end
2424

25+
26+
-- maximum exponent for a floating-point number
27+
local maxexp = 0
28+
do
29+
local p = 2.0
30+
while p < math.huge do
31+
maxexp = maxexp + 1
32+
p = p + p
33+
end
34+
end
35+
36+
2537
local function isNaN (x)
2638
return (x ~= x)
2739
end
@@ -34,8 +46,8 @@ do
3446
local x = 2.0^floatbits
3547
assert(x > x - 1.0 and x == x + 1.0)
3648

37-
print(string.format("%d-bit integers, %d-bit (mantissa) floats",
38-
intbits, floatbits))
49+
local msg = " %d-bit integers, %d-bit*2^%d floats"
50+
print(string.format(msg, intbits, floatbits, maxexp))
3951
end
4052

4153
assert(math.type(0) == "integer" and math.type(0.0) == "float"
@@ -803,7 +815,11 @@ do
803815
end
804816

805817

806-
print("testing 'math.random'")
818+
--
819+
-- [[==================================================================
820+
print("testing 'math.random'")
821+
-- -===================================================================
822+
--
807823

808824
local random, max, min = math.random, math.max, math.min
809825

@@ -1019,6 +1035,90 @@ assert(not pcall(random, minint + 1, minint))
10191035
assert(not pcall(random, maxint, maxint - 1))
10201036
assert(not pcall(random, maxint, minint))
10211037

1038+
-- ]]==================================================================
1039+
1040+
1041+
--
1042+
-- [[==================================================================
1043+
print("testing precision of 'tostring'")
1044+
-- -===================================================================
1045+
--
1046+
1047+
-- number of decimal digits supported by float precision
1048+
local decdig = math.floor(floatbits * math.log(2, 10))
1049+
print(string.format(" %d-digit float numbers with full precision",
1050+
decdig))
1051+
-- number of decimal digits supported by integer precision
1052+
local Idecdig = math.floor(math.log(maxint, 10))
1053+
print(string.format(" %d-digit integer numbers with full precision",
1054+
Idecdig))
1055+
1056+
do
1057+
-- Any number should print so that reading it back gives itself:
1058+
-- tonumber(tostring(x)) == x
1059+
1060+
-- Mersenne fractions
1061+
local p = 1.0
1062+
for i = 1, maxexp do
1063+
p = p + p
1064+
local x = 1 / (p - 1)
1065+
assert(x == tonumber(tostring(x)))
1066+
end
1067+
1068+
-- some random numbers in [0,1)
1069+
for i = 1, 100 do
1070+
local x = math.random()
1071+
assert(x == tonumber(tostring(x)))
1072+
end
1073+
1074+
-- different numbers shold print differently.
1075+
-- check pairs of floats with minimum detectable difference
1076+
local p = floatbits - 1
1077+
for i = 1, maxexp - 1 do
1078+
for _, i in ipairs{-i, i} do
1079+
local x = 2^i
1080+
local diff = 2^(i - p) -- least significant bit for 'x'
1081+
local y = x + diff
1082+
local fy = tostring(y)
1083+
assert(x ~= y and tostring(x) ~= fy)
1084+
assert(tonumber(fy) == y)
1085+
end
1086+
end
1087+
1088+
1089+
-- "reasonable" numerals should be printed like themselves
1090+
1091+
-- create random float numerals with 5 digits, with a decimal point
1092+
-- inserted in all places. (With more than 5, things like "0.00001"
1093+
-- reformats like "1e-5".)
1094+
for i = 1, 1000 do
1095+
-- random numeral with 5 digits
1096+
local x = string.format("%.5d", math.random(0, 99999))
1097+
for i = 2, #x do
1098+
-- insert decimal point at position 'i'
1099+
local y = string.sub(x, 1, i - 1) .. "." .. string.sub(x, i, -1)
1100+
y = string.gsub(y, "^0*(%d.-%d)0*$", "%1") -- trim extra zeros
1101+
assert(y == tostring(tonumber(y)))
1102+
end
1103+
end
1104+
1105+
-- all-random floats
1106+
local Fsz = string.packsize("n") -- size of floats in bytes
1107+
1108+
for i = 1, 400 do
1109+
local s = string.pack("j", math.random(0)) -- a random string of bits
1110+
while #s < Fsz do -- make 's' long enough
1111+
s = s .. string.pack("j", math.random(0))
1112+
end
1113+
local n = string.unpack("n", s) -- read 's' as a float
1114+
s = tostring(n)
1115+
if string.find(s, "^%-?%d") then -- avoid NaN, inf, -inf
1116+
assert(tonumber(s) == n)
1117+
end
1118+
end
1119+
1120+
end
1121+
-- ]]==================================================================
10221122

10231123

10241124
print('OK')

0 commit comments

Comments
 (0)