Skip to content

Commit 0923d1d

Browse files
committed
The other half of Issue python#1580: use short float repr where possible.
Addresses the float -> string conversion, using David Gay's code which was added in Mark Dickinson's checkin r71663. Also addresses these, which are intertwined with the short repr changes: - Issue python#5772: format(1e100, '<') produces '1e+100', not '1.0e+100' - Issue python#5515: 'n' formatting with commas no longer works poorly with leading zeros. - PEP 378 Format Specifier for Thousands Separator: implemented for floats.
1 parent b08a53a commit 0923d1d

File tree

16 files changed

+1497
-836
lines changed

16 files changed

+1497
-836
lines changed

Include/bytesobject.h

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,22 @@ PyAPI_FUNC(int) PyBytes_AsStringAndSize(
9191
into the string pointed to by buffer. For the argument descriptions,
9292
see Objects/stringlib/localeutil.h */
9393

94-
PyAPI_FUNC(int) _PyBytes_InsertThousandsGroupingLocale(char *buffer,
95-
Py_ssize_t n_buffer,
96-
Py_ssize_t n_digits,
97-
Py_ssize_t buf_size,
98-
Py_ssize_t *count,
99-
int append_zero_char);
94+
PyAPI_FUNC(Py_ssize_t) _PyBytes_InsertThousandsGroupingLocale(char *buffer,
95+
Py_ssize_t n_buffer,
96+
char *digits,
97+
Py_ssize_t n_digits,
98+
Py_ssize_t min_width);
10099

101100
/* Using explicit passed-in values, insert the thousands grouping
102101
into the string pointed to by buffer. For the argument descriptions,
103102
see Objects/stringlib/localeutil.h */
104-
PyAPI_FUNC(int) _PyBytes_InsertThousandsGrouping(char *buffer,
105-
Py_ssize_t n_buffer,
106-
Py_ssize_t n_digits,
107-
Py_ssize_t buf_size,
108-
Py_ssize_t *count,
109-
int append_zero_char,
110-
const char *grouping,
111-
const char *thousands_sep);
103+
PyAPI_FUNC(Py_ssize_t) _PyBytes_InsertThousandsGrouping(char *buffer,
104+
Py_ssize_t n_buffer,
105+
char *digits,
106+
Py_ssize_t n_digits,
107+
Py_ssize_t min_width,
108+
const char *grouping,
109+
const char *thousands_sep);
112110

113111
/* Flags used by string formatting */
114112
#define F_LJUST (1<<0)

Include/pystrtod.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ PyAPI_FUNC(double) PyOS_ascii_strtod(const char *str, char **ptr);
1010
PyAPI_FUNC(double) PyOS_ascii_atof(const char *str);
1111
PyAPI_FUNC(char *) PyOS_ascii_formatd(char *buffer, size_t buf_len, const char *format, double d);
1212

13+
/* The caller is responsible for calling PyMem_Free to free the buffer
14+
that's is returned. */
15+
PyAPI_FUNC(char *) PyOS_double_to_string(double val,
16+
char format_code,
17+
int precision,
18+
int flags,
19+
int *type);
20+
21+
22+
/* PyOS_double_to_string's "flags" parameter can be set to 0 or more of: */
23+
#define Py_DTSF_SIGN 0x01 /* always add the sign */
24+
#define Py_DTSF_ADD_DOT_0 0x02 /* if the result is an integer add ".0" */
25+
#define Py_DTSF_ALT 0x04 /* "alternate" formatting. it's format_code
26+
specific */
27+
28+
/* PyOS_double_to_string's "type", if non-NULL, will be set to one of: */
29+
#define Py_DTST_FINITE 0
30+
#define Py_DTST_INFINITE 1
31+
#define Py_DTST_NAN 2
1332

1433
#ifdef __cplusplus
1534
}

Include/unicodeobject.h

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,24 +1482,22 @@ PyAPI_FUNC(PyObject *) _PyUnicode_XStrip(
14821482
into the string pointed to by buffer. For the argument descriptions,
14831483
see Objects/stringlib/localeutil.h */
14841484

1485-
PyAPI_FUNC(int) _PyUnicode_InsertThousandsGroupingLocale(Py_UNICODE *buffer,
1486-
Py_ssize_t n_buffer,
1487-
Py_ssize_t n_digits,
1488-
Py_ssize_t buf_size,
1489-
Py_ssize_t *count,
1490-
int append_zero_char);
1485+
PyAPI_FUNC(Py_ssize_t) _PyUnicode_InsertThousandsGroupingLocale(Py_UNICODE *buffer,
1486+
Py_ssize_t n_buffer,
1487+
Py_UNICODE *digits,
1488+
Py_ssize_t n_digits,
1489+
Py_ssize_t min_width);
14911490

14921491
/* Using explicit passed-in values, insert the thousands grouping
14931492
into the string pointed to by buffer. For the argument descriptions,
14941493
see Objects/stringlib/localeutil.h */
1495-
PyAPI_FUNC(int) _PyUnicode_InsertThousandsGrouping(Py_UNICODE *buffer,
1496-
Py_ssize_t n_buffer,
1497-
Py_ssize_t n_digits,
1498-
Py_ssize_t buf_size,
1499-
Py_ssize_t *count,
1500-
int append_zero_char,
1501-
const char *grouping,
1502-
const char *thousands_sep);
1494+
PyAPI_FUNC(Py_ssize_t) _PyUnicode_InsertThousandsGrouping(Py_UNICODE *buffer,
1495+
Py_ssize_t n_buffer,
1496+
Py_UNICODE *digits,
1497+
Py_ssize_t n_digits,
1498+
Py_ssize_t min_width,
1499+
const char *grouping,
1500+
const char *thousands_sep);
15031501
/* === Characters Type APIs =============================================== */
15041502

15051503
/* Helper array used by Py_UNICODE_ISSPACE(). */

Lib/test/test_float.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
import unittest, struct
33
import os
4+
import sys
45
from test import support
56
import math
67
from math import isinf, isnan, copysign, ldexp
@@ -10,6 +11,10 @@
1011
INF = float("inf")
1112
NAN = float("nan")
1213

14+
#locate file with float format test values
15+
test_dir = os.path.dirname(__file__) or os.curdir
16+
format_testfile = os.path.join(test_dir, 'formatfloat_testcases.txt')
17+
1318
class GeneralFloatCases(unittest.TestCase):
1419

1520
def test_float(self):
@@ -24,6 +29,10 @@ def test_float(self):
2429
self.assertRaises(ValueError, float, "+-3.14")
2530
self.assertRaises(ValueError, float, "-+3.14")
2631
self.assertRaises(ValueError, float, "--3.14")
32+
self.assertRaises(ValueError, float, ".nan")
33+
self.assertRaises(ValueError, float, "+.inf")
34+
self.assertRaises(ValueError, float, ".")
35+
self.assertRaises(ValueError, float, "-.")
2736
self.assertEqual(float(b" \u0663.\u0661\u0664 ".decode('raw-unicode-escape')), 3.14)
2837

2938
@support.run_with_locale('LC_NUMERIC', 'fr_FR', 'de_DE')
@@ -316,6 +325,73 @@ def test_repr(self):
316325
self.assertEqual(v, eval(repr(v)))
317326
floats_file.close()
318327

328+
class FormatTestCase(unittest.TestCase):
329+
@unittest.skipUnless(float.__getformat__("double").startswith("IEEE"),
330+
"test requires IEEE 754 doubles")
331+
def test_format_testfile(self):
332+
for line in open(format_testfile):
333+
if line.startswith('--'):
334+
continue
335+
line = line.strip()
336+
if not line:
337+
continue
338+
339+
lhs, rhs = map(str.strip, line.split('->'))
340+
fmt, arg = lhs.split()
341+
self.assertEqual(fmt % float(arg), rhs)
342+
self.assertEqual(fmt % -float(arg), '-' + rhs)
343+
344+
@unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
345+
"applies only when using short float repr style")
346+
def test_short_repr(self):
347+
# test short float repr introduced in Python 3.1. One aspect
348+
# of this repr is that we get some degree of str -> float ->
349+
# str roundtripping. In particular, for any numeric string
350+
# containing 15 or fewer significant digits, those exact same
351+
# digits (modulo trailing zeros) should appear in the output.
352+
# No more repr(0.03) -> "0.029999999999999999"!
353+
354+
test_strings = [
355+
# output always includes *either* a decimal point and at
356+
# least one digit after that point, or an exponent.
357+
'0.0',
358+
'1.0',
359+
'0.01',
360+
'0.02',
361+
'0.03',
362+
'0.04',
363+
'0.05',
364+
'1.23456789',
365+
'10.0',
366+
'100.0',
367+
# values >= 1e16 get an exponent...
368+
'1000000000000000.0',
369+
'9999999999999990.0',
370+
'1e+16',
371+
'1e+17',
372+
# ... and so do values < 1e-4
373+
'0.001',
374+
'0.001001',
375+
'0.00010000000000001',
376+
'0.0001',
377+
'9.999999999999e-05',
378+
'1e-05',
379+
# values designed to provoke failure if the FPU rounding
380+
# precision isn't set correctly
381+
'8.72293771110361e+25',
382+
'7.47005307342313e+26',
383+
'2.86438000439698e+28',
384+
'8.89142905246179e+28',
385+
'3.08578087079232e+35',
386+
]
387+
388+
for s in test_strings:
389+
negs = '-'+s
390+
self.assertEqual(s, repr(float(s)))
391+
self.assertEqual(negs, repr(float(negs)))
392+
393+
394+
319395
# Beginning with Python 2.6 float has cross platform compatible
320396
# ways to create and represent inf and nan
321397
class InfNanTest(unittest.TestCase):

Lib/test/test_format.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ def test_format(self):
220220
testformat("%a", "\u0378", "'\\u0378'") # non printable
221221
testformat("%r", "\u0374", "'\u0374'") # printable
222222
testformat("%a", "\u0374", "'\\u0374'") # printable
223+
224+
# alternate float formatting
225+
testformat('%g', 1.1, '1.1')
226+
testformat('%#g', 1.1, '1.10000')
227+
223228
# Test exception for unknown format characters
224229
if verbose:
225230
print('Testing exceptions')

Lib/test/test_types.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def test(f, result):
113113
self.assertEqual(1.5e-101.__format__('e'), '1.500000e-101')
114114
self.assertEqual('%e' % 1.5e-101, '1.500000e-101')
115115

116+
self.assertEqual('%g' % 1.0, '1')
117+
self.assertEqual('%#g' % 1.0, '1.00000')
118+
116119
def test_normal_integers(self):
117120
# Ensure the first 256 integers are shared
118121
a = 256
@@ -358,6 +361,8 @@ def test(i, format_spec, result):
358361
self.assertRaises(TypeError, 3 .__format__, 0)
359362
# can't have ',' with 'n'
360363
self.assertRaises(ValueError, 3 .__format__, ",n")
364+
# can't have ',' with 'c'
365+
self.assertRaises(ValueError, 3 .__format__, ",c")
361366

362367
# ensure that only int and float type specifiers work
363368
for format_spec in ([chr(x) for x in range(ord('a'), ord('z')+1)] +
@@ -547,10 +552,34 @@ def test(f, format_spec, result):
547552
# a totaly empty format specifier means something else.
548553
# So, just use a sign flag
549554
test(1e200, '+g', '+1e+200')
550-
test(1e200, '+', '+1.0e+200')
555+
test(1e200, '+', '+1e+200')
556+
551557
test(1.1e200, '+g', '+1.1e+200')
552558
test(1.1e200, '+', '+1.1e+200')
553559

560+
# 0 padding
561+
test(1234., '010f', '1234.000000')
562+
test(1234., '011f', '1234.000000')
563+
test(1234., '012f', '01234.000000')
564+
test(-1234., '011f', '-1234.000000')
565+
test(-1234., '012f', '-1234.000000')
566+
test(-1234., '013f', '-01234.000000')
567+
test(-1234.12341234, '013f', '-01234.123412')
568+
test(-123456.12341234, '011.2f', '-0123456.12')
569+
570+
# 0 padding with commas
571+
test(1234., '011,f', '1,234.000000')
572+
test(1234., '012,f', '1,234.000000')
573+
test(1234., '013,f', '01,234.000000')
574+
test(-1234., '012,f', '-1,234.000000')
575+
test(-1234., '013,f', '-1,234.000000')
576+
test(-1234., '014,f', '-01,234.000000')
577+
test(-12345., '015,f', '-012,345.000000')
578+
test(-123456., '016,f', '-0,123,456.000000')
579+
test(-123456., '017,f', '-0,123,456.000000')
580+
test(-123456.12341234, '017,f', '-0,123,456.123412')
581+
test(-123456.12341234, '013,.2f', '-0,123,456.12')
582+
554583
# % formatting
555584
test(-1.0, '%', '-100.000000%')
556585

@@ -575,6 +604,24 @@ def test(f, format_spec, result):
575604
self.assertRaises(ValueError, format, 0.0, '#')
576605
self.assertRaises(ValueError, format, 0.0, '#20f')
577606

607+
def test_format_spec_errors(self):
608+
# int, float, and string all share the same format spec
609+
# mini-language parser.
610+
611+
# Check that we can't ask for too many digits. This is
612+
# probably a CPython specific test. It tries to put the width
613+
# into a C long.
614+
self.assertRaises(ValueError, format, 0, '1'*10000 + 'd')
615+
616+
# Similar with the precision.
617+
self.assertRaises(ValueError, format, 0, '.' + '1'*10000 + 'd')
618+
619+
# And may as well test both.
620+
self.assertRaises(ValueError, format, 0, '1'*1000 + '.' + '1'*10000 + 'd')
621+
622+
# Make sure commas aren't allowed with various type codes
623+
for code in 'xXobns':
624+
self.assertRaises(ValueError, format, 0, ',' + code)
578625

579626
def test_main():
580627
run_unittest(TypesTests)

Misc/NEWS

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

15+
- Issue #5772: format(1e100, '<') produces '1e+100', not '1.0e+100'.
16+
17+
- Issue #5515: str.format() presentation type 'n' with commas no
18+
longer works poorly with leading zeros when formatting ints and
19+
floats.
20+
21+
- Implement PEP 378, Format Specifier for Thousands Separator, for
22+
floats.
23+
1524
- The repr function switches to exponential notation at 1e16, not 1e17
1625
as it did before. This change applies to both 'short' and legacy
1726
float repr styles. For the new repr style, it avoids misleading

Modules/_pickle.c

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,16 +1016,31 @@ save_float(PicklerObject *self, PyObject *obj)
10161016
return -1;
10171017
if (pickler_write(self, pdata, 9) < 0)
10181018
return -1;
1019-
}
1019+
}
10201020
else {
1021-
char pdata[250];
1022-
pdata[0] = FLOAT;
1023-
PyOS_ascii_formatd(pdata + 1, sizeof(pdata) - 2, "%.17g", x);
1024-
/* Extend the formatted string with a newline character */
1025-
strcat(pdata, "\n");
1021+
int result = -1;
1022+
char *buf = NULL;
1023+
char op = FLOAT;
10261024

1027-
if (pickler_write(self, pdata, strlen(pdata)) < 0)
1028-
return -1;
1025+
if (pickler_write(self, &op, 1) < 0)
1026+
goto done;
1027+
1028+
buf = PyOS_double_to_string(x, 'r', 0, 0, NULL);
1029+
if (!buf) {
1030+
PyErr_NoMemory();
1031+
goto done;
1032+
}
1033+
1034+
if (pickler_write(self, buf, strlen(buf)) < 0)
1035+
goto done;
1036+
1037+
if (pickler_write(self, "\n", 1) < 0)
1038+
goto done;
1039+
1040+
result = 0;
1041+
done:
1042+
PyMem_Free(buf);
1043+
return result;
10291044
}
10301045

10311046
return 0;

Objects/bytesobject.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ PyBytes_AsStringAndSize(register PyObject *obj,
562562
/* -------------------------------------------------------------------- */
563563
/* Methods */
564564

565+
#include "stringlib/stringdefs.h"
565566
#define STRINGLIB_CHAR char
566567

567568
#define STRINGLIB_CMP memcmp

0 commit comments

Comments
 (0)