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: ...@@ -350,9 +350,18 @@ following:
| | positive numbers, and a minus sign on negative numbers. | | | 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 The ``'#'`` option causes the "alternate form" to be used for the
by ``'0b'``, ``'0o'``, or ``'0x'``, respectively. 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. The ``','`` option signals the use of a comma for a thousands separator.
For a locale aware separator, use the ``'n'`` integer presentation type For a locale aware separator, use the ``'n'`` integer presentation type
......
...@@ -5991,7 +5991,7 @@ _exact_half = re.compile('50*$').match ...@@ -5991,7 +5991,7 @@ _exact_half = re.compile('50*$').match
# #
# A format specifier for Decimal looks like: # 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 _parse_format_specifier_regex = re.compile(r"""\A
(?: (?:
...@@ -5999,6 +5999,7 @@ _parse_format_specifier_regex = re.compile(r"""\A ...@@ -5999,6 +5999,7 @@ _parse_format_specifier_regex = re.compile(r"""\A
(?P<align>[<>=^]) (?P<align>[<>=^])
)? )?
(?P<sign>[-+ ])? (?P<sign>[-+ ])?
(?P<alt>\#)?
(?P<zeropad>0)? (?P<zeropad>0)?
(?P<minimumwidth>(?!0)\d+)? (?P<minimumwidth>(?!0)\d+)?
(?P<thousands_sep>,)? (?P<thousands_sep>,)?
...@@ -6214,7 +6215,7 @@ def _format_number(is_negative, intpart, fracpart, exp, spec): ...@@ -6214,7 +6215,7 @@ def _format_number(is_negative, intpart, fracpart, exp, spec):
sign = _format_sign(is_negative, spec) sign = _format_sign(is_negative, spec)
if fracpart: if fracpart or spec['alt']:
fracpart = spec['decimal_point'] + fracpart fracpart = spec['decimal_point'] + fracpart
if exp != 0 or spec['type'] in 'eE': if exp != 0 or spec['type'] in 'eE':
......
...@@ -555,8 +555,28 @@ class ComplexTest(unittest.TestCase): ...@@ -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+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') self.assertEqual(format(1.5e21+3000j, ',.2f'), '1,500,000,000,000,000,000,000.00+3,000.00j')
# alternate is invalid # Issue 7094: Alternate formatting (specified by #)
self.assertRaises(ValueError, (1.5+0.5j).__format__, '#f') 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 # zero padding is invalid
self.assertRaises(ValueError, (1.5+0.5j).__format__, '010f') self.assertRaises(ValueError, (1.5+0.5j).__format__, '010f')
......
...@@ -818,6 +818,18 @@ class DecimalFormatTest(unittest.TestCase): ...@@ -818,6 +818,18 @@ class DecimalFormatTest(unittest.TestCase):
# issue 6850 # issue 6850
('a=-7.0', '0.12345', 'aaaa0.1'), ('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: for fmt, d, result in test_values:
self.assertEqual(format(Decimal(d), fmt), result) self.assertEqual(format(Decimal(d), fmt), result)
......
...@@ -706,11 +706,8 @@ class RoundTestCase(unittest.TestCase): ...@@ -706,11 +706,8 @@ class RoundTestCase(unittest.TestCase):
def test(fmt, value, expected): def test(fmt, value, expected):
# Test with both % and format(). # Test with both % and format().
self.assertEqual(fmt % value, expected, fmt) self.assertEqual(fmt % value, expected, fmt)
if not '#' in fmt: fmt = fmt[1:] # strip off the %
# Until issue 7094 is implemented, format() for floats doesn't self.assertEqual(format(value, fmt), expected, fmt)
# support '#' formatting
fmt = fmt[1:] # strip off the %
self.assertEqual(format(value, fmt), expected, fmt)
for fmt in ['%e', '%f', '%g', '%.0e', '%.6f', '%.20g', for fmt in ['%e', '%f', '%g', '%.0e', '%.6f', '%.20g',
'%#e', '%#f', '%#g', '%#.20e', '%#.15f', '%#.3g']: '%#e', '%#f', '%#g', '%#.20e', '%#.15f', '%#.3g']:
......
...@@ -396,13 +396,9 @@ class TypesTests(unittest.TestCase): ...@@ -396,13 +396,9 @@ class TypesTests(unittest.TestCase):
self.assertEqual(len(format(0, cfmt)), len(format(x, cfmt))) self.assertEqual(len(format(0, cfmt)), len(format(x, cfmt)))
def test_float__format__(self): 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): def test(f, format_spec, result):
assert type(f) == float
assert type(format_spec) == str
self.assertEqual(f.__format__(format_spec), result) self.assertEqual(f.__format__(format_spec), result)
self.assertEqual(format(f, format_spec), result)
test(0.0, 'f', '0.000000') test(0.0, 'f', '0.000000')
...@@ -516,9 +512,27 @@ class TypesTests(unittest.TestCase): ...@@ -516,9 +512,27 @@ class TypesTests(unittest.TestCase):
self.assertRaises(ValueError, format, 1e-100, format_spec) self.assertRaises(ValueError, format, 1e-100, format_spec)
self.assertRaises(ValueError, format, -1e-100, format_spec) self.assertRaises(ValueError, format, -1e-100, format_spec)
# Alternate formatting is not supported # Alternate float formatting
self.assertRaises(ValueError, format, 0.0, '#') test(1.0, '.0e', '1e+00')
self.assertRaises(ValueError, format, 0.0, '#20f') 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 # Issue 6902
test(12345.6, "0<20", '12345.60000000000000') test(12345.6, "0<20", '12345.60000000000000')
......
...@@ -318,6 +318,7 @@ David Goodger ...@@ -318,6 +318,7 @@ David Goodger
Hans de Graaff Hans de Graaff
Eddy De Greef Eddy De Greef
Duncan Grisby Duncan Grisby
Eric Groo
Dag Gruneau Dag Gruneau
Michael Guravage Michael Guravage
Lars Gustäbel Lars Gustäbel
...@@ -457,6 +458,7 @@ Lenny Kneler ...@@ -457,6 +458,7 @@ Lenny Kneler
Pat Knight Pat Knight
Greg Kochanski Greg Kochanski
Damon Kohler Damon Kohler
Vlad Korolev
Joseph Koshy Joseph Koshy
Maksim Kozyarchuk Maksim Kozyarchuk
Stefan Krah Stefan Krah
...@@ -536,6 +538,7 @@ David Marek ...@@ -536,6 +538,7 @@ David Marek
Doug Marien Doug Marien
Alex Martelli Alex Martelli
Anthony Martin Anthony Martin
Owen Martin
Sébastien Martini Sébastien Martini
Roger Masse Roger Masse
Nick Mathewson Nick Mathewson
...@@ -733,6 +736,7 @@ Michael Scharf ...@@ -733,6 +736,7 @@ Michael Scharf
Andreas Schawo Andreas Schawo
Neil Schemenauer Neil Schemenauer
David Scherer David Scherer
Bob Schmertz
Gregor Schmid Gregor Schmid
Ralf Schmitt Ralf Schmitt
Michael Schneider Michael Schneider
......
...@@ -15,6 +15,10 @@ Core and Builtins ...@@ -15,6 +15,10 @@ Core and Builtins
- Issue #10027. st_nlink was not being set on Windows calls to os.stat or - Issue #10027. st_nlink was not being set on Windows calls to os.stat or
os.lstat. Patch by Hirokazu Yamamoto. 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 #10474: range().count() should return integers.
- Issue #10255: Fix reference leak in Py_InitializeEx(). Patch by Neil - Issue #10255: Fix reference leak in Py_InitializeEx(). Patch by Neil
......
...@@ -941,13 +941,8 @@ format_float_internal(PyObject *value, ...@@ -941,13 +941,8 @@ format_float_internal(PyObject *value,
from a hard-code pseudo-locale */ from a hard-code pseudo-locale */
LocaleInfo locale; LocaleInfo locale;
/* Alternate is not allowed on floats. */ if (format->alternate)
if (format->alternate) { flags |= Py_DTSF_ALT;
PyErr_SetString(PyExc_ValueError,
"Alternate form (#) not allowed in float format "
"specifier");
goto done;
}
if (type == '\0') { if (type == '\0') {
/* Omitted type specifier. Behaves in the same way as repr(x) /* Omitted type specifier. Behaves in the same way as repr(x)
...@@ -1104,15 +1099,7 @@ format_complex_internal(PyObject *value, ...@@ -1104,15 +1099,7 @@ format_complex_internal(PyObject *value,
from a hard-code pseudo-locale */ from a hard-code pseudo-locale */
LocaleInfo locale; LocaleInfo locale;
/* Alternate is not allowed on complex. */ /* Zero padding is not allowed. */
if (format->alternate) {
PyErr_SetString(PyExc_ValueError,
"Alternate form (#) not allowed in complex format "
"specifier");
goto done;
}
/* Neither is zero pading. */
if (format->fill_char == '0') { if (format->fill_char == '0') {
PyErr_SetString(PyExc_ValueError, PyErr_SetString(PyExc_ValueError,
"Zero padding is not allowed in complex format " "Zero padding is not allowed in complex format "
...@@ -1135,6 +1122,9 @@ format_complex_internal(PyObject *value, ...@@ -1135,6 +1122,9 @@ format_complex_internal(PyObject *value,
if (im == -1.0 && PyErr_Occurred()) if (im == -1.0 && PyErr_Occurred())
goto done; goto done;
if (format->alternate)
flags |= Py_DTSF_ALT;
if (type == '\0') { if (type == '\0') {
/* Omitted type specifier. Should be like str(self). */ /* Omitted type specifier. Should be like str(self). */
type = 'r'; 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