diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index c2c7401..20e69ba 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}-d '.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..8770d2f 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -298,6 +298,41 @@ 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' + >>> '{}'.format(Fraction(1, 2)) + '1/2' + + """ + if not isinstance(specifier, str): + raise TypeError("must be str, not %s" % type(specifier).__name__) + + if specifier.endswith(tuple('eEfFgGn%')): # Format as decimal + # Use decimal rather than float to avoid precision loss + decimal_self = Decimal(self.numerator) / Decimal(self.denominator) + return decimal_self.__format__(specifier) + + digits_match = re.match('(.*)([0-9]+)$', specifier) + if digits_match: + width = digits_match.group(2) + specifier = digits_match.group(1) + else: + width = '' + + if specifier.endswith('-'): + return str(self).__format__(specifier[:-1] + width) + elif specifier.endswith('+'): + prefix = ('+' if self >= 0 else '') + return (prefix + str(self)).__format__(specifier[:-1] + width) + elif specifier.endswith(' '): + prefix = (' ' if self >= 0 else '') + return (prefix + str(self)).__format__(specifier[:-1] + width) + + return str(self).__format__(specifier + width) + def _operator_fallbacks(monomorphic_operator, fallback_operator): """Generates forward and reverse operators given a purely-rational operator and a function from the operator module. diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index e86d5ce..00ed23f 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -552,6 +552,37 @@ 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 ", '{:5}'.format(F(1, 2))) + self.assertEqual(" 1/2", '{:>5}'.format(F(1, 2))) + self.assertEqual("1/2 ", '{:<5}'.format(F(1, 2))) + self.assertEqual(" 1/2 ", '{:^5}'.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))) + + def testFormattingAsDecimal(self): + self.assertEqual("50%", '{:%}'.format(F(1, 2))) + self.assertEqual("0.42857", '{:.5f}'.format(F(3, 7))) + self.assertEqual("0.43", '{:.2f}'.format(F(3, 7))) + self.assertEqual("0.5", '{:g}'.format(F(1, 2))) + self.assertEqual("+0.5", '{:+g}'.format(F(1, 2))) + self.assertEqual(" 0.5", '{: g}'.format(F(1, 2))) + self.assertEqual("-0.5", '{: g}'.format(F(-1, 2))) + f = 4200 + F(1, 10**10) + self.assertEqual("4200.000000000100000", '{:.15f}'.format(f)) + self.assertEqual("0.5", '{:n}'.format(F(1, 2))) + self.assertEqual(" +1/2 ", '{:^+8}'.format(F(1, 2))) + + def testFormattingErrors(self): + for specifier in 'bcdoxX': + with self.assertRaises(ValueError): + ('{:' + specifier + '}').format(F(1, 2)) + def testHash(self): hmod = sys.hash_info.modulus hinf = sys.hash_info.inf