diff options
author | Mark Dickinson <dickinsm@gmail.com> | 2009-04-29 20:41:00 (GMT) |
---|---|---|
committer | Mark Dickinson <dickinsm@gmail.com> | 2009-04-29 20:41:00 (GMT) |
commit | 92fcc9c9919b41a611b1dc084ad8f414f1e4278d (patch) | |
tree | b13d3ca4bad7c446d6b26c8efaffefca3e51eedd /Python | |
parent | 867475c970417edc25f800c4b2aeb8a3108b62db (diff) | |
download | cpython-92fcc9c9919b41a611b1dc084ad8f414f1e4278d.zip cpython-92fcc9c9919b41a611b1dc084ad8f414f1e4278d.tar.gz cpython-92fcc9c9919b41a611b1dc084ad8f414f1e4278d.tar.bz2 |
Issue #5864: format(1234.5, '.4') gives misleading result
(Backport of r72109 from py3k.)
Diffstat (limited to 'Python')
-rw-r--r-- | Python/pystrtod.c | 115 |
1 files changed, 104 insertions, 11 deletions
diff --git a/Python/pystrtod.c b/Python/pystrtod.c index b24bc97..2df4e3d 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -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. */ |