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') |
