Issue #25928: Add Decimal.as_integer_ratio(). Python parts and docs by
Mark Dickinson.
This commit is contained in:
parent
ac1e7f6983
commit
53f2e0ad45
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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\
|
||||||
|
|
|
@ -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__']
|
||||||
|
|
Loading…
Reference in New Issue