]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-121798: Add class method Decimal.from_number() (GH-121801)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 14 Oct 2024 08:24:01 +0000 (11:24 +0300)
committerGitHub <noreply@github.com>
Mon, 14 Oct 2024 08:24:01 +0000 (08:24 +0000)
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.

Doc/library/decimal.rst
Doc/whatsnew/3.14.rst
Lib/_pydecimal.py
Lib/test/test_decimal.py
Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst [new file with mode: 0644]
Modules/_decimal/_decimal.c
Modules/_decimal/docstrings.h

index 916f17cadfaa7ed560f76789fb049aa966d03d57..c9a3e448cad063016da48fd4b4d5721205d320e3 100644 (file)
@@ -598,6 +598,23 @@ Decimal objects
 
       .. versionadded:: 3.1
 
+   .. classmethod:: from_number(number)
+
+      Alternative constructor that only accepts 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
index b22d1bd1e99d4e5df88f946f88ac3cc9396a6abe..25e69a59bdec62f10608d8f798d861a8df91f836 100644 (file)
@@ -239,6 +239,12 @@ ctypes
   to help match a non-default ABI.
   (Contributed by Petr Viktorin in :gh:`97702`.)
 
+decimal
+-------
+
+* Add alternative :class:`~decimal.Decimal` constructor
+  :meth:`Decimal.from_number() <decimal.Decimal.from_number>`.
+  (Contributed by Serhiy Storchaka in :gh:`121798`.)
 
 dis
 ---
index 75df3db262470b7f6555606c38796b778926abaa..5b60570c6c592aa44c9a23ace4616ce6d35f9762 100644 (file)
@@ -582,6 +582,21 @@ class Decimal(object):
 
         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.
index d1e7e69e7e951b3ff849ed104467ed5dc6896c40..bc6c64277409497198a0eda822d1d8b181d7ff11 100644 (file)
@@ -812,6 +812,29 @@ class ExplicitConstructionTest:
             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
 
diff --git a/Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst b/Misc/NEWS.d/next/Library/2024-07-15-19-25-25.gh-issue-121798.GmuBDu.rst
new file mode 100644 (file)
index 0000000..5706e4b
--- /dev/null
@@ -0,0 +1,2 @@
+Add alternative :class:`~decimal.Decimal` constructor
+:meth:`Decimal.from_number() <decimal.Decimal.from_number>`.
index a33c9793b5ad17d7e46588a65d5674feac4626ca..c564813036e5040ba2e72b5165c47868e68699b0 100644 (file)
@@ -2857,6 +2857,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)
@@ -5052,6 +5097,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 },
 
index a1823cdd32b74c1281c34fd585ae88e08205da61..b34bff83d3f4e95a679c08b789f08ad181737a25 100644 (file)
@@ -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\