diff options
author | Roberto Ierusalimschy <roberto@inf.puc-rio.br> | 2024-08-02 15:09:30 -0300 |
---|---|---|
committer | Roberto Ierusalimschy <roberto@inf.puc-rio.br> | 2024-08-02 15:09:30 -0300 |
commit | 1bf4b80f1ace8384eb9dd6f7f8b67256b3944a7a (patch) | |
tree | 5162b1f662c4dcb6127ba03866096f70d0c298ab | |
parent | 4c6afbcb01d1cae72d829af5301df5f592fa2079 (diff) | |
download | lua-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.c | 52 | ||||
-rw-r--r-- | luaconf.h | 16 | ||||
-rw-r--r-- | testes/math.lua | 106 |
3 files changed, 153 insertions, 21 deletions
@@ -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 | */ | ||
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. | ||
414 | */ | 444 | */ |
415 | static unsigned tostringbuff (TValue *obj, char *buff) { | 445 | static 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 | ||
@@ -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 |
23 | end | 23 | end |
24 | 24 | ||
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 | |||
25 | local function isNaN (x) | 37 | local function isNaN (x) |
26 | return (x ~= x) | 38 | return (x ~= x) |
27 | end | 39 | end |
@@ -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)) |
39 | end | 51 | end |
40 | 52 | ||
41 | assert(math.type(0) == "integer" and math.type(0.0) == "float" | 53 | assert(math.type(0) == "integer" and math.type(0.0) == "float" |
@@ -803,7 +815,11 @@ do | |||
803 | end | 815 | end |
804 | 816 | ||
805 | 817 | ||
806 | print("testing 'math.random'") | 818 | -- |
819 | -- [[================================================================== | ||
820 | print("testing 'math.random'") | ||
821 | -- -=================================================================== | ||
822 | -- | ||
807 | 823 | ||
808 | local random, max, min = math.random, math.max, math.min | 824 | local random, max, min = math.random, math.max, math.min |
809 | 825 | ||
@@ -1019,6 +1035,90 @@ assert(not pcall(random, minint + 1, minint)) | |||
1019 | assert(not pcall(random, maxint, maxint - 1)) | 1035 | assert(not pcall(random, maxint, maxint - 1)) |
1020 | assert(not pcall(random, maxint, minint)) | 1036 | assert(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 | ||
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 | -- ]]================================================================== | ||
1022 | 1122 | ||
1023 | 1123 | ||
1024 | print('OK') | 1124 | print('OK') |