diff --git a/Doc/library/string.rst b/Doc/library/string.rst index b785fc2c048..5b69fe705ef 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -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 diff --git a/Lib/decimal.py b/Lib/decimal.py index 5a9f8407711..f379ee57ce0 100644 --- a/Lib/decimal.py +++ b/Lib/decimal.py @@ -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[<>=^]) )? (?P[-+ ])? +(?P\#)? (?P0)? (?P(?!0)\d+)? (?P,)? @@ -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': diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 2810b367181..cc21aa7a622 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -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') diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 611ef550073..3036170aeb8 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -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) diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index e0479db274d..0072133aae3 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -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']: diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index d16dbba1735..8a98a035671 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -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') diff --git a/Misc/ACKS b/Misc/ACKS index d2cfd17e89b..1aa9613b2c0 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -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 diff --git a/Misc/NEWS b/Misc/NEWS index 6e27e83162e..f90deb989c8 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -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 diff --git a/Objects/stringlib/formatter.h b/Objects/stringlib/formatter.h index 4fdab06229a..4fdc62d650d 100644 --- a/Objects/stringlib/formatter.h +++ b/Objects/stringlib/formatter.h @@ -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';