mirror of
https://github.com/python/cpython.git
synced 2025-07-27 21:24:32 +00:00
Issue #5864: format(1234.5, '.4') gives misleading result
(Backport of r72109 from py3k.)
This commit is contained in:
parent
867475c970
commit
92fcc9c991
3 changed files with 112 additions and 11 deletions
|
@ -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)
|
||||
{
|
||||
int insert_count = 0;
|
||||
char* chars_to_insert;
|
||||
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 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. */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue