Commit 984bb580 authored by Eric Smith's avatar Eric Smith

Issue #7094: Add alternate ('#') flag to __format__ methods for float, complex...

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.
parent c1d98d68
......@@ -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
......
......@@ -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':
......
......@@ -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')
......
......@@ -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)
......
......@@ -706,11 +706,8 @@ 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)
fmt = fmt[1:] # strip off the %
self.assertEqual(format(value, fmt), expected, fmt)
for fmt in ['%e', '%f', '%g', '%.0e', '%.6f', '%.20g',
'%#e', '%#f', '%#g', '%#.20e', '%#.15f', '%#.3g']:
......
......@@ -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')
......
......@@ -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
......
......@@ -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
......
......@@ -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';
......
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