Skip to content

Commit 984bb58

Browse files
committed
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.
1 parent c1d98d6 commit 984bb58

9 files changed

Lines changed: 87 additions & 36 deletions

File tree

Doc/library/string.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,18 @@ following:
350350
| | positive numbers, and a minus sign on negative numbers. |
351351
+---------+----------------------------------------------------------+
352352

353-
The ``'#'`` option is only valid for integers, and only for binary, octal, or
354-
hexadecimal output. If present, it specifies that the output will be prefixed
355-
by ``'0b'``, ``'0o'``, or ``'0x'``, respectively.
353+
354+
The ``'#'`` option causes the "alternate form" to be used for the
355+
conversion. The alternate form is defined differently for different
356+
types. This option is only valid for integer, float, complex and
357+
Decimal types. For integers, when binary, octal, or hexadecimal output
358+
is used, this option adds the prefix respective ``'0b'``, ``'0o'``, or
359+
``'0x'`` to the output value. For floats, complex and Decimal the
360+
alternate form causes the result of the conversion to always contain a
361+
decimal-point character, even if no digits follow it. Normally, a
362+
decimal-point character appears in the result of these conversions
363+
only if a digit follows it. In addition, for ``'g'`` and ``'G'``
364+
conversions, trailing zeros are not removed from the result.
356365

357366
The ``','`` option signals the use of a comma for a thousands separator.
358367
For a locale aware separator, use the ``'n'`` integer presentation type

Lib/decimal.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5991,14 +5991,15 @@ def _convert_for_comparison(self, other, equality_op=False):
59915991
#
59925992
# A format specifier for Decimal looks like:
59935993
#
5994-
# [[fill]align][sign][0][minimumwidth][,][.precision][type]
5994+
# [[fill]align][sign][#][0][minimumwidth][,][.precision][type]
59955995

59965996
_parse_format_specifier_regex = re.compile(r"""\A
59975997
(?:
59985998
(?P<fill>.)?
59995999
(?P<align>[<>=^])
60006000
)?
60016001
(?P<sign>[-+ ])?
6002+
(?P<alt>\#)?
60026003
(?P<zeropad>0)?
60036004
(?P<minimumwidth>(?!0)\d+)?
60046005
(?P<thousands_sep>,)?
@@ -6214,7 +6215,7 @@ def _format_number(is_negative, intpart, fracpart, exp, spec):
62146215

62156216
sign = _format_sign(is_negative, spec)
62166217

6217-
if fracpart:
6218+
if fracpart or spec['alt']:
62186219
fracpart = spec['decimal_point'] + fracpart
62196220

62206221
if exp != 0 or spec['type'] in 'eE':

Lib/test/test_complex.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,28 @@ def test_format(self):
555555
self.assertEqual(format(1.5e21+3j, '^40,.2f'), ' 1,500,000,000,000,000,000,000.00+3.00j ')
556556
self.assertEqual(format(1.5e21+3000j, ',.2f'), '1,500,000,000,000,000,000,000.00+3,000.00j')
557557

558-
# alternate is invalid
559-
self.assertRaises(ValueError, (1.5+0.5j).__format__, '#f')
558+
# Issue 7094: Alternate formatting (specified by #)
559+
self.assertEqual(format(1+1j, '.0e'), '1e+00+1e+00j')
560+
self.assertEqual(format(1+1j, '#.0e'), '1.e+00+1.e+00j')
561+
self.assertEqual(format(1+1j, '.0f'), '1+1j')
562+
self.assertEqual(format(1+1j, '#.0f'), '1.+1.j')
563+
self.assertEqual(format(1.1+1.1j, 'g'), '1.1+1.1j')
564+
self.assertEqual(format(1.1+1.1j, '#g'), '1.10000+1.10000j')
565+
566+
# Alternate doesn't make a difference for these, they format the same with or without it
567+
self.assertEqual(format(1+1j, '.1e'), '1.0e+00+1.0e+00j')
568+
self.assertEqual(format(1+1j, '#.1e'), '1.0e+00+1.0e+00j')
569+
self.assertEqual(format(1+1j, '.1f'), '1.0+1.0j')
570+
self.assertEqual(format(1+1j, '#.1f'), '1.0+1.0j')
571+
572+
# Misc. other alternate tests
573+
self.assertEqual(format((-1.5+0.5j), '#f'), '-1.500000+0.500000j')
574+
self.assertEqual(format((-1.5+0.5j), '#.0f'), '-2.+0.j')
575+
self.assertEqual(format((-1.5+0.5j), '#e'), '-1.500000e+00+5.000000e-01j')
576+
self.assertEqual(format((-1.5+0.5j), '#.0e'), '-2.e+00+5.e-01j')
577+
self.assertEqual(format((-1.5+0.5j), '#g'), '-1.50000+0.500000j')
578+
self.assertEqual(format((-1.5+0.5j), '.0g'), '-2+0.5j')
579+
self.assertEqual(format((-1.5+0.5j), '#.0g'), '-2.+0.5j')
560580

561581
# zero padding is invalid
562582
self.assertRaises(ValueError, (1.5+0.5j).__format__, '010f')

Lib/test/test_decimal.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,18 @@ def test_formatting(self):
818818

819819
# issue 6850
820820
('a=-7.0', '0.12345', 'aaaa0.1'),
821+
822+
# Issue 7094: Alternate formatting (specified by #)
823+
('.0e', '1.0', '1e+0'),
824+
('#.0e', '1.0', '1.e+0'),
825+
('.0f', '1.0', '1'),
826+
('#.0f', '1.0', '1.'),
827+
('g', '1.1', '1.1'),
828+
('#g', '1.1', '1.1'),
829+
('.0g', '1', '1'),
830+
('#.0g', '1', '1.'),
831+
('.0%', '1.0', '100%'),
832+
('#.0%', '1.0', '100.%'),
821833
]
822834
for fmt, d, result in test_values:
823835
self.assertEqual(format(Decimal(d), fmt), result)

Lib/test/test_float.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -706,11 +706,8 @@ def test_format_specials(self):
706706
def test(fmt, value, expected):
707707
# Test with both % and format().
708708
self.assertEqual(fmt % value, expected, fmt)
709-
if not '#' in fmt:
710-
# Until issue 7094 is implemented, format() for floats doesn't
711-
# support '#' formatting
712-
fmt = fmt[1:] # strip off the %
713-
self.assertEqual(format(value, fmt), expected, fmt)
709+
fmt = fmt[1:] # strip off the %
710+
self.assertEqual(format(value, fmt), expected, fmt)
714711

715712
for fmt in ['%e', '%f', '%g', '%.0e', '%.6f', '%.20g',
716713
'%#e', '%#f', '%#g', '%#.20e', '%#.15f', '%#.3g']:

Lib/test/test_types.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,13 +396,9 @@ def test_int__format__locale(self):
396396
self.assertEqual(len(format(0, cfmt)), len(format(x, cfmt)))
397397

398398
def test_float__format__(self):
399-
# these should be rewritten to use both format(x, spec) and
400-
# x.__format__(spec)
401-
402399
def test(f, format_spec, result):
403-
assert type(f) == float
404-
assert type(format_spec) == str
405400
self.assertEqual(f.__format__(format_spec), result)
401+
self.assertEqual(format(f, format_spec), result)
406402

407403
test(0.0, 'f', '0.000000')
408404

@@ -516,9 +512,27 @@ def test(f, format_spec, result):
516512
self.assertRaises(ValueError, format, 1e-100, format_spec)
517513
self.assertRaises(ValueError, format, -1e-100, format_spec)
518514

519-
# Alternate formatting is not supported
520-
self.assertRaises(ValueError, format, 0.0, '#')
521-
self.assertRaises(ValueError, format, 0.0, '#20f')
515+
# Alternate float formatting
516+
test(1.0, '.0e', '1e+00')
517+
test(1.0, '#.0e', '1.e+00')
518+
test(1.0, '.0f', '1')
519+
test(1.0, '#.0f', '1.')
520+
test(1.1, 'g', '1.1')
521+
test(1.1, '#g', '1.10000')
522+
test(1.0, '.0%', '100%')
523+
test(1.0, '#.0%', '100.%')
524+
525+
# Issue 7094: Alternate formatting (specified by #)
526+
test(1.0, '0e', '1.000000e+00')
527+
test(1.0, '#0e', '1.000000e+00')
528+
test(1.0, '0f', '1.000000' )
529+
test(1.0, '#0f', '1.000000')
530+
test(1.0, '.1e', '1.0e+00')
531+
test(1.0, '#.1e', '1.0e+00')
532+
test(1.0, '.1f', '1.0')
533+
test(1.0, '#.1f', '1.0')
534+
test(1.0, '.1%', '100.0%')
535+
test(1.0, '#.1%', '100.0%')
522536

523537
# Issue 6902
524538
test(12345.6, "0<20", '12345.60000000000000')

Misc/ACKS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ David Goodger
318318
Hans de Graaff
319319
Eddy De Greef
320320
Duncan Grisby
321+
Eric Groo
321322
Dag Gruneau
322323
Michael Guravage
323324
Lars Gustäbel
@@ -457,6 +458,7 @@ Lenny Kneler
457458
Pat Knight
458459
Greg Kochanski
459460
Damon Kohler
461+
Vlad Korolev
460462
Joseph Koshy
461463
Maksim Kozyarchuk
462464
Stefan Krah
@@ -536,6 +538,7 @@ David Marek
536538
Doug Marien
537539
Alex Martelli
538540
Anthony Martin
541+
Owen Martin
539542
Sébastien Martini
540543
Roger Masse
541544
Nick Mathewson
@@ -733,6 +736,7 @@ Michael Scharf
733736
Andreas Schawo
734737
Neil Schemenauer
735738
David Scherer
739+
Bob Schmertz
736740
Gregor Schmid
737741
Ralf Schmitt
738742
Michael Schneider

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Core and Builtins
1515
- Issue #10027. st_nlink was not being set on Windows calls to os.stat or
1616
os.lstat. Patch by Hirokazu Yamamoto.
1717

18+
- Issue #7094: Added alternate formatting (specified by '#') to
19+
__format__ method of float, complex, and Decimal. This allows more
20+
precise control over when decimal points are displayed.
21+
1822
- Issue #10474: range().count() should return integers.
1923

2024
- Issue #10255: Fix reference leak in Py_InitializeEx(). Patch by Neil

Objects/stringlib/formatter.h

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -941,13 +941,8 @@ format_float_internal(PyObject *value,
941941
from a hard-code pseudo-locale */
942942
LocaleInfo locale;
943943

944-
/* Alternate is not allowed on floats. */
945-
if (format->alternate) {
946-
PyErr_SetString(PyExc_ValueError,
947-
"Alternate form (#) not allowed in float format "
948-
"specifier");
949-
goto done;
950-
}
944+
if (format->alternate)
945+
flags |= Py_DTSF_ALT;
951946

952947
if (type == '\0') {
953948
/* Omitted type specifier. Behaves in the same way as repr(x)
@@ -1104,15 +1099,7 @@ format_complex_internal(PyObject *value,
11041099
from a hard-code pseudo-locale */
11051100
LocaleInfo locale;
11061101

1107-
/* Alternate is not allowed on complex. */
1108-
if (format->alternate) {
1109-
PyErr_SetString(PyExc_ValueError,
1110-
"Alternate form (#) not allowed in complex format "
1111-
"specifier");
1112-
goto done;
1113-
}
1114-
1115-
/* Neither is zero pading. */
1102+
/* Zero padding is not allowed. */
11161103
if (format->fill_char == '0') {
11171104
PyErr_SetString(PyExc_ValueError,
11181105
"Zero padding is not allowed in complex format "
@@ -1135,6 +1122,9 @@ format_complex_internal(PyObject *value,
11351122
if (im == -1.0 && PyErr_Occurred())
11361123
goto done;
11371124

1125+
if (format->alternate)
1126+
flags |= Py_DTSF_ALT;
1127+
11381128
if (type == '\0') {
11391129
/* Omitted type specifier. Should be like str(self). */
11401130
type = 'r';

0 commit comments

Comments
 (0)