aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoberto Ierusalimschy <roberto@inf.puc-rio.br>2024-08-02 15:09:30 -0300
committerRoberto Ierusalimschy <roberto@inf.puc-rio.br>2024-08-02 15:09:30 -0300
commit1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a (patch)
tree5162b1f662c4dcb6127ba03866096f70d0c298ab
parent4c6afbcb01d1cae72d829af5301df5f592fa2079 (diff)
downloadlua-1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a.tar.gz
lua-1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a.tar.bz2
lua-1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a.zip
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".
-rw-r--r--lobject.c52
-rw-r--r--luaconf.h16
-rw-r--r--testes/math.lua106
3 files changed, 153 insertions, 21 deletions
diff --git a/lobject.c b/lobject.c
index 1c4ea1af..f7159547 100644
--- a/lobject.c
+++ b/lobject.c
@@ -10,6 +10,7 @@
10#include "lprefix.h" 10#include "lprefix.h"
11 11
12 12
13#include <float.h>
13#include <locale.h> 14#include <locale.h>
14#include <math.h> 15#include <math.h>
15#include <stdarg.h> 16#include <stdarg.h>
@@ -401,29 +402,54 @@ int luaO_utf8esc (char *buff, unsigned long x) {
401/* 402/*
402** Maximum length of the conversion of a number to a string. Must be 403** Maximum length of the conversion of a number to a string. Must be
403** enough to accommodate both LUA_INTEGER_FMT and LUA_NUMBER_FMT. 404** 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** 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** adding to 21. For a long double, it can go to a sign, the dot, an
406** the dot, an exponent letter, an exponent sign, 5 exponent digits, 407** exponent letter, an exponent sign, 4 exponent digits, the final
407** and a final '\0', adding to 43.) 408** '\0', plus the significant digits, which are approximately the *_DIG
409** attribute.
408*/ 410*/
409#define MAXNUMBER2STR 44 411#define MAXNUMBER2STR (20 + l_floatatt(DIG))
410 412
411 413
412/* 414/*
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*/
423static 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.
414*/ 444*/
415static unsigned tostringbuff (TValue *obj, char *buff) { 445static unsigned tostringbuff (TValue *obj, char *buff) {
416 int len; 446 int len;
417 lua_assert(ttisnumber(obj)); 447 lua_assert(ttisnumber(obj));
418 if (ttisinteger(obj)) 448 if (ttisinteger(obj))
419 len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj)); 449 len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
420 else { 450 else
421 len = lua_number2str(buff, MAXNUMBER2STR, fltvalue(obj)); 451 len = tostringbuffFloat(fltvalue(obj), buff);
422 if (buff[strspn(buff, "-0123456789")] == '\0') { /* looks like an int? */ 452 lua_assert(len < MAXNUMBER2STR);
423 buff[len++] = lua_getlocaledecpoint();
424 buff[len++] = '0'; /* adds '.0' to result */
425 }
426 }
427 return cast_uint(len); 453 return cast_uint(len);
428} 454}
429 455
diff --git a/luaconf.h b/luaconf.h
index 80349acc..afc1b8b5 100644
--- a/luaconf.h
+++ b/luaconf.h
@@ -416,8 +416,13 @@
416@@ l_floatatt(x) corrects float attribute 'x' to the proper float type 416@@ l_floatatt(x) corrects float attribute 'x' to the proper float type
417** by prefixing it with one of FLT/DBL/LDBL. 417** by prefixing it with one of FLT/DBL/LDBL.
418@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats. 418@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats.
419@@ LUA_NUMBER_FMT is the format for writing floats. 419@@ LUA_NUMBER_FMT is the format for writing floats with the maximum
420@@ lua_number2str converts a float to a string. 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.)
421@@ l_mathop allows the addition of an 'l' or 'f' to all math operations. 426@@ l_mathop allows the addition of an 'l' or 'f' to all math operations.
422@@ l_floor takes the floor of a float. 427@@ l_floor takes the floor of a float.
423@@ lua_str2number converts a decimal numeral to a number. 428@@ lua_str2number converts a decimal numeral to a number.
@@ -428,8 +433,6 @@
428 433
429#define l_floor(x) (l_mathop(floor)(x)) 434#define l_floor(x) (l_mathop(floor)(x))
430 435
431#define lua_number2str(s,sz,n) \
432 l_sprintf((s), sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)(n))
433 436
434/* 437/*
435@@ lua_numbertointeger converts a float number with an integral value 438@@ lua_numbertointeger converts a float number with an integral value
@@ -458,6 +461,7 @@
458 461
459#define LUA_NUMBER_FRMLEN "" 462#define LUA_NUMBER_FRMLEN ""
460#define LUA_NUMBER_FMT "%.7g" 463#define LUA_NUMBER_FMT "%.7g"
464#define LUA_NUMBER_FMT_N "%.9g"
461 465
462#define l_mathop(op) op##f 466#define l_mathop(op) op##f
463 467
@@ -474,6 +478,7 @@
474 478
475#define LUA_NUMBER_FRMLEN "L" 479#define LUA_NUMBER_FRMLEN "L"
476#define LUA_NUMBER_FMT "%.19Lg" 480#define LUA_NUMBER_FMT "%.19Lg"
481#define LUA_NUMBER_FMT_N "%.21Lg"
477 482
478#define l_mathop(op) op##l 483#define l_mathop(op) op##l
479 484
@@ -488,7 +493,8 @@
488#define LUAI_UACNUMBER double 493#define LUAI_UACNUMBER double
489 494
490#define LUA_NUMBER_FRMLEN "" 495#define LUA_NUMBER_FRMLEN ""
491#define LUA_NUMBER_FMT "%.14g" 496#define LUA_NUMBER_FMT "%.15g"
497#define LUA_NUMBER_FMT_N "%.17g"
492 498
493#define l_mathop(op) op 499#define l_mathop(op) op
494 500
diff --git a/testes/math.lua b/testes/math.lua
index 0191f7dd..3937b9ce 100644
--- a/testes/math.lua
+++ b/testes/math.lua
@@ -22,6 +22,18 @@ do
22 end 22 end
23end 23end
24 24
25
26-- maximum exponent for a floating-point number
27local maxexp = 0
28do
29 local p = 2.0
30 while p < math.huge do
31 maxexp = maxexp + 1
32 p = p + p
33 end
34end
35
36
25local function isNaN (x) 37local function isNaN (x)
26 return (x ~= x) 38 return (x ~= x)
27end 39end
@@ -34,8 +46,8 @@ do
34 local x = 2.0^floatbits 46 local x = 2.0^floatbits
35 assert(x > x - 1.0 and x == x + 1.0) 47 assert(x > x - 1.0 and x == x + 1.0)
36 48
37 print(string.format("%d-bit integers, %d-bit (mantissa) floats", 49 local msg = " %d-bit integers, %d-bit*2^%d floats"
38 intbits, floatbits)) 50 print(string.format(msg, intbits, floatbits, maxexp))
39end 51end
40 52
41assert(math.type(0) == "integer" and math.type(0.0) == "float" 53assert(math.type(0) == "integer" and math.type(0.0) == "float"
@@ -803,7 +815,11 @@ do
803end 815end
804 816
805 817
806print("testing 'math.random'") 818--
819-- [[==================================================================
820 print("testing 'math.random'")
821-- -===================================================================
822--
807 823
808local random, max, min = math.random, math.max, math.min 824local random, max, min = math.random, math.max, math.min
809 825
@@ -1019,6 +1035,90 @@ assert(not pcall(random, minint + 1, minint))
1019assert(not pcall(random, maxint, maxint - 1)) 1035assert(not pcall(random, maxint, maxint - 1))
1020assert(not pcall(random, maxint, minint)) 1036assert(not pcall(random, maxint, minint))
1021 1037
1038-- ]]==================================================================
1039
1040
1041--
1042-- [[==================================================================
1043 print("testing precision of 'tostring'")
1044-- -===================================================================
1045--
1046
1047-- number of decimal digits supported by float precision
1048local decdig = math.floor(floatbits * math.log(2, 10))
1049print(string.format(" %d-digit float numbers with full precision",
1050 decdig))
1051-- number of decimal digits supported by integer precision
1052local Idecdig = math.floor(math.log(maxint, 10))
1053print(string.format(" %d-digit integer numbers with full precision",
1054 Idecdig))
1055
1056do
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
1120end
1121-- ]]==================================================================
1022 1122
1023 1123
1024print('OK') 1124print('OK')