Issue #7094: Add alternate ('#') flag to __format__ methods for float, complex and Decimal. Allows greater control over when decimal points appear. Added to make transitioning from %-formatting easier. '#g' still has a problem with Decimal which I'll fix soon.

This commit is contained in:
Eric Smith 2010-11-25 16:08:06 +00:00
parent c1d98d6850
commit 984bb58000
9 changed files with 87 additions and 36 deletions

View File

@ -350,9 +350,18 @@ following:
| | positive numbers, and a minus sign on negative numbers. |
+---------+----------------------------------------------------------+
The ``'#'`` option is only valid for integers, and only for binary, octal, or
hexadecimal output. If present, it specifies that the output will be prefixed
by ``'0b'``, ``'0o'``, or ``'0x'``, respectively.
The ``'#'`` option causes the "alternate form" to be used for the
conversion. The alternate form is defined differently for different
types. This option is only valid for integer, float, complex and
Decimal types. For integers, when binary, octal, or hexadecimal output
is used, this option adds the prefix respective ``'0b'``, ``'0o'``, or
``'0x'`` to the output value. For floats, complex and Decimal the
alternate form causes the result of the conversion to always contain a
decimal-point character, even if no digits follow it. Normally, a
decimal-point character appears in the result of these conversions
only if a digit follows it. In addition, for ``'g'`` and ``'G'``
conversions, trailing zeros are not removed from the result.
The ``','`` option signals the use of a comma for a thousands separator.
For a locale aware separator, use the ``'n'`` integer presentation type

View File

@ -5991,7 +5991,7 @@ _exact_half = re.compile('50*$').match
#
# A format specifier for Decimal looks like:
#
# [[fill]align][sign][0][minimumwidth][,][.precision][type]
# [[fill]align][sign][#][0][minimumwidth][,][.precision][type]
_parse_format_specifier_regex = re.compile(r"""\A
(?:
@ -5999,6 +5999,7 @@ _parse_format_specifier_regex = re.compile(r"""\A
(?P<align>[<>=^])
)?
(?P<sign>[-+ ])?
(?P<alt>\#)?
(?P<zeropad>0)?
(?P<minimumwidth>(?!0)\d+)?
(?P<thousands_sep>,)?
@ -6214,7 +6215,7 @@ def _format_number(is_negative, intpart, fracpart, exp, spec):
sign = _format_sign(is_negative, spec)
if fracpart:
if fracpart or spec['alt']:
fracpart = spec['decimal_point'] + fracpart
if exp != 0 or spec['type'] in 'eE':

View File

@ -555,8 +555,28 @@ class ComplexTest(unittest.TestCase):
self.assertEqual(format(1.5e21+3j, '^40,.2f'), ' 1,500,000,000,000,000,000,000.00+3.00j ')
self.assertEqual(format(1.5e21+3000j, ',.2f'), '1,500,000,000,000,000,000,000.00+3,000.00j')
# alternate is invalid
self.assertRaises(ValueError, (1.5+0.5j).__format__, '#f')
# Issue 7094: Alternate formatting (specified by #)
self.assertEqual(format(1+1j, '.0e'), '1e+00+1e+00j')
self.assertEqual(format(1+1j, '#.0e'), '1.e+00+1.e+00j')
self.assertEqual(format(1+1j, '.0f'), '1+1j')
self.assertEqual(format(1+1j, '#.0f'), '1.+1.j')
self.assertEqual(format(1.1+1.1j, 'g'), '1.1+1.1j')
self.assertEqual(format(1.1+1.1j, '#g'), '1.10000+1.10000j')
# Alternate doesn't make a difference for these, they format the same with or without it
self.assertEqual(format(1+1j, '.1e'), '1.0e+00+1.0e+00j')
self.assertEqual(format(1+1j, '#.1e'), '1.0e+00+1.0e+00j')
self.assertEqual(format(1+1j, '.1f'), '1.0+1.0j')
self.assertEqual(format(1+1j, '#.1f'), '1.0+1.0j')
# Misc. other alternate tests
self.assertEqual(format((-1.5+0.5j), '#f'), '-1.500000+0.500000j')
self.assertEqual(format((-1.5+0.5j), '#.0f'), '-2.+0.j')
self.assertEqual(format((-1.5+0.5j), '#e'), '-1.500000e+00+5.000000e-01j')
self.assertEqual(format((-1.5+0.5j), '#.0e'), '-2.e+00+5.e-01j')
self.assertEqual(format((-1.5+0.5j), '#g'), '-1.50000+0.500000j')
self.assertEqual(format((-1.5+0.5j), '.0g'), '-2+0.5j')
self.assertEqual(format((-1.5+0.5j), '#.0g'), '-2.+0.5j')
# zero padding is invalid
self.assertRaises(ValueError, (1.5+0.5j).__format__, '010f')

View File

@ -818,6 +818,18 @@ class DecimalFormatTest(unittest.TestCase):
# issue 6850
('a=-7.0', '0.12345', 'aaaa0.1'),
# Issue 7094: Alternate formatting (specified by #)
('.0e', '1.0', '1e+0'),
('#.0e', '1.0', '1.e+0'),
('.0f', '1.0', '1'),
('#.0f', '1.0', '1.'),
('g', '1.1', '1.1'),
('#g', '1.1', '1.1'),
('.0g', '1', '1'),
('#.0g', '1', '1.'),
('.0%', '1.0', '100%'),
('#.0%', '1.0', '100.%'),
]
for fmt, d, result in test_values:
self.assertEqual(format(Decimal(d), fmt), result)

View File

@ -706,9 +706,6 @@ class RoundTestCase(unittest.TestCase):
def test(fmt, value, expected):
# Test with both % and format().
self.assertEqual(fmt % value, expected, fmt)
if not '#' in fmt:
# Until issue 7094 is implemented, format() for floats doesn't
# support '#' formatting
fmt = fmt[1:] # strip off the %
self.assertEqual(format(value, fmt), expected, fmt)

View File

@ -396,13 +396,9 @@ class TypesTests(unittest.TestCase):
self.assertEqual(len(format(0, cfmt)), len(format(x, cfmt)))
def test_float__format__(self):
# these should be rewritten to use both format(x, spec) and
# x.__format__(spec)
def test(f, format_spec, result):
assert type(f) == float
assert type(format_spec) == str
self.assertEqual(f.__format__(format_spec), result)
self.assertEqual(format(f, format_spec), result)
test(0.0, 'f', '0.000000')
@ -516,9 +512,27 @@ class TypesTests(unittest.TestCase):
self.assertRaises(ValueError, format, 1e-100, format_spec)
self.assertRaises(ValueError, format, -1e-100, format_spec)
# Alternate formatting is not supported
self.assertRaises(ValueError, format, 0.0, '#')
self.assertRaises(ValueError, format, 0.0, '#20f')
# Alternate float formatting
test(1.0, '.0e', '1e+00')
test(1.0, '#.0e', '1.e+00')
test(1.0, '.0f', '1')
test(1.0, '#.0f', '1.')
test(1.1, 'g', '1.1')
test(1.1, '#g', '1.10000')
test(1.0, '.0%', '100%')
test(1.0, '#.0%', '100.%')
# Issue 7094: Alternate formatting (specified by #)
test(1.0, '0e', '1.000000e+00')
test(1.0, '#0e', '1.000000e+00')
test(1.0, '0f', '1.000000' )
test(1.0, '#0f', '1.000000')
test(1.0, '.1e', '1.0e+00')
test(1.0, '#.1e', '1.0e+00')
test(1.0, '.1f', '1.0')
test(1.0, '#.1f', '1.0')
test(1.0, '.1%', '100.0%')
test(1.0, '#.1%', '100.0%')
# Issue 6902
test(12345.6, "0<20", '12345.60000000000000')

View File

@ -318,6 +318,7 @@ David Goodger
Hans de Graaff
Eddy De Greef
Duncan Grisby
Eric Groo
Dag Gruneau
Michael Guravage
Lars Gustäbel
@ -457,6 +458,7 @@ Lenny Kneler
Pat Knight
Greg Kochanski
Damon Kohler
Vlad Korolev
Joseph Koshy
Maksim Kozyarchuk
Stefan Krah
@ -536,6 +538,7 @@ David Marek
Doug Marien
Alex Martelli
Anthony Martin
Owen Martin
Sébastien Martini
Roger Masse
Nick Mathewson
@ -733,6 +736,7 @@ Michael Scharf
Andreas Schawo
Neil Schemenauer
David Scherer
Bob Schmertz
Gregor Schmid
Ralf Schmitt
Michael Schneider

View File

@ -15,6 +15,10 @@ Core and Builtins
- Issue #10027. st_nlink was not being set on Windows calls to os.stat or
os.lstat. Patch by Hirokazu Yamamoto.
- Issue #7094: Added alternate formatting (specified by '#') to
__format__ method of float, complex, and Decimal. This allows more
precise control over when decimal points are displayed.
- Issue #10474: range().count() should return integers.
- Issue #10255: Fix reference leak in Py_InitializeEx(). Patch by Neil

View File

@ -941,13 +941,8 @@ format_float_internal(PyObject *value,
from a hard-code pseudo-locale */
LocaleInfo locale;
/* Alternate is not allowed on floats. */
if (format->alternate) {
PyErr_SetString(PyExc_ValueError,
"Alternate form (#) not allowed in float format "
"specifier");
goto done;
}
if (format->alternate)
flags |= Py_DTSF_ALT;
if (type == '\0') {
/* Omitted type specifier. Behaves in the same way as repr(x)
@ -1104,15 +1099,7 @@ format_complex_internal(PyObject *value,
from a hard-code pseudo-locale */
LocaleInfo locale;
/* Alternate is not allowed on complex. */
if (format->alternate) {
PyErr_SetString(PyExc_ValueError,
"Alternate form (#) not allowed in complex format "
"specifier");
goto done;
}
/* Neither is zero pading. */
/* Zero padding is not allowed. */
if (format->fill_char == '0') {
PyErr_SetString(PyExc_ValueError,
"Zero padding is not allowed in complex format "
@ -1135,6 +1122,9 @@ format_complex_internal(PyObject *value,
if (im == -1.0 && PyErr_Occurred())
goto done;
if (format->alternate)
flags |= Py_DTSF_ALT;
if (type == '\0') {
/* Omitted type specifier. Should be like str(self). */
type = 'r';