فهرست منبع

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".
Roberto Ierusalimschy 1 سال پیش
والد
کامیت
1bf4b80f1a
3فایلهای تغییر یافته به همراه153 افزوده شده و 21 حذف شده
  1. 39 13
      lobject.c
  2. 11 5
      luaconf.h
  3. 103 3
      testes/math.lua

+ 39 - 13
lobject.c

@@ -10,6 +10,7 @@
 #include "lprefix.h"
 
 
+#include <float.h>
 #include <locale.h>
 #include <math.h>
 #include <stdarg.h>
@@ -401,29 +402,54 @@ int luaO_utf8esc (char *buff, unsigned long x) {
 /*
 ** Maximum length of the conversion of a number to a string. Must be
 ** enough to accommodate both LUA_INTEGER_FMT and LUA_NUMBER_FMT.
-** (For a long long int, this is 19 digits plus a sign and a final '\0',
-** adding to 21. For a long double, it can go to a sign, 33 digits,
-** the dot, an exponent letter, an exponent sign, 5 exponent digits,
-** and a final '\0', adding to 43.)
+** For a long long int, this is 19 digits plus a sign and a final '\0',
+** adding to 21. For a long double, it can go to a sign, the dot, an
+** exponent letter, an exponent sign, 4 exponent digits, the final
+** '\0', plus the significant digits, which are approximately the *_DIG
+** attribute.
 */
-#define MAXNUMBER2STR	44
+#define MAXNUMBER2STR	(20 + l_floatatt(DIG))
 
 
 /*
-** Convert a number object to a string, adding it to a buffer
+** Convert a float to a string, adding it to a buffer. First try with
+** a not too large number of digits, to avoid noise (for instance,
+** 1.1 going to "1.1000000000000001"). If that lose precision, so
+** that reading the result back gives a different number, then do the
+** conversion again with extra precision. Moreover, if the numeral looks
+** like an integer (without a decimal point or an exponent), add ".0" to
+** its end.
+*/
+static int tostringbuffFloat (lua_Number n, char *buff) {
+  /* first conversion */
+  int len = l_sprintf(buff, MAXNUMBER2STR, LUA_NUMBER_FMT,
+                            (LUAI_UACNUMBER)n);
+  lua_Number check = lua_str2number(buff, NULL);  /* read it back */
+  if (check != n) {  /* not enough precision? */
+    /* convert again with more precision */
+    len = l_sprintf(buff, MAXNUMBER2STR, LUA_NUMBER_FMT_N,
+                          (LUAI_UACNUMBER)n);
+  }
+  /* looks like an integer? */
+  if (buff[strspn(buff, "-0123456789")] == '\0') {
+    buff[len++] = lua_getlocaledecpoint();
+    buff[len++] = '0';  /* adds '.0' to result */
+  }
+  return len;
+}
+
+
+/*
+** Convert a number object to a string, adding it to a buffer.
 */
 static unsigned tostringbuff (TValue *obj, char *buff) {
   int len;
   lua_assert(ttisnumber(obj));
   if (ttisinteger(obj))
     len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
-  else {
-    len = lua_number2str(buff, MAXNUMBER2STR, fltvalue(obj));
-    if (buff[strspn(buff, "-0123456789")] == '\0') {  /* looks like an int? */
-      buff[len++] = lua_getlocaledecpoint();
-      buff[len++] = '0';  /* adds '.0' to result */
-    }
-  }
+  else
+    len = tostringbuffFloat(fltvalue(obj), buff);
+  lua_assert(len < MAXNUMBER2STR);
   return cast_uint(len);
 }
 

+ 11 - 5
luaconf.h

@@ -416,8 +416,13 @@
 @@ l_floatatt(x) corrects float attribute 'x' to the proper float type
 ** by prefixing it with one of FLT/DBL/LDBL.
 @@ LUA_NUMBER_FRMLEN is the length modifier for writing floats.
-@@ LUA_NUMBER_FMT is the format for writing floats.
-@@ lua_number2str converts a float to a string.
+@@ LUA_NUMBER_FMT is the format for writing floats with the maximum
+** number of digits that respects tostring(tonumber(numeral)) == numeral.
+** (That would be floor(log10(2^n)), where n is the number of bits in
+** the float mantissa.)
+@@ LUA_NUMBER_FMT_N is the format for writing floats with the minimum
+** number of digits that ensures tonumber(tostring(number)) == number.
+** (That would be LUA_NUMBER_FMT+2.)
 @@ l_mathop allows the addition of an 'l' or 'f' to all math operations.
 @@ l_floor takes the floor of a float.
 @@ lua_str2number converts a decimal numeral to a number.
@@ -428,8 +433,6 @@
 
 #define l_floor(x)		(l_mathop(floor)(x))
 
-#define lua_number2str(s,sz,n)  \
-	l_sprintf((s), sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)(n))
 
 /*
 @@ lua_numbertointeger converts a float number with an integral value
@@ -458,6 +461,7 @@
 
 #define LUA_NUMBER_FRMLEN	""
 #define LUA_NUMBER_FMT		"%.7g"
+#define LUA_NUMBER_FMT_N	"%.9g"
 
 #define l_mathop(op)		op##f
 
@@ -474,6 +478,7 @@
 
 #define LUA_NUMBER_FRMLEN	"L"
 #define LUA_NUMBER_FMT		"%.19Lg"
+#define LUA_NUMBER_FMT_N	"%.21Lg"
 
 #define l_mathop(op)		op##l
 
@@ -488,7 +493,8 @@
 #define LUAI_UACNUMBER	double
 
 #define LUA_NUMBER_FRMLEN	""
-#define LUA_NUMBER_FMT		"%.14g"
+#define LUA_NUMBER_FMT		"%.15g"
+#define LUA_NUMBER_FMT_N	"%.17g"
 
 #define l_mathop(op)		op
 

+ 103 - 3
testes/math.lua

@@ -22,6 +22,18 @@ do
   end
 end
 
+
+-- maximum exponent for a floating-point number
+local maxexp = 0
+do
+  local p = 2.0
+  while p < math.huge do
+    maxexp = maxexp + 1
+    p = p + p
+  end
+end
+
+
 local function isNaN (x)
   return (x ~= x)
 end
@@ -34,8 +46,8 @@ do
   local x = 2.0^floatbits
   assert(x > x - 1.0 and x == x + 1.0)
 
-  print(string.format("%d-bit integers, %d-bit (mantissa) floats",
-                       intbits, floatbits))
+  local msg = "  %d-bit integers, %d-bit*2^%d floats"
+  print(string.format(msg, intbits, floatbits, maxexp))
 end
 
 assert(math.type(0) == "integer" and math.type(0.0) == "float"
@@ -803,7 +815,11 @@ do
 end
 
 
-print("testing 'math.random'")
+--
+-- [[==================================================================
+      print("testing 'math.random'")
+-- -===================================================================
+--
 
 local random, max, min = math.random, math.max, math.min
 
@@ -1019,6 +1035,90 @@ assert(not pcall(random, minint + 1, minint))
 assert(not pcall(random, maxint, maxint - 1))
 assert(not pcall(random, maxint, minint))
 
+-- ]]==================================================================
+
+
+--
+-- [[==================================================================
+    print("testing precision of 'tostring'")
+-- -===================================================================
+--
+
+-- number of decimal digits supported by float precision
+local decdig = math.floor(floatbits * math.log(2, 10))
+print(string.format("  %d-digit float numbers with full precision",
+                    decdig))
+-- number of decimal digits supported by integer precision
+local Idecdig = math.floor(math.log(maxint, 10))
+print(string.format("  %d-digit integer numbers with full precision",
+                    Idecdig))
+
+do
+  -- Any number should print so that reading it back gives itself:
+  -- tonumber(tostring(x)) == x
+
+  -- Mersenne fractions
+  local p = 1.0
+  for i = 1, maxexp do
+    p = p + p
+    local x = 1 / (p - 1)
+    assert(x == tonumber(tostring(x)))
+  end
+
+  -- some random numbers in [0,1)
+  for i = 1, 100 do
+    local x = math.random()
+    assert(x == tonumber(tostring(x)))
+  end
+
+  -- different numbers shold print differently.
+  -- check pairs of floats with minimum detectable difference
+  local p = floatbits - 1
+  for i = 1, maxexp - 1 do
+    for _, i in ipairs{-i, i} do
+      local x = 2^i
+      local diff = 2^(i - p)   -- least significant bit for 'x'
+      local y = x + diff
+      local fy = tostring(y)
+      assert(x ~= y and tostring(x) ~= fy)
+      assert(tonumber(fy) == y)
+    end
+  end
+
+
+  -- "reasonable" numerals should be printed like themselves
+
+  -- create random float numerals with 5 digits, with a decimal point
+  -- inserted in all places. (With more than 5, things like "0.00001"
+  -- reformats like "1e-5".)
+  for i = 1, 1000 do
+    -- random numeral with 5 digits
+    local x = string.format("%.5d", math.random(0, 99999))
+    for i = 2, #x do
+      -- insert decimal point at position 'i'
+      local y = string.sub(x, 1, i - 1) .. "." .. string.sub(x, i, -1)
+      y = string.gsub(y, "^0*(%d.-%d)0*$", "%1")   -- trim extra zeros
+      assert(y == tostring(tonumber(y)))
+    end
+  end
+
+  -- all-random floats
+  local Fsz = string.packsize("n")   -- size of floats in bytes
+
+  for i = 1, 400 do
+    local s = string.pack("j", math.random(0))   -- a random string of bits
+    while #s < Fsz do   -- make 's' long enough
+      s = s .. string.pack("j", math.random(0))
+    end
+    local n = string.unpack("n", s)   -- read 's' as a float
+    s = tostring(n)
+    if string.find(s, "^%-?%d") then   -- avoid NaN, inf, -inf
+      assert(tonumber(s) == n)
+    end
+  end
+
+end
+-- ]]==================================================================
 
 
 print('OK')