Issue #25928: Add Decimal.as_integer_ratio(). Python parts and docs by

Mark Dickinson.
This commit is contained in:
Stefan Krah 2015-12-28 23:02:02 +01:00
parent ac1e7f6983
commit 53f2e0ad45
7 changed files with 213 additions and 3 deletions

View File

@ -448,6 +448,19 @@ Decimal objects
``Decimal('321e+5').adjusted()`` returns seven. Used for determining the ``Decimal('321e+5').adjusted()`` returns seven. Used for determining the
position of the most significant digit with respect to the decimal point. 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() .. method:: as_tuple()

View File

@ -1010,6 +1010,58 @@ class Decimal(object):
""" """
return DecimalTuple(self._sign, tuple(map(int, self._int)), self._exp) 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): def __repr__(self):
"""Represents the number as an instance of Decimal.""" """Represents the number as an instance of Decimal."""
# Invariant: eval(repr(d)) == d # Invariant: eval(repr(d)) == d

View File

@ -2047,6 +2047,39 @@ class UsabilityTest(unittest.TestCase):
d = Decimal( (1, (0, 2, 7, 1), 'F') ) d = Decimal( (1, (0, 2, 7, 1), 'F') )
self.assertEqual(d.as_tuple(), (1, (0,), '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): def test_subclassing(self):
# Different behaviours when subclassing Decimal # Different behaviours when subclassing Decimal
Decimal = self.decimal.Decimal Decimal = self.decimal.Decimal

View File

@ -123,6 +123,8 @@ Core and Builtins
Library Library
------- -------
- Issue #25928: Add Decimal.as_integer_ratio().
- Issue #25768: Have the functions in compileall return booleans instead of - Issue #25768: Have the functions in compileall return booleans instead of
ints and add proper documentation and tests for the return values. ints and add proper documentation and tests for the return values.

View File

@ -3380,6 +3380,106 @@ dec_as_long(PyObject *dec, PyObject *context, int round)
return (PyObject *) pylong; 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 * static PyObject *
PyDec_ToIntegralValue(PyObject *dec, PyObject *args, PyObject *kwds) PyDec_ToIntegralValue(PyObject *dec, PyObject *args, PyObject *kwds)
{ {
@ -4688,6 +4788,7 @@ static PyMethodDef dec_methods [] =
/* Miscellaneous */ /* Miscellaneous */
{ "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float }, { "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float },
{ "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple }, { "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple },
{ "as_integer_ratio", dec_as_integer_ratio, METH_NOARGS, doc_as_integer_ratio },
/* Special methods */ /* Special methods */
{ "__copy__", dec_copy, METH_NOARGS, NULL }, { "__copy__", dec_copy, METH_NOARGS, NULL },

View File

@ -70,6 +70,15 @@ PyDoc_STRVAR(doc_as_tuple,
Return a tuple representation of the number.\n\ Return a tuple representation of the number.\n\
\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, PyDoc_STRVAR(doc_canonical,
"canonical($self, /)\n--\n\n\ "canonical($self, /)\n--\n\n\
Return the canonical encoding of the argument. Currently, the encoding\n\ Return the canonical encoding of the argument. Currently, the encoding\n\

View File

@ -50,8 +50,8 @@ Functions = {
'__abs__', '__bool__', '__ceil__', '__complex__', '__copy__', '__abs__', '__bool__', '__ceil__', '__complex__', '__copy__',
'__floor__', '__float__', '__hash__', '__int__', '__neg__', '__floor__', '__float__', '__hash__', '__int__', '__neg__',
'__pos__', '__reduce__', '__repr__', '__str__', '__trunc__', '__pos__', '__reduce__', '__repr__', '__str__', '__trunc__',
'adjusted', 'as_tuple', 'canonical', 'conjugate', 'copy_abs', 'adjusted', 'as_integer_ratio', 'as_tuple', 'canonical', 'conjugate',
'copy_negate', 'is_canonical', 'is_finite', 'is_infinite', 'copy_abs', 'copy_negate', 'is_canonical', 'is_finite', 'is_infinite',
'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix' 'is_nan', 'is_qnan', 'is_signed', 'is_snan', 'is_zero', 'radix'
), ),
# Unary with optional context: # Unary with optional context:
@ -128,7 +128,7 @@ ContextFunctions = {
# Functions that require a restricted exponent range for reasonable runtimes. # Functions that require a restricted exponent range for reasonable runtimes.
UnaryRestricted = [ UnaryRestricted = [
'__ceil__', '__floor__', '__int__', '__trunc__', '__ceil__', '__floor__', '__int__', '__trunc__',
'to_integral', 'to_integral_value' 'as_integer_ratio', 'to_integral', 'to_integral_value'
] ]
BinaryRestricted = ['__round__'] BinaryRestricted = ['__round__']