Index: Python/pystrtod.c =================================================================== --- Python/pystrtod.c (revision 72085) +++ Python/pystrtod.c (working copy) @@ -355,13 +355,17 @@ } /* 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. */ -Py_LOCAL_INLINE(void) -ensure_decimal_point(char* buffer, size_t buf_size) + 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; @@ -369,8 +373,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))) { @@ -387,8 +393,21 @@ } 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 { + chars_to_insert = ".0"; + insert_count = 2; + } } if (insert_count) { size_t buf_len = strlen(buffer); @@ -403,6 +422,29 @@ 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 may overwrite + trailing nul byte */ + p[1] = '.'; + p += digit_count+1; + buf_avail = buf_size-(p-buffer); + if (buf_avail <= 0) + return NULL; + /* 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 these 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; + } + return buffer; } /* see FORMATBUFLEN in unicodeobject.c */ @@ -425,12 +467,14 @@ * 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. **/ char * _PyOS_ascii_formatd(char *buffer, size_t buf_size, const char *format, - double d) + double d, + int precision) { char format_char; size_t format_len = strlen(format); @@ -497,7 +541,7 @@ /* 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). */ if (format_char == 'Z') - ensure_decimal_point(buffer, buf_size); + buffer = ensure_decimal_point(buffer, buf_size, precision); return buffer; } @@ -513,7 +557,7 @@ "use PyOS_double_to_string instead", 1) < 0) return NULL; - return _PyOS_ascii_formatd(buffer, buf_size, format, d); + return _PyOS_ascii_formatd(buffer, buf_size, format, d, -1); } #ifdef PY_NO_SHORT_FLOAT_REPR @@ -649,7 +693,7 @@ PyOS_snprintf(format, sizeof(format), "%%%s.%i%c", (flags & Py_DTSF_ALT ? "#" : ""), precision, format_code); - _PyOS_ascii_formatd(buf, sizeof(buf), format, val); + _PyOS_ascii_formatd(buf, sizeof(buf), format, val, precision); /* remove trailing zeros if necessary */ if (strip_trailing_zeros) remove_trailing_zeros(buf); @@ -851,7 +895,8 @@ vdigits_end = decpt + precision; break; case 'g': - if (decpt <= -4 || decpt > precision) + if (decpt <= -4 || decpt > + (add_dot_0_if_integer ? precision-1 : precision)) use_exp = 1; if (use_alt_formatting) vdigits_end = precision; Index: Lib/test/formatfloat_testcases.txt =================================================================== --- Lib/test/formatfloat_testcases.txt (revision 72085) +++ Lib/test/formatfloat_testcases.txt (working copy) @@ -339,6 +339,8 @@ %s 1e10 -> 10000000000.0 %s 9.999e10 -> 99990000000.0 %s 99999999999 -> 99999999999.0 +%s 99999999999.9 -> 99999999999.9 +%s 99999999999.99 -> 1e+11 %s 1e11 -> 1e+11 %s 1e12 -> 1e+12 Index: Lib/test/test_float.py =================================================================== --- Lib/test/test_float.py (revision 72085) +++ Lib/test/test_float.py (working copy) @@ -328,6 +328,11 @@ self.assertEqual(fmt % float(arg), rhs) self.assertEqual(fmt % -float(arg), '-' + rhs) + 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],