From 44e7ee2e356452139156e8175c46f646835d27ff Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Sat, 4 Mar 2017 23:14:52 -0800 Subject: [PATCH] Fewer rounding errors with (format "%f" fixnum) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * etc/NEWS: Document this. * src/editfns.c (styled_format): When formatting integers via a floating-point format, use long double instead of double conversion, if long double’s extra precision might help. --- etc/NEWS | 8 ++++++++ src/editfns.c | 56 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index a8db54c51ef..9c995930fbf 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -907,6 +907,14 @@ compares their numerical values. According to this predicate, due to internal rounding errors. For example, (< most-positive-fixnum (+ 1.0 most-positive-fixnum)) now correctly returns t on 64-bit hosts. +--- +** On hosts like GNU/Linux x86-64 where a 'long double' fraction +contains at least EMACS_INT_WIDTH - 3 bits, 'format' no longer returns +incorrect answers due to internal rounding errors when formatting +Emacs integers with %e, %f, or %g conversions. For example, on these +hosts (eql N (string-to-number (format "%.0f" N))) now returns t for +all Emacs integers N. + +++ ** The new function 'char-from-name' converts a Unicode name string to the corresponding character code. diff --git a/src/editfns.c b/src/editfns.c index db9ad066909..612893c377b 100644 --- a/src/editfns.c +++ b/src/editfns.c @@ -4145,6 +4145,9 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) } } + bool float_conversion + = conversion == 'e' || conversion == 'f' || conversion == 'g'; + if (conversion == 's') { /* handle case (precision[n] >= 0) */ @@ -4229,8 +4232,7 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) } } else if (! (conversion == 'c' || conversion == 'd' - || conversion == 'e' || conversion == 'f' - || conversion == 'g' || conversion == 'i' + || float_conversion || conversion == 'i' || conversion == 'o' || conversion == 'x' || conversion == 'X')) error ("Invalid format operation %%%c", @@ -4242,11 +4244,22 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) { enum { + /* Lower bound on the number of bits per + base-FLT_RADIX digit. */ + DIG_BITS_LBOUND = FLT_RADIX < 16 ? 1 : 4, + + /* 1 if integers should be formatted as long doubles, + because they may be so large that there is a rounding + error when converting them to double, and long doubles + are wider than doubles. */ + INT_AS_LDBL = (DIG_BITS_LBOUND * DBL_MANT_DIG < FIXNUM_BITS - 1 + && DBL_MANT_DIG < LDBL_MANT_DIG), + /* Maximum precision for a %f conversion such that the trailing output digit might be nonzero. Any precision larger than this will not yield useful information. */ USEFUL_PRECISION_MAX = - ((1 - DBL_MIN_EXP) + ((1 - LDBL_MIN_EXP) * (FLT_RADIX == 2 || FLT_RADIX == 10 ? 1 : FLT_RADIX == 16 ? 4 : -1)), @@ -4255,7 +4268,7 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) precision is no more than USEFUL_PRECISION_MAX. On all practical hosts, %f is the worst case. */ SPRINTF_BUFSIZE = - sizeof "-." + (DBL_MAX_10_EXP + 1) + USEFUL_PRECISION_MAX, + sizeof "-." + (LDBL_MAX_10_EXP + 1) + USEFUL_PRECISION_MAX, /* Length of pM (that is, of pMd without the trailing "d"). */ @@ -4269,9 +4282,10 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) /* Create the copy of the conversion specification, with any width and precision removed, with ".*" inserted, + with "L" possibly inserted for floating-point formats, and with pM inserted for integer formats. At most two flags F can be specified at once. */ - char convspec[sizeof "%FF.*d" + pMlen]; + char convspec[sizeof "%FF.*d" + max (INT_AS_LDBL, pMlen)]; { char *f = convspec; *f++ = '%'; @@ -4281,9 +4295,15 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) *f = '#'; f += sharp_flag; *f++ = '.'; *f++ = '*'; - if (conversion == 'd' || conversion == 'i' - || conversion == 'o' || conversion == 'x' - || conversion == 'X') + if (float_conversion) + { + if (INT_AS_LDBL) + { + *f = 'L'; + f += INTEGERP (args[n]); + } + } + else if (conversion != 'c') { memcpy (f, pMd, pMlen); f += pMlen; @@ -4310,9 +4330,20 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) not suitable here. */ char sprintf_buf[SPRINTF_BUFSIZE]; ptrdiff_t sprintf_bytes; - if (conversion == 'e' || conversion == 'f' || conversion == 'g') - sprintf_bytes = sprintf (sprintf_buf, convspec, prec, - XFLOATINT (args[n])); + if (float_conversion) + { + if (INT_AS_LDBL && INTEGERP (args[n])) + { + /* Although long double may have a rounding error if + DIG_BITS_LBOUND * LDBL_MANT_DIG < FIXNUM_BITS - 1, + it is more accurate than plain 'double'. */ + long double x = XINT (args[n]); + sprintf_bytes = sprintf (sprintf_buf, convspec, prec, x); + } + else + sprintf_bytes = sprintf (sprintf_buf, convspec, prec, + XFLOATINT (args[n])); + } else if (conversion == 'c') { /* Don't use sprintf here, as it might mishandle prec. */ @@ -4374,8 +4405,7 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message) uintmax_t leading_zeros = 0, trailing_zeros = 0; if (excess_precision) { - if (conversion == 'e' || conversion == 'f' - || conversion == 'g') + if (float_conversion) { if ((conversion == 'g' && ! sharp_flag) || ! ('0' <= sprintf_buf[sprintf_bytes - 1] -- 2.39.5