Skip to content

Commit

Permalink
pythongh-121798: Add class method Decimal.from_number()
Browse files Browse the repository at this point in the history
It is an alternate constructor which only accepts a single numeric argument.
Unlike to Decimal.from_float() it accepts also Decimal.
Unlike to the standard constructor, it does not accept strings and tuples.
  • Loading branch information
serhiy-storchaka committed Jul 15, 2024
1 parent 8303d32 commit c76ebf2
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 0 deletions.
17 changes: 17 additions & 0 deletions Doc/library/decimal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,23 @@ Decimal objects

.. versionadded:: 3.1

.. classmethod:: from_number(number)

Alternative constructor that only accepts numbers (instances of
:class:`float`, :class:`int` or :class:`Decimal`), but not strings
or tuples.

.. doctest::

>>> Decimal.from_number(314)
Decimal('314')
>>> Decimal.from_number(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal.from_number(Decimal('3.14'))
Decimal('3.14')

.. versionadded:: 3.14

.. method:: fma(other, third, context=None)

Fused multiply-add. Return self*other+third with no rounding of the
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ ast

(Contributed by Bénédikt Tran in :gh:`121141`.)

decimal
-------

* Add alternative :class:`~decimal.Decimal` constructor
:meth:`Decimal.from_number() <decimal.Decimal.from_number>`.
(Contributed by Serhiy Storchaka in :gh:`121798`.)

os
--

Expand Down
15 changes: 15 additions & 0 deletions Lib/_pydecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,21 @@ def __new__(cls, value="0", context=None):

raise TypeError("Cannot convert %r to Decimal" % value)

@classmethod
def from_number(cls, number):
"""Converts a real number to a decimal number, exactly.
>>> Decimal.from_number(314) # int
Decimal('314')
>>> Decimal.from_number(0.1) # float
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> Decimal.from_number(Decimal('3.14')) # another decimal instance
Decimal('3.14')
"""
if isinstance(number, (int, Decimal, float)):
return cls(number)
raise TypeError("Cannot convert %r to Decimal" % number)

@classmethod
def from_float(cls, f):
"""Converts a float to a decimal number, exactly.
Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,29 @@ def test_explicit_context_create_from_float(self):
x = random.expovariate(0.01) * (random.random() * 2.0 - 1.0)
self.assertEqual(x, float(nc.create_decimal(x))) # roundtrip

def test_from_number(self, cls=None):
Decimal = self.decimal.Decimal
if cls is None:
cls = Decimal

def check(arg, expected):
d = cls.from_number(arg)
self.assertIs(type(d), cls)
self.assertEqual(d, expected)

check(314, Decimal(314))
check(3.14, Decimal.from_float(3.14))
check(Decimal('3.14'), Decimal('3.14'))
self.assertRaises(TypeError, cls.from_number, 3+4j)
self.assertRaises(TypeError, cls.from_number, '314')
self.assertRaises(TypeError, cls.from_number, (0, (3, 1, 4), 0))
self.assertRaises(TypeError, cls.from_number, object())

def test_from_number_subclass(self, cls=None):
class DecimalSubclass(self.decimal.Decimal):
pass
self.test_from_number(DecimalSubclass)

def test_unicode_digits(self):
Decimal = self.decimal.Decimal

Expand Down Expand Up @@ -1280,6 +1303,7 @@ def __init__(self, a):
a = A.from_float(42)
self.assertEqual(self.decimal.Decimal, a.a_type)


@requires_cdecimal
class CFormatTest(FormatTest, unittest.TestCase):
decimal = C
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add alternative :class:`~decimal.Decimal` constructor
:meth:`Decimal.from_number() <decimal.Decimal.from_number>`.
46 changes: 46 additions & 0 deletions Modules/_decimal/_decimal.c
Original file line number Diff line number Diff line change
Expand Up @@ -2828,6 +2828,51 @@ dec_from_float(PyObject *type, PyObject *pyfloat)
return result;
}

/* 'v' can have any numeric type accepted by the Decimal constructor. Attempt
an exact conversion. If the result does not meet the restrictions
for an mpd_t, fail with InvalidOperation. */
static PyObject *
PyDecType_FromNumberExact(PyTypeObject *type, PyObject *v, PyObject *context)
{
decimal_state *state = get_module_state_by_def(type);
assert(v != NULL);
if (PyDec_Check(state, v)) {
return PyDecType_FromDecimalExact(type, v, context);
}
else if (PyLong_Check(v)) {
return PyDecType_FromLongExact(type, v, context);
}
else if (PyFloat_Check(v)) {
if (dec_addstatus(context, MPD_Float_operation)) {
return NULL;
}
return PyDecType_FromFloatExact(type, v, context);
}
else {
PyErr_Format(PyExc_TypeError,
"conversion from %s to Decimal is not supported",
Py_TYPE(v)->tp_name);
return NULL;
}
}

/* class method */
static PyObject *
dec_from_number(PyObject *type, PyObject *number)
{
PyObject *context;
PyObject *result;

decimal_state *state = get_module_state_by_def((PyTypeObject *)type);
CURRENT_CONTEXT(state, context);
result = PyDecType_FromNumberExact(state->PyDec_Type, number, context);
if (type != (PyObject *)state->PyDec_Type && result != NULL) {
Py_SETREF(result, PyObject_CallFunctionObjArgs(type, result, NULL));
}

return result;
}

/* create_decimal_from_float */
static PyObject *
ctx_from_float(PyObject *context, PyObject *v)
Expand Down Expand Up @@ -5017,6 +5062,7 @@ static PyMethodDef dec_methods [] =

/* Miscellaneous */
{ "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float },
{ "from_number", dec_from_number, METH_O|METH_CLASS, doc_from_number },
{ "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple },
{ "as_integer_ratio", dec_as_integer_ratio, METH_NOARGS, doc_as_integer_ratio },

Expand Down
13 changes: 13 additions & 0 deletions Modules/_decimal/docstrings.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,19 @@ Decimal.from_float(0.1) is not the same as Decimal('0.1').\n\
\n\
\n");

PyDoc_STRVAR(doc_from_number,
"from_number($type, number, /)\n--\n\n\
Class method that converts a real number to a decimal number, exactly.\n\
\n\
>>> Decimal.from_number(314) # int\n\
Decimal('314')\n\
>>> Decimal.from_number(0.1) # float\n\
Decimal('0.1000000000000000055511151231257827021181583404541015625')\n\
>>> Decimal.from_number(Decimal('3.14')) # another decimal instance\n\
Decimal('3.14')\n\
\n\
\n");

PyDoc_STRVAR(doc_fma,
"fma($self, /, other, third, context=None)\n--\n\n\
Fused multiply-add. Return self*other+third with no rounding of the\n\
Expand Down

0 comments on commit c76ebf2

Please sign in to comment.