]> granicus.if.org Git - python/commitdiff
Issue #5864: format(1234.5, '.4') gives misleading result
authorMark Dickinson <dickinsm@gmail.com>
Wed, 29 Apr 2009 20:41:00 +0000 (20:41 +0000)
committerMark Dickinson <dickinsm@gmail.com>
Wed, 29 Apr 2009 20:41:00 +0000 (20:41 +0000)
(Backport of r72109 from py3k.)

Lib/test/test_float.py
Misc/NEWS
Python/pystrtod.c

index 0b1ae96495bc9c27a3cf0ae7cab8398b6ee22ddd..da9c4398957f8899776b7c40e3d8083c1c0c6228 100644 (file)
@@ -253,6 +253,11 @@ class IEEEFormatTestCase(unittest.TestCase):
             self.assertEquals(math.atan2(float('-1e-1000'), -1),
                               math.atan2(-0.0, -1))
 
+    def test_issue5864(self):
+        self.assertEquals(format(123.456, '.4'), '123.5')
+        self.assertEquals(format(1234.56, '.4'), '1.235e+03')
+        self.assertEquals(format(12345.6, '.4'), '1.235e+04')
+
 class ReprTestCase(unittest.TestCase):
     def test_repr(self):
         floats_file = open(os.path.join(os.path.split(__file__)[0],
index 3cd5094de6ebe2578fcb2b4e3daf89e0efb40bb5..35846eae784cba94ff6322fdc6b2b045aa7b88a6 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -12,6 +12,9 @@ What's New in Python 2.7 alpha 1
 Core and Builtins
 -----------------
 
+- Issue #5864: Fix empty format code formatting for floats so that it
+  never gives more than the requested number of significant digits.
+
 - Issue #5793: Rationalize isdigit / isalpha / tolower, etc. Includes
   new Py_ISDIGIT / Py_ISALPHA / Py_TOLOWER, etc. in pctypes.h.
 
index b24bc9745f2dd2ecab924573636a2d420f1555ee..2df4e3d0ad23a7c49746218ee2814ddae4c4f4c6 100644 (file)
@@ -348,14 +348,61 @@ ensure_minimum_exponent_length(char* buffer, size_t buf_size)
        }
 }
 
-/* Ensure that buffer has a decimal point in it.  The decimal point will not
-   be in the current locale, it will always be '.'. Don't add a decimal if an
-   exponent is present. */
+/* Remove trailing zeros after the decimal point from a numeric string; also
+   remove the decimal point if all digits following it are zero.  The numeric
+   string must end in '\0', and should not have any leading or trailing
+   whitespace.  Assumes that the decimal point is '.'. */
 Py_LOCAL_INLINE(void)
-ensure_decimal_point(char* buffer, size_t buf_size)
+remove_trailing_zeros(char *buffer)
+{
+       char *old_fraction_end, *new_fraction_end, *end, *p;
+
+       p = buffer;
+       if (*p == '-' || *p == '+')
+               /* Skip leading sign, if present */
+               ++p;
+       while (Py_ISDIGIT(*p))
+               ++p;
+
+       /* if there's no decimal point there's nothing to do */
+       if (*p++ != '.')
+               return;
+
+       /* scan any digits after the point */
+       while (Py_ISDIGIT(*p))
+               ++p;
+       old_fraction_end = p;
+
+       /* scan up to ending '\0' */
+       while (*p != '\0')
+               p++;
+       /* +1 to make sure that we move the null byte as well */
+       end = p+1;
+
+       /* scan back from fraction_end, looking for removable zeros */
+       p = old_fraction_end;
+       while (*(p-1) == '0')
+               --p;
+       /* and remove point if we've got that far */
+       if (*(p-1) == '.')
+               --p;
+       new_fraction_end = p;
+
+       memmove(new_fraction_end, old_fraction_end, end-old_fraction_end);
+}
+
+/* Ensure that buffer has a decimal point in it.  The decimal point will not
+   be in the current locale, it will always be '.'. Don't add a decimal point
+   if an exponent is present.  Also, convert to exponential notation where
+   adding a '.0' would produce too many significant digits (see issue 5864).
+
+   Returns a pointer to the fixed buffer, or NULL on failure.
+*/
+Py_LOCAL_INLINE(char *)
+ensure_decimal_point(char* buffer, size_t buf_size, int precision)
 {
-       int insert_count = 0;
-       char* chars_to_insert;
+       int digit_count, insert_count = 0, convert_to_exp = 0;
+       char* chars_to_insert, *digits_start;
 
        /* search for the first non-digit character */
        char *p = buffer;
@@ -363,8 +410,10 @@ ensure_decimal_point(char* buffer, size_t buf_size)
                /* Skip leading sign, if present.  I think this could only
                   ever be '-', but it can't hurt to check for both. */
                ++p;
+       digits_start = p;
        while (*p && Py_ISDIGIT(*p))
                ++p;
+       digit_count = Py_SAFE_DOWNCAST(p - digits_start, Py_ssize_t, int);
 
        if (*p == '.') {
                if (Py_ISDIGIT(*(p+1))) {
@@ -374,6 +423,8 @@ ensure_decimal_point(char* buffer, size_t buf_size)
                else {
                        /* We have a decimal point, but no following
                           digit.  Insert a zero after the decimal. */
+                       /* can't ever get here via PyOS_double_to_string */
+                       assert(precision == -1);
                        ++p;
                        chars_to_insert = "0";
                        insert_count = 1;
@@ -381,8 +432,22 @@ ensure_decimal_point(char* buffer, size_t buf_size)
        }
        else if (!(*p == 'e' || *p == 'E')) {
                /* Don't add ".0" if we have an exponent. */
-               chars_to_insert = ".0";
-               insert_count = 2;
+               if (digit_count == precision) {
+                       /* issue 5864: don't add a trailing .0 in the case
+                          where the '%g'-formatted result already has as many
+                          significant digits as were requested.  Switch to
+                          exponential notation instead. */
+                       convert_to_exp = 1;
+                       /* no exponent, no point, and we shouldn't land here
+                          for infs and nans, so we must be at the end of the
+                          string. */
+                       assert(*p == '\0');
+               }
+               else {
+                       assert(precision == -1 || digit_count < precision);
+                       chars_to_insert = ".0";
+                       insert_count = 2;
+               }
        }
        if (insert_count) {
                size_t buf_len = strlen(buffer);
@@ -397,6 +462,30 @@ ensure_decimal_point(char* buffer, size_t buf_size)
                        memcpy(p, chars_to_insert, insert_count);
                }
        }
+       if (convert_to_exp) {
+               int written;
+               size_t buf_avail;
+               p = digits_start;
+               /* insert decimal point */
+               assert(digit_count >= 1);
+               memmove(p+2, p+1, digit_count); /* safe, but overwrites nul */
+               p[1] = '.';
+               p += digit_count+1;
+               assert(p <= buf_size+buffer);
+               buf_avail = buf_size+buffer-p;
+               if (buf_avail == 0)
+                       return NULL;
+               /* Add exponent.  It's okay to use lower case 'e': we only
+                  arrive here as a result of using the empty format code or
+                  repr/str builtins and those never want an upper case 'E' */
+               written = PyOS_snprintf(p, buf_avail, "e%+.02d", digit_count-1);
+               if (!(0 <= written &&
+                     written < Py_SAFE_DOWNCAST(buf_avail, size_t, int)))
+                       /* output truncated, or something else bad happened */
+                       return NULL;
+               remove_trailing_zeros(buffer);
+       }
+       return buffer;
 }
 
 /* see FORMATBUFLEN in unicodeobject.c */
@@ -419,6 +508,7 @@ ensure_decimal_point(char* buffer, size_t buf_size)
  *     at least one digit after the decimal.
  *
  * Return value: The pointer to the buffer with the converted string.
+ * On failure returns NULL but does not set any Python exception.
  **/
 /* DEPRECATED, will be deleted in 2.8 and 3.2 */
 PyAPI_FUNC(char *)
@@ -495,9 +585,12 @@ PyOS_ascii_formatd(char       *buffer,
        ensure_minimum_exponent_length(buffer, buf_size);
 
        /* If format_char is 'Z', make sure we have at least one character
-          after the decimal point (and make sure we have a decimal point). */
+          after the decimal point (and make sure we have a decimal point);
+          also switch to exponential notation in some edge cases where the
+          extra character would produce more significant digits that we
+          really want. */
        if (format_char == 'Z')
-               ensure_decimal_point(buffer, buf_size);
+               buffer = ensure_decimal_point(buffer, buf_size, -1);
 
        return buffer;
 }
@@ -600,7 +693,7 @@ _PyOS_double_to_string(char *buf, size_t buf_len, double val,
                /* Possibly make sure we have at least one character after the
                   decimal point (and make sure we have a decimal point). */
                if (flags & Py_DTSF_ADD_DOT_0)
-                       ensure_decimal_point(buf, buf_len);
+                       buf = ensure_decimal_point(buf, buf_len, precision);
        }
 
        /* Add the sign if asked and the result isn't negative. */