[Python-checkins] cpython: Issue #25928: Add Decimal.as_integer_ratio(). Python parts and docs by

stefan.krah python-checkins at python.org
Mon Dec 28 17:14:11 EST 2015


https://hg.python.org/cpython/rev/f3b09c269af0
changeset:   99700:f3b09c269af0
parent:      99696:1472c08d9c23
user:        Stefan Krah <skrah at bytereef.org>
date:        Mon Dec 28 23:02:02 2015 +0100
summary:
  Issue #25928: Add Decimal.as_integer_ratio(). Python parts and docs by
Mark Dickinson.

files:
  Doc/library/decimal.rst            |   13 ++
  Lib/_pydecimal.py                  |   52 ++++++++
  Lib/test/test_decimal.py           |   33 +++++
  Misc/NEWS                          |    2 +
  Modules/_decimal/_decimal.c        |  101 +++++++++++++++++
  Modules/_decimal/docstrings.h      |    9 +
  Modules/_decimal/tests/deccheck.py |    6 +-
  7 files changed, 213 insertions(+), 3 deletions(-)


diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst
--- a/Doc/library/decimal.rst
+++ b/Doc/library/decimal.rst
@@ -448,6 +448,19 @@
       ``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 and
+      with a positive denominator::
+
+          >>> Decimal('-3.14').as_integer_ratio()
+          (-157, 50)
+
+      The conversion is exact.  Raise OverflowError on infinities and ValueError
+      on NaNs.
+
+   .. versionadded:: 3.6
 
    .. method:: as_tuple()
 
diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py
--- a/Lib/_pydecimal.py
+++ b/Lib/_pydecimal.py
@@ -1010,6 +1010,58 @@
         """
         return DecimalTuple(self._sign, tuple(map(int, self._int)), self._exp)
 
+    def as_integer_ratio(self):
+        """Express a finite Decimal instance in the form n / d.
+
+        Returns a pair (n, d) of integers.  When called on an infinity
+        or NaN, raises OverflowError or ValueError respectively.
+
+        >>> Decimal('3.14').as_integer_ratio()
+        (157, 50)
+        >>> Decimal('-123e5').as_integer_ratio()
+        (-12300000, 1)
+        >>> Decimal('0.00').as_integer_ratio()
+        (0, 1)
+
+        """
+        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
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -2047,6 +2047,39 @@
         d = Decimal( (1, (0, 2, 7, 1), 'F') )
         self.assertEqual(d.as_tuple(), (1, (0,), 'F'))
 
+    def test_as_integer_ratio(self):
+        Decimal = self.decimal.Decimal
+
+        # 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('snan123'))
+
+        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(math.gcd(p, q), 1)
+
+                    # check that p/q actually gives the correct value
+                    self.assertEqual(Decimal(p) / Decimal(q), d)
+
     def test_subclassing(self):
         # Different behaviours when subclassing Decimal
         Decimal = self.decimal.Decimal
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -123,6 +123,8 @@
 Library
 -------
 
+- Issue #25928: Add Decimal.as_integer_ratio().
+
 - Issue #25768: Have the functions in compileall return booleans instead of
   ints and add proper documentation and tests for the return values.
 
diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c
--- a/Modules/_decimal/_decimal.c
+++ b/Modules/_decimal/_decimal.c
@@ -3380,6 +3380,106 @@
     return (PyObject *) pylong;
 }
 
+/* Convert a Decimal to its exact integer ratio representation. */
+static PyObject *
+dec_as_integer_ratio(PyObject *self, PyObject *args UNUSED)
+{
+    PyObject *numerator = NULL;
+    PyObject *denominator = NULL;
+    PyObject *exponent = NULL;
+    PyObject *result = NULL;
+    PyObject *tmp;
+    mpd_ssize_t exp;
+    PyObject *context;
+    uint32_t status = 0;
+    PyNumberMethods *long_methods = PyLong_Type.tp_as_number;
+
+    if (mpd_isspecial(MPD(self))) {
+        if (mpd_isnan(MPD(self))) {
+            PyErr_SetString(PyExc_ValueError,
+                "cannot convert NaN to integer ratio");
+        }
+        else {
+            PyErr_SetString(PyExc_OverflowError,
+                "cannot convert Infinity to integer ratio");
+        }
+        return NULL;
+    }
+
+    CURRENT_CONTEXT(context);
+
+    tmp = dec_alloc();
+    if (tmp == NULL) {
+        return NULL;
+    }
+
+    if (!mpd_qcopy(MPD(tmp), MPD(self), &status)) {
+        Py_DECREF(tmp);
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    exp = mpd_iszero(MPD(tmp)) ? 0 : MPD(tmp)->exp;
+    MPD(tmp)->exp = 0;
+
+    /* context and rounding are unused here: the conversion is exact */
+    numerator = dec_as_long(tmp, context, MPD_ROUND_FLOOR);
+    Py_DECREF(tmp);
+    if (numerator == NULL) {
+        goto error;
+    }
+
+    exponent = PyLong_FromSsize_t(exp < 0 ? -exp : exp);
+    if (exponent == NULL) {
+        goto error;
+    }
+
+    tmp = PyLong_FromLong(10);
+    if (tmp == NULL) {
+        goto error;
+    }
+
+    Py_SETREF(exponent, long_methods->nb_power(tmp, exponent, Py_None));
+    Py_DECREF(tmp);
+    if (exponent == NULL) {
+        goto error;
+    }
+
+    if (exp >= 0) {
+        Py_SETREF(numerator, long_methods->nb_multiply(numerator, exponent));
+        if (numerator == NULL) {
+            goto error;
+        }
+        denominator = PyLong_FromLong(1);
+        if (denominator == NULL) {
+            goto error;
+        }
+    }
+    else {
+        denominator = exponent;
+        exponent = NULL;
+        tmp = _PyLong_GCD(numerator, denominator);
+        if (tmp == NULL) {
+            goto error;
+        }
+        Py_SETREF(numerator, long_methods->nb_floor_divide(numerator, tmp));
+        Py_SETREF(denominator, long_methods->nb_floor_divide(denominator, tmp));
+        Py_DECREF(tmp);
+        if (numerator == NULL || denominator == NULL) {
+            goto error;
+        }
+    }
+
+    result = PyTuple_Pack(2, numerator, denominator);
+
+
+error:
+    Py_XDECREF(exponent);
+    Py_XDECREF(denominator);
+    Py_XDECREF(numerator);
+    return result;
+}
+
 static PyObject *
 PyDec_ToIntegralValue(PyObject *dec, PyObject *args, PyObject *kwds)
 {
@@ -4688,6 +4788,7 @@
   /* Miscellaneous */
   { "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float },
   { "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple },
+  { "as_integer_ratio", dec_as_integer_ratio, METH_NOARGS, doc_as_integer_ratio },
 
   /* Special methods */
   { "__copy__", dec_copy, METH_NOARGS, NULL },
diff --git a/Modules/_decimal/docstrings.h b/Modules/_decimal/docstrings.h
--- a/Modules/_decimal/docstrings.h
+++ b/Modules/_decimal/docstrings.h
@@ -70,6 +70,15 @@
 Return a tuple representation of the number.\n\
 \n");
 
+PyDoc_STRVAR(doc_as_integer_ratio,
+"as_integer_ratio($self, /)\n--\n\n\
+Decimal.as_integer_ratio() -> (int, int)\n\
+\n\
+Return a pair of integers, whose ratio is exactly equal to the original\n\
+Decimal and with a positive denominator. The ratio is in lowest terms.\n\
+Raise OverflowError on infinities and a ValueError on NaNs.\n\
+\n");
+
 PyDoc_STRVAR(doc_canonical,
 "canonical($self, /)\n--\n\n\
 Return the canonical encoding of the argument.  Currently, the encoding\n\
diff --git a/Modules/_decimal/tests/deccheck.py b/Modules/_decimal/tests/deccheck.py
--- a/Modules/_decimal/tests/deccheck.py
+++ b/Modules/_decimal/tests/deccheck.py
@@ -50,8 +50,8 @@
         '__abs__', '__bool__', '__ceil__', '__complex__', '__copy__',
         '__floor__', '__float__', '__hash__', '__int__', '__neg__',
         '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__',
-        'adjusted', 'as_tuple', 'canonical', 'conjugate', 'copy_abs',
-        'copy_negate', 'is_canonical', 'is_finite', 'is_infinite',
+        'adjusted', 'as_integer_ratio', 'as_tuple', 'canonical', 'conjugate',
+        'copy_abs', 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite',
         'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix'
     ),
     # Unary with optional context:
@@ -128,7 +128,7 @@
 # Functions that require a restricted exponent range for reasonable runtimes.
 UnaryRestricted = [
   '__ceil__', '__floor__', '__int__', '__trunc__',
-  'to_integral', 'to_integral_value'
+  'as_integer_ratio', 'to_integral', 'to_integral_value'
 ]
 
 BinaryRestricted = ['__round__']

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list