Skip to content

Commit c76ebf2

Browse files
gh-121798: Add class method Decimal.from_number()
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.
1 parent 8303d32 commit c76ebf2

File tree

7 files changed

+124
-0
lines changed

7 files changed

+124
-0
lines changed

Doc/library/decimal.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,23 @@ Decimal objects
598598

599599
.. versionadded:: 3.1
600600

601+
.. classmethod:: from_number(number)
602+
603+
Alternative constructor that only accepts numbers (instances of
604+
:class:`float`, :class:`int` or :class:`Decimal`), but not strings
605+
or tuples.
606+
607+
.. doctest::
608+
609+
>>> Decimal.from_number(314)
610+
Decimal('314')
611+
>>> Decimal.from_number(0.1)
612+
Decimal('0.1000000000000000055511151231257827021181583404541015625')
613+
>>> Decimal.from_number(Decimal('3.14'))
614+
Decimal('3.14')
615+
616+
.. versionadded:: 3.14
617+
601618
.. method:: fma(other, third, context=None)
602619

603620
Fused multiply-add. Return self*other+third with no rounding of the

Doc/whatsnew/3.14.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ ast
9696

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

99+
decimal
100+
-------
101+
102+
* Add alternative :class:`~decimal.Decimal` constructor
103+
:meth:`Decimal.from_number() <decimal.Decimal.from_number>`.
104+
(Contributed by Serhiy Storchaka in :gh:`121798`.)
105+
99106
os
100107
--
101108

Lib/_pydecimal.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,21 @@ def __new__(cls, value="0", context=None):
582582

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

585+
@classmethod
586+
def from_number(cls, number):
587+
"""Converts a real number to a decimal number, exactly.
588+
589+
>>> Decimal.from_number(314) # int
590+
Decimal('314')
591+
>>> Decimal.from_number(0.1) # float
592+
Decimal('0.1000000000000000055511151231257827021181583404541015625')
593+
>>> Decimal.from_number(Decimal('3.14')) # another decimal instance
594+
Decimal('3.14')
595+
"""
596+
if isinstance(number, (int, Decimal, float)):
597+
return cls(number)
598+
raise TypeError("Cannot convert %r to Decimal" % number)
599+
585600
@classmethod
586601
def from_float(cls, f):
587602
"""Converts a float to a decimal number, exactly.

Lib/test/test_decimal.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,29 @@ def test_explicit_context_create_from_float(self):
812812
x = random.expovariate(0.01) * (random.random() * 2.0 - 1.0)
813813
self.assertEqual(x, float(nc.create_decimal(x))) # roundtrip
814814

815+
def test_from_number(self, cls=None):
816+
Decimal = self.decimal.Decimal
817+
if cls is None:
818+
cls = Decimal
819+
820+
def check(arg, expected):
821+
d = cls.from_number(arg)
822+
self.assertIs(type(d), cls)
823+
self.assertEqual(d, expected)
824+
825+
check(314, Decimal(314))
826+
check(3.14, Decimal.from_float(3.14))
827+
check(Decimal('3.14'), Decimal('3.14'))
828+
self.assertRaises(TypeError, cls.from_number, 3+4j)
829+
self.assertRaises(TypeError, cls.from_number, '314')
830+
self.assertRaises(TypeError, cls.from_number, (0, (3, 1, 4), 0))
831+
self.assertRaises(TypeError, cls.from_number, object())
832+
833+
def test_from_number_subclass(self, cls=None):
834+
class DecimalSubclass(self.decimal.Decimal):
835+
pass
836+
self.test_from_number(DecimalSubclass)
837+
815838
def test_unicode_digits(self):
816839
Decimal = self.decimal.Decimal
817840

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

1306+
12831307
@requires_cdecimal
12841308
class CFormatTest(FormatTest, unittest.TestCase):
12851309
decimal = C
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add alternative :class:`~decimal.Decimal` constructor
2+
:meth:`Decimal.from_number() <decimal.Decimal.from_number>`.

Modules/_decimal/_decimal.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2828,6 +2828,51 @@ dec_from_float(PyObject *type, PyObject *pyfloat)
28282828
return result;
28292829
}
28302830

2831+
/* 'v' can have any numeric type accepted by the Decimal constructor. Attempt
2832+
an exact conversion. If the result does not meet the restrictions
2833+
for an mpd_t, fail with InvalidOperation. */
2834+
static PyObject *
2835+
PyDecType_FromNumberExact(PyTypeObject *type, PyObject *v, PyObject *context)
2836+
{
2837+
decimal_state *state = get_module_state_by_def(type);
2838+
assert(v != NULL);
2839+
if (PyDec_Check(state, v)) {
2840+
return PyDecType_FromDecimalExact(type, v, context);
2841+
}
2842+
else if (PyLong_Check(v)) {
2843+
return PyDecType_FromLongExact(type, v, context);
2844+
}
2845+
else if (PyFloat_Check(v)) {
2846+
if (dec_addstatus(context, MPD_Float_operation)) {
2847+
return NULL;
2848+
}
2849+
return PyDecType_FromFloatExact(type, v, context);
2850+
}
2851+
else {
2852+
PyErr_Format(PyExc_TypeError,
2853+
"conversion from %s to Decimal is not supported",
2854+
Py_TYPE(v)->tp_name);
2855+
return NULL;
2856+
}
2857+
}
2858+
2859+
/* class method */
2860+
static PyObject *
2861+
dec_from_number(PyObject *type, PyObject *number)
2862+
{
2863+
PyObject *context;
2864+
PyObject *result;
2865+
2866+
decimal_state *state = get_module_state_by_def((PyTypeObject *)type);
2867+
CURRENT_CONTEXT(state, context);
2868+
result = PyDecType_FromNumberExact(state->PyDec_Type, number, context);
2869+
if (type != (PyObject *)state->PyDec_Type && result != NULL) {
2870+
Py_SETREF(result, PyObject_CallFunctionObjArgs(type, result, NULL));
2871+
}
2872+
2873+
return result;
2874+
}
2875+
28312876
/* create_decimal_from_float */
28322877
static PyObject *
28332878
ctx_from_float(PyObject *context, PyObject *v)
@@ -5017,6 +5062,7 @@ static PyMethodDef dec_methods [] =
50175062

50185063
/* Miscellaneous */
50195064
{ "from_float", dec_from_float, METH_O|METH_CLASS, doc_from_float },
5065+
{ "from_number", dec_from_number, METH_O|METH_CLASS, doc_from_number },
50205066
{ "as_tuple", PyDec_AsTuple, METH_NOARGS, doc_as_tuple },
50215067
{ "as_integer_ratio", dec_as_integer_ratio, METH_NOARGS, doc_as_integer_ratio },
50225068

Modules/_decimal/docstrings.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ Decimal.from_float(0.1) is not the same as Decimal('0.1').\n\
189189
\n\
190190
\n");
191191

192+
PyDoc_STRVAR(doc_from_number,
193+
"from_number($type, number, /)\n--\n\n\
194+
Class method that converts a real number to a decimal number, exactly.\n\
195+
\n\
196+
>>> Decimal.from_number(314) # int\n\
197+
Decimal('314')\n\
198+
>>> Decimal.from_number(0.1) # float\n\
199+
Decimal('0.1000000000000000055511151231257827021181583404541015625')\n\
200+
>>> Decimal.from_number(Decimal('3.14')) # another decimal instance\n\
201+
Decimal('3.14')\n\
202+
\n\
203+
\n");
204+
192205
PyDoc_STRVAR(doc_fma,
193206
"fma($self, /, other, third, context=None)\n--\n\n\
194207
Fused multiply-add. Return self*other+third with no rounding of the\n\

0 commit comments

Comments
 (0)