Commit 92fcc9c9 authored by Mark Dickinson's avatar Mark Dickinson

Issue #5864: format(1234.5, '.4') gives misleading result

(Backport of r72109 from py3k.)
parent 867475c9
...@@ -253,6 +253,11 @@ class IEEEFormatTestCase(unittest.TestCase): ...@@ -253,6 +253,11 @@ class IEEEFormatTestCase(unittest.TestCase):
self.assertEquals(math.atan2(float('-1e-1000'), -1), self.assertEquals(math.atan2(float('-1e-1000'), -1),
math.atan2(-0.0, -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): class ReprTestCase(unittest.TestCase):
def test_repr(self): def test_repr(self):
floats_file = open(os.path.join(os.path.split(__file__)[0], floats_file = open(os.path.join(os.path.split(__file__)[0],
......
...@@ -12,6 +12,9 @@ What's New in Python 2.7 alpha 1 ...@@ -12,6 +12,9 @@ What's New in Python 2.7 alpha 1
Core and Builtins 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 - Issue #5793: Rationalize isdigit / isalpha / tolower, etc. Includes
new Py_ISDIGIT / Py_ISALPHA / Py_TOLOWER, etc. in pctypes.h. new Py_ISDIGIT / Py_ISALPHA / Py_TOLOWER, etc. in pctypes.h.
......
...@@ -348,14 +348,61 @@ ensure_minimum_exponent_length(char* buffer, size_t buf_size) ...@@ -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 /* Remove trailing zeros after the decimal point from a numeric string; also
be in the current locale, it will always be '.'. Don't add a decimal if an remove the decimal point if all digits following it are zero. The numeric
exponent is present. */ string must end in '\0', and should not have any leading or trailing
whitespace. Assumes that the decimal point is '.'. */
Py_LOCAL_INLINE(void) 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; int digit_count, insert_count = 0, convert_to_exp = 0;
char* chars_to_insert; char* chars_to_insert, *digits_start;
/* search for the first non-digit character */ /* search for the first non-digit character */
char *p = buffer; char *p = buffer;
...@@ -363,8 +410,10 @@ ensure_decimal_point(char* buffer, size_t buf_size) ...@@ -363,8 +410,10 @@ ensure_decimal_point(char* buffer, size_t buf_size)
/* Skip leading sign, if present. I think this could only /* Skip leading sign, if present. I think this could only
ever be '-', but it can't hurt to check for both. */ ever be '-', but it can't hurt to check for both. */
++p; ++p;
digits_start = p;
while (*p && Py_ISDIGIT(*p)) while (*p && Py_ISDIGIT(*p))
++p; ++p;
digit_count = Py_SAFE_DOWNCAST(p - digits_start, Py_ssize_t, int);
if (*p == '.') { if (*p == '.') {
if (Py_ISDIGIT(*(p+1))) { if (Py_ISDIGIT(*(p+1))) {
...@@ -374,6 +423,8 @@ ensure_decimal_point(char* buffer, size_t buf_size) ...@@ -374,6 +423,8 @@ ensure_decimal_point(char* buffer, size_t buf_size)
else { else {
/* We have a decimal point, but no following /* We have a decimal point, but no following
digit. Insert a zero after the decimal. */ digit. Insert a zero after the decimal. */
/* can't ever get here via PyOS_double_to_string */
assert(precision == -1);
++p; ++p;
chars_to_insert = "0"; chars_to_insert = "0";
insert_count = 1; insert_count = 1;
...@@ -381,9 +432,23 @@ ensure_decimal_point(char* buffer, size_t buf_size) ...@@ -381,9 +432,23 @@ ensure_decimal_point(char* buffer, size_t buf_size)
} }
else if (!(*p == 'e' || *p == 'E')) { else if (!(*p == 'e' || *p == 'E')) {
/* Don't add ".0" if we have an exponent. */ /* Don't add ".0" if we have an exponent. */
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"; chars_to_insert = ".0";
insert_count = 2; insert_count = 2;
} }
}
if (insert_count) { if (insert_count) {
size_t buf_len = strlen(buffer); size_t buf_len = strlen(buffer);
if (buf_len + insert_count + 1 >= buf_size) { if (buf_len + insert_count + 1 >= buf_size) {
...@@ -397,6 +462,30 @@ ensure_decimal_point(char* buffer, size_t buf_size) ...@@ -397,6 +462,30 @@ ensure_decimal_point(char* buffer, size_t buf_size)
memcpy(p, chars_to_insert, insert_count); 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 */ /* see FORMATBUFLEN in unicodeobject.c */
...@@ -419,6 +508,7 @@ ensure_decimal_point(char* buffer, size_t buf_size) ...@@ -419,6 +508,7 @@ ensure_decimal_point(char* buffer, size_t buf_size)
* at least one digit after the decimal. * at least one digit after the decimal.
* *
* Return value: The pointer to the buffer with the converted string. * 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 */ /* DEPRECATED, will be deleted in 2.8 and 3.2 */
PyAPI_FUNC(char *) PyAPI_FUNC(char *)
...@@ -495,9 +585,12 @@ PyOS_ascii_formatd(char *buffer, ...@@ -495,9 +585,12 @@ PyOS_ascii_formatd(char *buffer,
ensure_minimum_exponent_length(buffer, buf_size); ensure_minimum_exponent_length(buffer, buf_size);
/* If format_char is 'Z', make sure we have at least one character /* 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') if (format_char == 'Z')
ensure_decimal_point(buffer, buf_size); buffer = ensure_decimal_point(buffer, buf_size, -1);
return buffer; return buffer;
} }
...@@ -600,7 +693,7 @@ _PyOS_double_to_string(char *buf, size_t buf_len, double val, ...@@ -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 /* Possibly make sure we have at least one character after the
decimal point (and make sure we have a decimal point). */ decimal point (and make sure we have a decimal point). */
if (flags & Py_DTSF_ADD_DOT_0) 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. */ /* Add the sign if asked and the result isn't negative. */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment