Skip to content

Commit

Permalink
Issue python#7094: Add alternate ('#') flag to __format__ methods for…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
ericvsmith committed Nov 25, 2010
1 parent c1d98d6 commit 984bb58
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 36 deletions.
15 changes: 12 additions & 3 deletions Doc/library/string.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Lib/decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5991,14 +5991,15 @@ def _convert_for_comparison(self, other, equality_op=False):
#
# 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
(?:
(?P<fill>.)?
(?P<align>[<>=^])
)?
(?P<sign>[-+ ])?
(?P<alt>\#)?
(?P<zeropad>0)?
(?P<minimumwidth>(?!0)\d+)?
(?P<thousands_sep>,)?
Expand Down Expand Up @@ -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':
Expand Down
24 changes: 22 additions & 2 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,28 @@ def test_format(self):
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')
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,18 @@ def test_formatting(self):

# 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)
Expand Down
7 changes: 2 additions & 5 deletions Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,11 +706,8 @@ def test_format_specials(self):
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']:
Expand Down
30 changes: 22 additions & 8 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,13 +396,9 @@ def test_int__format__locale(self):
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')

Expand Down Expand Up @@ -516,9 +512,27 @@ def test(f, format_spec, result):
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')
Expand Down
4 changes: 4 additions & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ David Goodger
Hans de Graaff
Eddy De Greef
Duncan Grisby
Eric Groo
Dag Gruneau
Michael Guravage
Lars Gustäbel
Expand Down Expand Up @@ -457,6 +458,7 @@ Lenny Kneler
Pat Knight
Greg Kochanski
Damon Kohler
Vlad Korolev
Joseph Koshy
Maksim Kozyarchuk
Stefan Krah
Expand Down Expand Up @@ -536,6 +538,7 @@ David Marek
Doug Marien
Alex Martelli
Anthony Martin
Owen Martin
Sébastien Martini
Roger Masse
Nick Mathewson
Expand Down Expand Up @@ -733,6 +736,7 @@ Michael Scharf
Andreas Schawo
Neil Schemenauer
David Scherer
Bob Schmertz
Gregor Schmid
Ralf Schmitt
Michael Schneider
Expand Down
4 changes: 4 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 6 additions & 16 deletions Objects/stringlib/formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 "
Expand All @@ -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';
Expand Down

0 comments on commit 984bb58

Please sign in to comment.