diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index c2c7401..767299f 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -164,6 +164,25 @@ another rational number, or from a string. method can also be accessed through the :func:`round` function. + .. method:: __format__(specifier) + + Format Fraction instance according to given specifier. + + If the specifier ends with one of ``'e'``, ``'E'``, ``'f'``, + ``'F'``, ``'g'``, ``'G'``, ``'n'`` or ``'%'``, then the Fraction + is formatted as a decimal number and all the formatting options + that work with :class:`Decimals ` are available. + + Otherwise the Fraction is formatted as a string + ``'numerator/denominator'``. See :ref:`formatspec` for available + formatting options. Also the sign options ``'-'``, ``'+'`` or + ``' '`` are supported. + + >>> '{:.2f}'.format(Fraction(1, 2)) + '0.50' + >>> '{:_^+8}'.format(Fraction(1, 2)) + '__+1/2__' + .. function:: gcd(a, b) Return the greatest common divisor of the integers *a* and *b*. If either diff --git a/Lib/fractions.py b/Lib/fractions.py index 5ddc84c..5945e9b 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -3,7 +3,7 @@ """Fraction, infinite-precision, real numbers.""" -from decimal import Decimal +from decimal import Decimal, Context as DecimalContext import math import numbers import operator @@ -298,6 +298,35 @@ class Fraction(numbers.Rational): else: return '%s/%s' % (self._numerator, self._denominator) + def __format__(self, specifier): + """Format Fraction instance according to given specifier. + + >>> '{:.2f}'.format(Fraction(1, 2)) + '0.50' + >>> '{:_^+8}'.format(Fraction(1, 2)) + '__+1/2__' + + """ + if not isinstance(specifier, str): + raise TypeError("must be str, not %s" % type(specifier).__name__) + + parts = _parse_format_specifier(specifier) + + if parts['type'] in ('', 's'): # Format as fraction + sign = parts['sign'] + modified_specifier = _make_format_specifier(dict(parts, sign='')) + prefix = (sign if (sign in ('+', ' ') and self >= 0) else '') + return format(prefix + str(self), modified_specifier) + elif parts['type'] in 'eEfFgGn%': # Format as decimal + prec = int(parts.get('precision') or 6) + length = len(str(self.numerator // self.denominator)) + decimal_self = DecimalContext(prec=(length + prec)).divide( + Decimal(self.numerator), Decimal(self.denominator)) + return format(decimal_self, specifier) + raise ValueError('Unknown format code {!r} for {} object'.format( + parts['type'], type(self).__name__)) + + def _operator_fallbacks(monomorphic_operator, fallback_operator): """Generates forward and reverse operators given a purely-rational operator and a function from the operator module. @@ -644,3 +673,31 @@ class Fraction(numbers.Rational): if type(self) == Fraction: return self # My components are also immutable return self.__class__(self._numerator, self._denominator) + + +_FORMAT_SPECIFIER_REGEX = re.compile( + '((?P.)?(?P[<>=^]))?' + '(?P[-+ ]?)' + '(?P#?)' + '(?P0?)' + '(?P\d*)' + '(?P,?)' + '(\.(?P\d+))?' + '(?P[bcdeEfFgGnosxX%]?)' + '$' +) + +_FORMAT_SPECIFIER_TEMPLATE = ( + '{fill}{align}{sign}{hash}{zero}{width}{comma}{precision}{type}') + + +def _parse_format_specifier(specifier): + match = _FORMAT_SPECIFIER_REGEX.match(specifier) + if match: + return match.groupdict() + raise ValueError('Invalid format specifier: ' + specifier) + + +def _make_format_specifier(parts): + parts_without_nones = {k: (v or '') for (k, v) in parts.items()} + return _FORMAT_SPECIFIER_TEMPLATE.format(**parts_without_nones) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index e86d5ce..436b785 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -552,6 +552,52 @@ class FractionTest(unittest.TestCase): self.assertEqual("7/3", str(F(7, 3))) self.assertEqual("7", str(F(7, 1))) + def testFormattingAsFraction(self): + self.assertEqual("1/2", format(F(1, 2), "")) + self.assertEqual("1/2 ", format(F(1, 2), "5")) + self.assertEqual(" 1/2", format(F(1, 2), ">5")) + self.assertEqual("1/2 ", format(F(1, 2), "<5")) + self.assertEqual(" 1/2 ", format(F(1, 2), "^5")) + self.assertEqual("+1/2", format(F(1, 2), "+")) + self.assertEqual("-1/2", format(F(-1, 2), "+")) + self.assertEqual("1/2", format(F(1, 2), "-")) + self.assertEqual("-1/2", format(F(-1, 2), "-")) + self.assertEqual(" 1/2", format(F(1, 2), " ")) + self.assertEqual("-1/2", format(F(-1, 2), " ")) + self.assertEqual("__+1/2__", format(F(1, 2), "_^+8")) + + def testFormattingAsDecimal(self): + self.assertEqual("0.42857", format(F(3, 7), ".5f")) + self.assertEqual("0.43", format(F(3, 7), ".2f")) + self.assertEqual("0.5", format(F(1, 2), "g")) + self.assertEqual("+0.5", format(F(1, 2), "+g")) + self.assertEqual(" 0.5", format(F(1, 2), " g")) + self.assertEqual("-0.5", format(F(-1, 2), " g")) + self.assertEqual("0.5", format(F(1, 2), "n")) + self.assertEqual("50%", format(F(1, 2), "%")) + + def testFormattingPrecision(self): + self.assertEqual("4200.000000000100000", + format(4200 + F(1, 10**10), ".15f")) + self.assertEqual("0." + 30 * "3", format(F(1, 3), ".30f")) + self.assertEqual("0." + 200 * "3", format(F(1, 3), ".200f")) + self.assertEqual("0." + 99 * "6" + "7", format(F(2, 3), ".100f")) + self.assertEqual("1" + (200 * "0") + "." + (99 * "6") + "7", + format(10**200 + F(2, 3), ".100f")) + + def testFormattingErrors(self): + data = [ + (tuple('bcdoxX'), + 'Unknown format code {!r} for Fraction object'), + (('z', '1.2.3', '--', 'foobar'), + 'Invalid format specifier: {}'), + ] + for (specifiers, error_msg_template) in data: + for specifier in specifiers: + error_msg = error_msg_template.format(specifier) + with self.assertRaisesRegexp(ValueError, error_msg): + format(F(1, 2), specifier) + def testHash(self): hmod = sys.hash_info.modulus hinf = sys.hash_info.inf