[Python-checkins] r72119 - in python/trunk: Lib/test/test_float.py Misc/NEWS Python/pystrtod.c

mark.dickinson python-checkins at python.org
Wed Apr 29 22:41:00 CEST 2009


Author: mark.dickinson
Date: Wed Apr 29 22:41:00 2009
New Revision: 72119

Log:
Issue #5864: format(1234.5, '.4') gives misleading result
(Backport of r72109 from py3k.)


Modified:
   python/trunk/Lib/test/test_float.py
   python/trunk/Misc/NEWS
   python/trunk/Python/pystrtod.c

Modified: python/trunk/Lib/test/test_float.py
==============================================================================
--- python/trunk/Lib/test/test_float.py	(original)
+++ python/trunk/Lib/test/test_float.py	Wed Apr 29 22:41:00 2009
@@ -253,6 +253,11 @@
             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],

Modified: python/trunk/Misc/NEWS
==============================================================================
--- python/trunk/Misc/NEWS	(original)
+++ python/trunk/Misc/NEWS	Wed Apr 29 22:41:00 2009
@@ -12,6 +12,9 @@
 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.
 

Modified: python/trunk/Python/pystrtod.c
==============================================================================
--- python/trunk/Python/pystrtod.c	(original)
+++ python/trunk/Python/pystrtod.c	Wed Apr 29 22:41:00 2009
@@ -348,14 +348,61 @@
 	}
 }
 
-/* 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 @@
 		/* 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 @@
 		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 @@
 	}
 	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 @@
 			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 @@
  *     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 @@
 	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 @@
 		/* 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. */


More information about the Python-checkins mailing list