Skip to content

Commit d3ca557

Browse files
committed
Issue #5864: Fix problem with empty code formatting for floats,
where a bogus trailing zero could be added.
1 parent b507d2e commit d3ca557

File tree

4 files changed

+124
-78
lines changed

4 files changed

+124
-78
lines changed

Lib/test/formatfloat_testcases.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@
339339
%s 1e10 -> 10000000000.0
340340
%s 9.999e10 -> 99990000000.0
341341
%s 99999999999 -> 99999999999.0
342+
%s 99999999999.9 -> 99999999999.9
343+
%s 99999999999.99 -> 1e+11
342344
%s 1e11 -> 1e+11
343345
%s 1e12 -> 1e+12
344346

Lib/test/test_float.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ def test_format_testfile(self):
328328
self.assertEqual(fmt % float(arg), rhs)
329329
self.assertEqual(fmt % -float(arg), '-' + rhs)
330330

331+
def test_issue5864(self):
332+
self.assertEquals(format(123.456, '.4'), '123.5')
333+
self.assertEquals(format(1234.56, '.4'), '1.235e+03')
334+
self.assertEquals(format(12345.6, '.4'), '1.235e+04')
335+
331336
class ReprTestCase(unittest.TestCase):
332337
def test_repr(self):
333338
floats_file = open(os.path.join(os.path.split(__file__)[0],

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ What's New in Python 3.1 beta 1?
1212
Core and Builtins
1313
-----------------
1414

15+
- Issue #5864: Fix empty format code formatting for floats so that it
16+
never gives more than the requested number of significant digits.
17+
1518
- Issue #5793: Rationalize isdigit / isalpha / tolower, etc. Includes
1619
new Py_ISDIGIT / Py_ISALPHA / Py_TOLOWER, etc. in pctypes.h.
1720

Python/pystrtod.c

Lines changed: 114 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -354,23 +354,72 @@ ensure_minimum_exponent_length(char* buffer, size_t buf_size)
354354
}
355355
}
356356

357-
/* Ensure that buffer has a decimal point in it. The decimal point will not
358-
be in the current locale, it will always be '.'. Don't add a decimal if an
359-
exponent is present. */
357+
/* Remove trailing zeros after the decimal point from a numeric string; also
358+
remove the decimal point if all digits following it are zero. The numeric
359+
string must end in '\0', and should not have any leading or trailing
360+
whitespace. Assumes that the decimal point is '.'. */
360361
Py_LOCAL_INLINE(void)
361-
ensure_decimal_point(char* buffer, size_t buf_size)
362+
remove_trailing_zeros(char *buffer)
363+
{
364+
char *old_fraction_end, *new_fraction_end, *end, *p;
365+
366+
p = buffer;
367+
if (*p == '-' || *p == '+')
368+
/* Skip leading sign, if present */
369+
++p;
370+
while (Py_ISDIGIT(*p))
371+
++p;
372+
373+
/* if there's no decimal point there's nothing to do */
374+
if (*p++ != '.')
375+
return;
376+
377+
/* scan any digits after the point */
378+
while (Py_ISDIGIT(*p))
379+
++p;
380+
old_fraction_end = p;
381+
382+
/* scan up to ending '\0' */
383+
while (*p != '\0')
384+
p++;
385+
/* +1 to make sure that we move the null byte as well */
386+
end = p+1;
387+
388+
/* scan back from fraction_end, looking for removable zeros */
389+
p = old_fraction_end;
390+
while (*(p-1) == '0')
391+
--p;
392+
/* and remove point if we've got that far */
393+
if (*(p-1) == '.')
394+
--p;
395+
new_fraction_end = p;
396+
397+
memmove(new_fraction_end, old_fraction_end, end-old_fraction_end);
398+
}
399+
400+
/* Ensure that buffer has a decimal point in it. The decimal point will not
401+
be in the current locale, it will always be '.'. Don't add a decimal point
402+
if an exponent is present. Also, convert to exponential notation where
403+
adding a '.0' would produce too many significant digits (see issue 5864).
404+
405+
Returns a pointer to the fixed buffer, or NULL on failure.
406+
*/
407+
Py_LOCAL_INLINE(char *)
408+
ensure_decimal_point(char* buffer, size_t buf_size, int precision)
362409
{
363-
int insert_count = 0;
364-
char* chars_to_insert;
410+
int digit_count, insert_count = 0, convert_to_exp = 0;
411+
char *chars_to_insert, *digits_start;
365412

366413
/* search for the first non-digit character */
367414
char *p = buffer;
368415
if (*p == '-' || *p == '+')
369416
/* Skip leading sign, if present. I think this could only
370417
ever be '-', but it can't hurt to check for both. */
371418
++p;
419+
digits_start = p;
372420
while (*p && Py_ISDIGIT(*p))
373421
++p;
422+
digit_count = Py_SAFE_DOWNCAST(p - digits_start, Py_ssize_t, int);
374423

375424
if (*p == '.') {
376425
if (Py_ISDIGIT(*(p+1))) {
@@ -380,15 +429,31 @@ ensure_decimal_point(char* buffer, size_t buf_size)
380429
else {
381430
/* We have a decimal point, but no following
382431
digit. Insert a zero after the decimal. */
432+
/* can't ever get here via PyOS_double_to_string */
433+
assert(precision == -1);
383434
++p;
384435
chars_to_insert = "0";
385436
insert_count = 1;
386437
}
387438
}
388439
else if (!(*p == 'e' || *p == 'E')) {
389440
/* Don't add ".0" if we have an exponent. */
390-
chars_to_insert = ".0";
391-
insert_count = 2;
441+
if (digit_count == precision) {
442+
/* issue 5864: don't add a trailing .0 in the case
443+
where the '%g'-formatted result already has as many
444+
significant digits as were requested. Switch to
445+
exponential notation instead. */
446+
convert_to_exp = 1;
447+
/* no exponent, no point, and we shouldn't land here
448+
for infs and nans, so we must be at the end of the
449+
string. */
450+
assert(*p == '\0');
451+
}
452+
else {
453+
assert(precision == -1 || digit_count < precision);
454+
chars_to_insert = ".0";
455+
insert_count = 2;
456+
}
392457
}
393458
if (insert_count) {
394459
size_t buf_len = strlen(buffer);
@@ -403,6 +468,30 @@ ensure_decimal_point(char* buffer, size_t buf_size)
403468
memcpy(p, chars_to_insert, insert_count);
404469
}
405470
}
471+
if (convert_to_exp) {
472+
int written;
473+
size_t buf_avail;
474+
p = digits_start;
475+
/* insert decimal point */
476+
assert(digit_count >= 1);
477+
memmove(p+2, p+1, digit_count); /* safe, but overwrites nul */
478+
p[1] = '.';
479+
p += digit_count+1;
480+
assert(p <= buf_size+buffer);
481+
buf_avail = buf_size+buffer-p;
482+
if (buf_avail == 0)
483+
return NULL;
484+
/* Add exponent. It's okay to use lower case 'e': we only
485+
arrive here as a result of using the empty format code or
486+
repr/str builtins and those never want an upper case 'E' */
487+
written = PyOS_snprintf(p, buf_avail, "e%+.02d", digit_count-1);
488+
if (!(0 <= written &&
489+
written < Py_SAFE_DOWNCAST(buf_avail, size_t, int)))
490+
/* output truncated, or something else bad happened */
491+
return NULL;
492+
remove_trailing_zeros(buffer);
493+
}
494+
return buffer;
406495
}
407496

408497
/* see FORMATBUFLEN in unicodeobject.c */
@@ -425,12 +514,14 @@ ensure_decimal_point(char* buffer, size_t buf_size)
425514
* at least one digit after the decimal.
426515
*
427516
* Return value: The pointer to the buffer with the converted string.
517+
* On failure returns NULL but does not set any Python exception.
428518
**/
429519
char *
430520
_PyOS_ascii_formatd(char *buffer,
431521
size_t buf_size,
432522
const char *format,
433-
double d)
523+
double d,
524+
int precision)
434525
{
435526
char format_char;
436527
size_t format_len = strlen(format);
@@ -495,9 +586,12 @@ _PyOS_ascii_formatd(char *buffer,
495586
ensure_minimum_exponent_length(buffer, buf_size);
496587

497588
/* If format_char is 'Z', make sure we have at least one character
498-
after the decimal point (and make sure we have a decimal point). */
589+
after the decimal point (and make sure we have a decimal point);
590+
also switch to exponential notation in some edge cases where the
591+
extra character would produce more significant digits that we
592+
really want. */
499593
if (format_char == 'Z')
500-
ensure_decimal_point(buffer, buf_size);
594+
buffer = ensure_decimal_point(buffer, buf_size, precision);
501595

502596
return buffer;
503597
}
@@ -513,57 +607,13 @@ PyOS_ascii_formatd(char *buffer,
513607
"use PyOS_double_to_string instead", 1) < 0)
514608
return NULL;
515609

516-
return _PyOS_ascii_formatd(buffer, buf_size, format, d);
610+
return _PyOS_ascii_formatd(buffer, buf_size, format, d, -1);
517611
}
518612

519613
#ifdef PY_NO_SHORT_FLOAT_REPR
520614

521615
/* The fallback code to use if _Py_dg_dtoa is not available. */
522616

523-
/* Remove trailing zeros after the decimal point from a numeric string; also
524-
remove the decimal point if all digits following it are zero. The numeric
525-
string must end in '\0', and should not have any leading or trailing
526-
whitespace. Assumes that the decimal point is '.'. */
527-
Py_LOCAL_INLINE(void)
528-
remove_trailing_zeros(char *buffer)
529-
{
530-
char *old_fraction_end, *new_fraction_end, *end, *p;
531-
532-
p = buffer;
533-
if (*p == '-' || *p == '+')
534-
/* Skip leading sign, if present */
535-
++p;
536-
while (isdigit(Py_CHARMASK(*p)))
537-
++p;
538-
539-
/* if there's no decimal point there's nothing to do */
540-
if (*p++ != '.')
541-
return;
542-
543-
/* scan any digits after the point */
544-
while (isdigit(Py_CHARMASK(*p)))
545-
++p;
546-
old_fraction_end = p;
547-
548-
/* scan up to ending '\0' */
549-
while (*p != '\0')
550-
p++;
551-
/* +1 to make sure that we move the null byte as well */
552-
end = p+1;
553-
554-
/* scan back from fraction_end, looking for removable zeros */
555-
p = old_fraction_end;
556-
while (*(p-1) == '0')
557-
--p;
558-
/* and remove point if we've got that far */
559-
if (*(p-1) == '.')
560-
--p;
561-
new_fraction_end = p;
562-
563-
memmove(new_fraction_end, old_fraction_end, end-old_fraction_end);
564-
}
565-
566-
567617
PyAPI_FUNC(char *) PyOS_double_to_string(double val,
568618
char format_code,
569619
int precision,
@@ -577,7 +627,6 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val,
577627
char *p;
578628
int t;
579629
int upper = 0;
580-
int strip_trailing_zeros = 0;
581630

582631
/* Validate format_code, and map upper and lower case */
583632
switch (format_code) {
@@ -612,17 +661,8 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val,
612661
PyErr_BadInternalCall();
613662
return NULL;
614663
}
615-
/* switch to exponential notation at 1e11, or 1e12 if we're
616-
not adding a .0 */
617-
if (fabs(val) >= (flags & Py_DTSF_ADD_DOT_0 ? 1e11 : 1e12)) {
618-
precision = 11;
619-
format_code = 'e';
620-
strip_trailing_zeros = 1;
621-
}
622-
else {
623-
precision = 12;
624-
format_code = 'g';
625-
}
664+
precision = 12;
665+
format_code = 'g';
626666
break;
627667
default:
628668
PyErr_BadInternalCall();
@@ -641,18 +681,13 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val,
641681
t = Py_DTST_INFINITE;
642682
} else {
643683
t = Py_DTST_FINITE;
644-
645-
646-
if ((flags & Py_DTSF_ADD_DOT_0) && (format_code != 'e'))
684+
if (flags & Py_DTSF_ADD_DOT_0)
647685
format_code = 'Z';
648686

649687
PyOS_snprintf(format, sizeof(format), "%%%s.%i%c",
650688
(flags & Py_DTSF_ALT ? "#" : ""), precision,
651689
format_code);
652-
_PyOS_ascii_formatd(buf, sizeof(buf), format, val);
653-
/* remove trailing zeros if necessary */
654-
if (strip_trailing_zeros)
655-
remove_trailing_zeros(buf);
690+
_PyOS_ascii_formatd(buf, sizeof(buf), format, val, precision);
656691
}
657692

658693
len = strlen(buf);
@@ -678,7 +713,7 @@ PyAPI_FUNC(char *) PyOS_double_to_string(double val,
678713
/* Convert to upper case. */
679714
char *p1;
680715
for (p1 = p; *p1; p1++)
681-
*p1 = toupper(*p1);
716+
*p1 = Py_TOUPPER(*p1);
682717
}
683718

684719
if (type)
@@ -766,7 +801,7 @@ format_float_short(double d, char format_code,
766801
assert(digits_end != NULL && digits_end >= digits);
767802
digits_len = digits_end - digits;
768803

769-
if (digits_len && !isdigit(Py_CHARMASK(digits[0]))) {
804+
if (digits_len && !Py_ISDIGIT(digits[0])) {
770805
/* Infinities and nans here; adapt Gay's output,
771806
so convert Infinity to inf and NaN to nan, and
772807
ignore sign of nan. Then return. */
@@ -851,7 +886,8 @@ format_float_short(double d, char format_code,
851886
vdigits_end = decpt + precision;
852887
break;
853888
case 'g':
854-
if (decpt <= -4 || decpt > precision)
889+
if (decpt <= -4 || decpt >
890+
(add_dot_0_if_integer ? precision-1 : precision))
855891
use_exp = 1;
856892
if (use_alt_formatting)
857893
vdigits_end = precision;

0 commit comments

Comments
 (0)