mirror of https://github.com/python/cpython
gh-67790: Support basic formatting for Fraction (#111320)
PR #100161 added fancy float-style formatting for the Fraction type, but left us in a state where basic formatting for fractions (alignment, fill, minimum width, thousands separators) still wasn't supported. This PR adds that support. --------- Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
84df3172ef
commit
fe479fb8a9
|
@ -106,6 +106,10 @@ another rational number, or from a string.
|
|||
presentation types ``"e"``, ``"E"``, ``"f"``, ``"F"``, ``"g"``, ``"G"``
|
||||
and ``"%""``.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
Formatting of :class:`Fraction` instances without a presentation type
|
||||
now supports fill, alignment, sign handling, minimum width and grouping.
|
||||
|
||||
.. attribute:: numerator
|
||||
|
||||
Numerator of the Fraction in lowest term.
|
||||
|
@ -201,17 +205,36 @@ another rational number, or from a string.
|
|||
|
||||
.. method:: __format__(format_spec, /)
|
||||
|
||||
Provides support for float-style formatting of :class:`Fraction`
|
||||
instances via the :meth:`str.format` method, the :func:`format` built-in
|
||||
function, or :ref:`Formatted string literals <f-strings>`. The
|
||||
presentation types ``"e"``, ``"E"``, ``"f"``, ``"F"``, ``"g"``, ``"G"``
|
||||
and ``"%"`` are supported. For these presentation types, formatting for a
|
||||
:class:`Fraction` object ``x`` follows the rules outlined for
|
||||
the :class:`float` type in the :ref:`formatspec` section.
|
||||
Provides support for formatting of :class:`Fraction` instances via the
|
||||
:meth:`str.format` method, the :func:`format` built-in function, or
|
||||
:ref:`Formatted string literals <f-strings>`.
|
||||
|
||||
If the ``format_spec`` format specification string does not end with one
|
||||
of the presentation types ``'e'``, ``'E'``, ``'f'``, ``'F'``, ``'g'``,
|
||||
``'G'`` or ``'%'`` then formatting follows the general rules for fill,
|
||||
alignment, sign handling, minimum width, and grouping as described in the
|
||||
:ref:`format specification mini-language <formatspec>`. The "alternate
|
||||
form" flag ``'#'`` is supported: if present, it forces the output string
|
||||
to always include an explicit denominator, even when the value being
|
||||
formatted is an exact integer. The zero-fill flag ``'0'`` is not
|
||||
supported.
|
||||
|
||||
If the ``format_spec`` format specification string ends with one of
|
||||
the presentation types ``'e'``, ``'E'``, ``'f'``, ``'F'``, ``'g'``,
|
||||
``'G'`` or ``'%'`` then formatting follows the rules outlined for the
|
||||
:class:`float` type in the :ref:`formatspec` section.
|
||||
|
||||
Here are some examples::
|
||||
|
||||
>>> from fractions import Fraction
|
||||
>>> format(Fraction(103993, 33102), '_')
|
||||
'103_993/33_102'
|
||||
>>> format(Fraction(1, 7), '.^+10')
|
||||
'...+1/7...'
|
||||
>>> format(Fraction(3, 1), '')
|
||||
'3'
|
||||
>>> format(Fraction(3, 1), '#')
|
||||
'3/1'
|
||||
>>> format(Fraction(1, 7), '.40g')
|
||||
'0.1428571428571428571428571428571428571429'
|
||||
>>> format(Fraction('1234567.855'), '_.2f')
|
||||
|
|
|
@ -212,6 +212,14 @@ email
|
|||
(Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve
|
||||
the CVE-2023-27043 fix.)
|
||||
|
||||
fractions
|
||||
---------
|
||||
|
||||
* Formatting for objects of type :class:`fractions.Fraction` now supports
|
||||
the standard format specification mini-language rules for fill, alignment,
|
||||
sign handling, minimum width and grouping. (Contributed by Mark Dickinson
|
||||
in :gh:`111320`)
|
||||
|
||||
glob
|
||||
----
|
||||
|
||||
|
|
|
@ -139,6 +139,23 @@ def _round_to_figures(n, d, figures):
|
|||
return sign, significand, exponent
|
||||
|
||||
|
||||
# Pattern for matching non-float-style format specifications.
|
||||
_GENERAL_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
|
||||
(?:
|
||||
(?P<fill>.)?
|
||||
(?P<align>[<>=^])
|
||||
)?
|
||||
(?P<sign>[-+ ]?)
|
||||
# Alt flag forces a slash and denominator in the output, even for
|
||||
# integer-valued Fraction objects.
|
||||
(?P<alt>\#)?
|
||||
# We don't implement the zeropad flag since there's no single obvious way
|
||||
# to interpret it.
|
||||
(?P<minimumwidth>0|[1-9][0-9]*)?
|
||||
(?P<thousands_sep>[,_])?
|
||||
""", re.DOTALL | re.VERBOSE).fullmatch
|
||||
|
||||
|
||||
# Pattern for matching float-style format specifications;
|
||||
# supports 'e', 'E', 'f', 'F', 'g', 'G' and '%' presentation types.
|
||||
_FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
|
||||
|
@ -414,27 +431,42 @@ class Fraction(numbers.Rational):
|
|||
else:
|
||||
return '%s/%s' % (self._numerator, self._denominator)
|
||||
|
||||
def __format__(self, format_spec, /):
|
||||
"""Format this fraction according to the given format specification."""
|
||||
|
||||
# Backwards compatiblility with existing formatting.
|
||||
if not format_spec:
|
||||
return str(self)
|
||||
def _format_general(self, match):
|
||||
"""Helper method for __format__.
|
||||
|
||||
Handles fill, alignment, signs, and thousands separators in the
|
||||
case of no presentation type.
|
||||
"""
|
||||
# Validate and parse the format specifier.
|
||||
match = _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec)
|
||||
if match is None:
|
||||
raise ValueError(
|
||||
f"Invalid format specifier {format_spec!r} "
|
||||
f"for object of type {type(self).__name__!r}"
|
||||
)
|
||||
elif match["align"] is not None and match["zeropad"] is not None:
|
||||
# Avoid the temptation to guess.
|
||||
raise ValueError(
|
||||
f"Invalid format specifier {format_spec!r} "
|
||||
f"for object of type {type(self).__name__!r}; "
|
||||
"can't use explicit alignment when zero-padding"
|
||||
)
|
||||
fill = match["fill"] or " "
|
||||
align = match["align"] or ">"
|
||||
pos_sign = "" if match["sign"] == "-" else match["sign"]
|
||||
alternate_form = bool(match["alt"])
|
||||
minimumwidth = int(match["minimumwidth"] or "0")
|
||||
thousands_sep = match["thousands_sep"] or ''
|
||||
|
||||
# Determine the body and sign representation.
|
||||
n, d = self._numerator, self._denominator
|
||||
if d > 1 or alternate_form:
|
||||
body = f"{abs(n):{thousands_sep}}/{d:{thousands_sep}}"
|
||||
else:
|
||||
body = f"{abs(n):{thousands_sep}}"
|
||||
sign = '-' if n < 0 else pos_sign
|
||||
|
||||
# Pad with fill character if necessary and return.
|
||||
padding = fill * (minimumwidth - len(sign) - len(body))
|
||||
if align == ">":
|
||||
return padding + sign + body
|
||||
elif align == "<":
|
||||
return sign + body + padding
|
||||
elif align == "^":
|
||||
half = len(padding) // 2
|
||||
return padding[:half] + sign + body + padding[half:]
|
||||
else: # align == "="
|
||||
return sign + padding + body
|
||||
|
||||
def _format_float_style(self, match):
|
||||
"""Helper method for __format__; handles float presentation types."""
|
||||
fill = match["fill"] or " "
|
||||
align = match["align"] or ">"
|
||||
pos_sign = "" if match["sign"] == "-" else match["sign"]
|
||||
|
@ -530,6 +562,23 @@ class Fraction(numbers.Rational):
|
|||
else: # align == "="
|
||||
return sign + padding + body
|
||||
|
||||
def __format__(self, format_spec, /):
|
||||
"""Format this fraction according to the given format specification."""
|
||||
|
||||
if match := _GENERAL_FORMAT_SPECIFICATION_MATCHER(format_spec):
|
||||
return self._format_general(match)
|
||||
|
||||
if match := _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec):
|
||||
# Refuse the temptation to guess if both alignment _and_
|
||||
# zero padding are specified.
|
||||
if match["align"] is None or match["zeropad"] is None:
|
||||
return self._format_float_style(match)
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid format specifier {format_spec!r} "
|
||||
f"for object of type {type(self).__name__!r}"
|
||||
)
|
||||
|
||||
def _operator_fallbacks(monomorphic_operator, fallback_operator):
|
||||
"""Generates forward and reverse operators given a purely-rational
|
||||
operator and a function from the operator module.
|
||||
|
|
|
@ -849,12 +849,50 @@ class FractionTest(unittest.TestCase):
|
|||
self.assertEqual(type(f.denominator), myint)
|
||||
|
||||
def test_format_no_presentation_type(self):
|
||||
# Triples (fraction, specification, expected_result)
|
||||
# Triples (fraction, specification, expected_result).
|
||||
testcases = [
|
||||
(F(1, 3), '', '1/3'),
|
||||
(F(-1, 3), '', '-1/3'),
|
||||
(F(3), '', '3'),
|
||||
(F(-3), '', '-3'),
|
||||
# Explicit sign handling
|
||||
(F(2, 3), '+', '+2/3'),
|
||||
(F(-2, 3), '+', '-2/3'),
|
||||
(F(3), '+', '+3'),
|
||||
(F(-3), '+', '-3'),
|
||||
(F(2, 3), ' ', ' 2/3'),
|
||||
(F(-2, 3), ' ', '-2/3'),
|
||||
(F(3), ' ', ' 3'),
|
||||
(F(-3), ' ', '-3'),
|
||||
(F(2, 3), '-', '2/3'),
|
||||
(F(-2, 3), '-', '-2/3'),
|
||||
(F(3), '-', '3'),
|
||||
(F(-3), '-', '-3'),
|
||||
# Padding
|
||||
(F(0), '5', ' 0'),
|
||||
(F(2, 3), '5', ' 2/3'),
|
||||
(F(-2, 3), '5', ' -2/3'),
|
||||
(F(2, 3), '0', '2/3'),
|
||||
(F(2, 3), '1', '2/3'),
|
||||
(F(2, 3), '2', '2/3'),
|
||||
# Alignment
|
||||
(F(2, 3), '<5', '2/3 '),
|
||||
(F(2, 3), '>5', ' 2/3'),
|
||||
(F(2, 3), '^5', ' 2/3 '),
|
||||
(F(2, 3), '=5', ' 2/3'),
|
||||
(F(-2, 3), '<5', '-2/3 '),
|
||||
(F(-2, 3), '>5', ' -2/3'),
|
||||
(F(-2, 3), '^5', '-2/3 '),
|
||||
(F(-2, 3), '=5', '- 2/3'),
|
||||
# Fill
|
||||
(F(2, 3), 'X>5', 'XX2/3'),
|
||||
(F(-2, 3), '.<5', '-2/3.'),
|
||||
(F(-2, 3), '\n^6', '\n-2/3\n'),
|
||||
# Thousands separators
|
||||
(F(1234, 5679), ',', '1,234/5,679'),
|
||||
(F(-1234, 5679), '_', '-1_234/5_679'),
|
||||
(F(1234567), '_', '1_234_567'),
|
||||
(F(-1234567), ',', '-1,234,567'),
|
||||
# Alternate form forces a slash in the output
|
||||
(F(123), '#', '123/1'),
|
||||
(F(-123), '#', '-123/1'),
|
||||
(F(0), '#', '0/1'),
|
||||
]
|
||||
for fraction, spec, expected in testcases:
|
||||
with self.subTest(fraction=fraction, spec=spec):
|
||||
|
@ -1218,6 +1256,10 @@ class FractionTest(unittest.TestCase):
|
|||
'.%',
|
||||
# Z instead of z for negative zero suppression
|
||||
'Z.2f'
|
||||
# z flag not supported for general formatting
|
||||
'z',
|
||||
# zero padding not supported for general formatting
|
||||
'05',
|
||||
]
|
||||
for spec in invalid_specs:
|
||||
with self.subTest(spec=spec):
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Implement basic formatting support (minimum width, alignment, fill) for
|
||||
:class:`fractions.Fraction`.
|
Loading…
Reference in New Issue