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.
.. 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')
(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
----
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"""
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"]
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.
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):
'.%',
# 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):
--- /dev/null
+Implement basic formatting support (minimum width, alignment, fill) for
+:class:`fractions.Fraction`.