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..59a953e 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( + r'\A' + r'((?P.)?(?P[<>=^]))?' + r'(?P[-+ ]?)' + r'(?P#?)' + r'(?P0?)' + r'(?P\d*)' + r'(?P,?)' + r'(\.(?P\d+))?' + r'(?P[bcdeEfFgGnosxX%]?)' + r'\Z', + re.DOTALL) +_FORMAT_SPECIFIER_TEMPLATE = ( + '{fill}{align}{sign}{alt}{zeropad}{width}{thousands_sep}{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..ddc2c02 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -552,6 +552,78 @@ 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), "%")) + self.assertEqual("2e+3", format(F(5000, 3), '.1g')) + self.assertEqual("1.7e+3", format(F(5000, 3), '.2g')) + self.assertEqual("1.67e+3", format(F(5000, 3), '.3g')) + self.assertEqual("1667", format(F(5000, 3), '.4g')) + + def testFormattingPrecision(self): + for k in [1, -1]: + sign = '-' if k < 0 else '' + self.assertEqual(sign + "4200.000000000100000", + format(k * (4200 + F(1, 10**10)), ".15f")) + self.assertEqual(sign + "0." + 30 * "3", + format(k * F(1, 3), ".30f")) + self.assertEqual(sign + "0." + 200 * "3", + format(k * F(1, 3), ".200f")) + self.assertEqual(sign + "0." + 99 * "6" + "7", + format(k * F(2, 3), ".100f")) + self.assertEqual(sign + "1" + (200 * "0") + "." + (99 * "6") + "7", + format(k * (10**200 + F(2, 3)), ".100f")) + + self.assertEqual(sign + "4200.0000000001", + format(k * (4200 + F(1, 10**10)), ".15g")) + self.assertEqual(sign + "0." + 30 * "3", + format(k * F(1, 3), ".30g")) + self.assertEqual(sign + "0." + 200 * "3", + format(k * F(1, 3), ".200g")) + self.assertEqual(sign + "0." + 99 * "6" + "7", + format(k * F(2, 3), ".100g")) + self.assertEqual(sign + "1" + (200 * "0") + "." + (98 * "6") + "7", + format(k * (10**200 + F(2, 3)), ".300g")) + + self.assertEqual(sign + "1.0006666666666666666666666666666667e+35", + format(k * (1 + F(2, 3000)) * 10**35, '.35g')) + + self.assertEqual(sign + "1." + (99 * "6") + "7e+100", + format(k * (1 + F(2, 3)) * 10**100, '.100e')) + + 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