summaryrefslogtreecommitdiffstats
path: root/Python
diff options
context:
space:
mode:
authorMark Dickinson <dickinsm@gmail.com>2009-04-29 20:41:00 (GMT)
committerMark Dickinson <dickinsm@gmail.com>2009-04-29 20:41:00 (GMT)
commit92fcc9c9919b41a611b1dc084ad8f414f1e4278d (patch)
treeb13d3ca4bad7c446d6b26c8efaffefca3e51eedd /Python
parent867475c970417edc25f800c4b2aeb8a3108b62db (diff)
downloadcpython-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.c115
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. */