Index: Doc/library/decimal.rst =================================================================== --- Doc/library/decimal.rst (revision 81892) +++ Doc/library/decimal.rst (working copy) @@ -392,7 +392,16 @@ ``Decimal('321e+5').adjusted()`` returns seven. Used for determining the position of the most significant digit with respect to the decimal point. + .. method:: as_integer_ratio() + Return a pair ``(n, d)`` of integers that represent the given + :class:`Decimal` instance as a fraction, in lowest terms:: + + >>> Decimal('-3.14').as_integer_ratio() + (-157, 50) + + .. versionadded:: 3.2 + .. method:: as_tuple() Return a :term:`named tuple` representation of the number: Index: Lib/decimal.py =================================================================== --- Lib/decimal.py (revision 81892) +++ Lib/decimal.py (working copy) @@ -965,6 +965,67 @@ """ return DecimalTuple(self._sign, tuple(map(int, self._int)), self._exp) + def as_integer_ratio(self): + """Return a pair (n, d) such that self == n / d. + + >>> Decimal('3.14').as_integer_ratio() + (157, 50) + >>> Decimal('-123e5').as_integer_ratio() + (-12300000, 1) + >>> Decimal('0.00').as_integer_ratio() + (0, 1) + >>> Decimal('Infinity').as_integer_ratio() + Traceback (most recent call last): + ... + ... + ... + OverflowError: Cannot pass infinity to decimal.as_integer_ratio. + >>> Decimal('NaN').as_integer_ratio() + Traceback (most recent call last): + ... + ... + ... + ValueError: Cannot pass NaN to decimal.as_integer_ratio. + + """ + if self._is_special: + if self.is_nan(): + raise ValueError("Cannot pass NaN " + "to decimal.as_integer_ratio.") + else: + raise OverflowError("Cannot pass infinity " + "to decimal.as_integer_ratio.") + + if not self: + return 0, 1 + + # Find n, d in lowest terms such that abs(self) == n / d; + # we'll deal with the sign later. + n = int(self._int) + if self._exp >= 0: + # self is an integer. + n, d = n * 10**self._exp, 1 + else: + # Find d2, d5 such that abs(self) = n / (2**d2 * 5**d5). + d5 = -self._exp + while d5 > 0 and n % 5 == 0: + n //= 5 + d5 -= 1 + + # (n & -n).bit_length() - 1 counts trailing zeros in binary + # representation of n (provided n is nonzero). + d2 = -self._exp + shift2 = min((n & -n).bit_length() - 1, d2) + if shift2: + n >>= shift2 + d2 -= shift2 + + d = 5**d5 << d2 + + if self._sign: + n = -n + return n, d + def __repr__(self): """Represents the number as an instance of Decimal.""" # Invariant: eval(repr(d)) == d Index: Lib/test/test_decimal.py =================================================================== --- Lib/test/test_decimal.py (revision 81892) +++ Lib/test/test_decimal.py (working copy) @@ -31,6 +31,7 @@ import pickle, copy import unittest from decimal import * +from fractions import gcd import numbers from test.support import run_unittest, run_doctest, is_resource_enabled from test.support import check_warnings @@ -1566,6 +1567,39 @@ d = Decimal( (1, (0, 2, 7, 1), 'F') ) self.assertEqual(d.as_tuple(), (1, (0,), 'F')) + def test_as_integer_ratio(self): + # exceptional cases + self.assertRaises(OverflowError, + Decimal.as_integer_ratio, Decimal('inf')) + self.assertRaises(OverflowError, + Decimal.as_integer_ratio, Decimal('-Inf')) + self.assertRaises(ValueError, + Decimal.as_integer_ratio, Decimal('-nan')) + self.assertRaises(ValueError, + Decimal.as_integer_ratio, Decimal('snAN')) + + for exp in range(-4, 2): + for coeff in range(1000): + for sign in '+', '-': + d = Decimal('%s%dE%d' % (sign, coeff, exp)) + pq = d.as_integer_ratio() + p, q = pq + + # check return type + self.assertIsInstance(pq, tuple) + self.assertIsInstance(p, int) + self.assertIsInstance(q, int) + + # check normalization: q should be positive; + # p should be relatively prime to q. + self.assertGreater(q, 0) + self.assertEqual(gcd(p, q), 1) + + # check that p/q actually gives the correct value + self.assertEqual(Decimal(p) / Decimal(q), d) + + + def test_immutability_operations(self): # Do operations and check that it didn't change change internal objects.