From 559e7f165ad03731e6bc2211c0e6d8d9c02fb549 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 23 Feb 2020 13:21:29 +0200 Subject: [PATCH] bpo-39648: Expand math.gcd() and math.lcm() to handle multiple arguments. (GH-18604) * bpo-39648: Expand math.gcd() and math.lcm() to handle multiple arguments. * Simplify fast path. * Difine lcm() without arguments returning 1. * Apply suggestions from code review Co-Authored-By: Mark Dickinson Co-authored-by: Mark Dickinson --- Doc/library/math.rst | 33 +-- Doc/whatsnew/3.9.rst | 9 +- Lib/test/test_math.py | 56 ++--- .../2020-02-22-12-49-04.bpo-39648.Y-9N7F.rst | 1 + Modules/clinic/mathmodule.c.h | 62 +----- Modules/mathmodule.c | 191 +++++++++++------- 6 files changed, 174 insertions(+), 178 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-02-22-12-49-04.bpo-39648.Y-9N7F.rst diff --git a/Doc/library/math.rst b/Doc/library/math.rst index d8ac35256d1..6ec1feee35a 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -126,23 +126,19 @@ Number-theoretic and representation functions `_\. -.. function:: gcd(a, b) +.. function:: gcd(*integers) - Return the greatest common divisor of the integers *a* and *b*. If either - *a* or *b* is nonzero, then the value of ``gcd(a, b)`` is the largest - positive integer that divides both *a* and *b*. ``gcd(0, 0)`` returns - ``0``. + Return the greatest common divisor of the specified integer arguments. + If any of the arguments is nonzero, then the returned value is the largest + positive integer that is a divisor af all arguments. If all arguments + are zero, then the returned value is ``0``. ``gcd()`` without arguments + returns ``0``. .. versionadded:: 3.5 - -.. function:: lcm(a, b) - - Return the least common multiple of integers *a* and *b*. The value of - ``lcm(a, b)`` is the smallest nonnegative integer that is a multiple of - both *a* and *b*. If either *a* or *b* is zero then ``lcm(a, b)`` is zero. - - .. versionadded:: 3.9 + .. versionchanged:: 3.9 + Added support for an arbitrary number of arguments. Formerly, only two + arguments were supported. .. function:: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0) @@ -210,6 +206,17 @@ Number-theoretic and representation functions .. versionadded:: 3.8 +.. function:: lcm(*integers) + + Return the least common multiple of the specified integer arguments. + If all arguments are nonzero, then the returned value is the smallest + positive integer that is a multiple of all arguments. If any of the arguments + is zero, then the returned value is ``0``. ``lcm()`` without arguments + returns ``1``. + + .. versionadded:: 3.9 + + .. function:: ldexp(x, i) Return ``x * (2**i)``. This is essentially the inverse of function diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 161675d32f6..a0ae4eb9b82 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -216,8 +216,13 @@ import attempts. math ---- -Add :func:`math.lcm`: return the least common multiple of *a* and *b*. -(Contributed by Ananthakrishnan in :issue:`39479`.) +Expanded the :func:`math.gcd` function to handle multiple arguments. +Formerly, it only supported two arguments. +(Contributed by Serhiy Storchaka in :issue:`39648`.) + +Add :func:`math.lcm`: return the least common multiple of specified arguments. +(Contributed by Mark Dickinson, Ananthakrishnan and Serhiy Storchaka in +:issue:`39479` and :issue:`39648`.) Add :func:`math.nextafter`: return the next floating-point value after *x* towards *y*. diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index ad8273d50cc..cc39402b3c6 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -705,33 +705,32 @@ class MathTests(unittest.TestCase): self.assertEqual(gcd(84, -120), 12) self.assertEqual(gcd(1216342683557601535506311712, 436522681849110124616458784), 32) - c = 652560 + x = 434610456570399902378880679233098819019853229470286994367836600566 y = 1064502245825115327754847244914921553977 - a = x * c - b = y * c - self.assertEqual(gcd(a, b), c) - self.assertEqual(gcd(b, a), c) - self.assertEqual(gcd(-a, b), c) - self.assertEqual(gcd(b, -a), c) - self.assertEqual(gcd(a, -b), c) - self.assertEqual(gcd(-b, a), c) - self.assertEqual(gcd(-a, -b), c) - self.assertEqual(gcd(-b, -a), c) - c = 576559230871654959816130551884856912003141446781646602790216406874 - a = x * c - b = y * c - self.assertEqual(gcd(a, b), c) - self.assertEqual(gcd(b, a), c) - self.assertEqual(gcd(-a, b), c) - self.assertEqual(gcd(b, -a), c) - self.assertEqual(gcd(a, -b), c) - self.assertEqual(gcd(-b, a), c) - self.assertEqual(gcd(-a, -b), c) - self.assertEqual(gcd(-b, -a), c) + for c in (652560, + 576559230871654959816130551884856912003141446781646602790216406874): + a = x * c + b = y * c + self.assertEqual(gcd(a, b), c) + self.assertEqual(gcd(b, a), c) + self.assertEqual(gcd(-a, b), c) + self.assertEqual(gcd(b, -a), c) + self.assertEqual(gcd(a, -b), c) + self.assertEqual(gcd(-b, a), c) + self.assertEqual(gcd(-a, -b), c) + self.assertEqual(gcd(-b, -a), c) + self.assertEqual(gcd(), 0) + self.assertEqual(gcd(120), 120) + self.assertEqual(gcd(-120), 120) + self.assertEqual(gcd(120, 84, 102), 6) + self.assertEqual(gcd(120, 1, 84), 1) + + self.assertRaises(TypeError, gcd, 120.0) self.assertRaises(TypeError, gcd, 120.0, 84) self.assertRaises(TypeError, gcd, 120, 84.0) + self.assertRaises(TypeError, gcd, 120, 1, 84.0) self.assertEqual(gcd(MyIndexable(120), MyIndexable(84)), 12) def testHypot(self): @@ -989,9 +988,9 @@ class MathTests(unittest.TestCase): self.assertEqual(lcm(1216342683557601535506311712, 436522681849110124616458784), 16592536571065866494401400422922201534178938447014944) + x = 43461045657039990237 y = 10645022458251153277 - for c in (652560, 57655923087165495981): a = x * c @@ -1005,9 +1004,18 @@ class MathTests(unittest.TestCase): self.assertEqual(lcm(-b, a), d) self.assertEqual(lcm(-a, -b), d) self.assertEqual(lcm(-b, -a), d) - self.assertEqual(lcm(MyIndexable(120), MyIndexable(84)), 840) + + self.assertEqual(lcm(), 1) + self.assertEqual(lcm(120), 120) + self.assertEqual(lcm(-120), 120) + self.assertEqual(lcm(120, 84, 102), 14280) + self.assertEqual(lcm(120, 0, 84), 0) + + self.assertRaises(TypeError, lcm, 120.0) self.assertRaises(TypeError, lcm, 120.0, 84) self.assertRaises(TypeError, lcm, 120, 84.0) + self.assertRaises(TypeError, lcm, 120, 0, 84.0) + self.assertEqual(lcm(MyIndexable(120), MyIndexable(84)), 840) def testLdexp(self): self.assertRaises(TypeError, math.ldexp) diff --git a/Misc/NEWS.d/next/Library/2020-02-22-12-49-04.bpo-39648.Y-9N7F.rst b/Misc/NEWS.d/next/Library/2020-02-22-12-49-04.bpo-39648.Y-9N7F.rst new file mode 100644 index 00000000000..f205911ad9b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-02-22-12-49-04.bpo-39648.Y-9N7F.rst @@ -0,0 +1 @@ +Expanded :func:`math.gcd` and :func:`math.lcm` to handle multiple arguments. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index df45a1a0c5e..65f3dd4f520 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -2,36 +2,6 @@ preserve [clinic start generated code]*/ -PyDoc_STRVAR(math_gcd__doc__, -"gcd($module, x, y, /)\n" -"--\n" -"\n" -"greatest common divisor of x and y"); - -#define MATH_GCD_METHODDEF \ - {"gcd", (PyCFunction)(void(*)(void))math_gcd, METH_FASTCALL, math_gcd__doc__}, - -static PyObject * -math_gcd_impl(PyObject *module, PyObject *a, PyObject *b); - -static PyObject * -math_gcd(PyObject *module, PyObject *const *args, Py_ssize_t nargs) -{ - PyObject *return_value = NULL; - PyObject *a; - PyObject *b; - - if (!_PyArg_CheckPositional("gcd", nargs, 2, 2)) { - goto exit; - } - a = args[0]; - b = args[1]; - return_value = math_gcd_impl(module, a, b); - -exit: - return return_value; -} - PyDoc_STRVAR(math_ceil__doc__, "ceil($module, x, /)\n" "--\n" @@ -85,36 +55,6 @@ PyDoc_STRVAR(math_factorial__doc__, #define MATH_FACTORIAL_METHODDEF \ {"factorial", (PyCFunction)math_factorial, METH_O, math_factorial__doc__}, -PyDoc_STRVAR(math_lcm__doc__, -"lcm($module, x, y, /)\n" -"--\n" -"\n" -"least common multiple of x and y"); - -#define MATH_LCM_METHODDEF \ - {"lcm", (PyCFunction)(void(*)(void))math_lcm, METH_FASTCALL, math_lcm__doc__}, - -static PyObject * -math_lcm_impl(PyObject *module, PyObject *a, PyObject *b); - -static PyObject * -math_lcm(PyObject *module, PyObject *const *args, Py_ssize_t nargs) -{ - PyObject *return_value = NULL; - PyObject *a; - PyObject *b; - - if (!_PyArg_CheckPositional("lcm", nargs, 2, 2)) { - goto exit; - } - a = args[0]; - b = args[1]; - return_value = math_lcm_impl(module, a, b); - -exit: - return return_value; -} - PyDoc_STRVAR(math_trunc__doc__, "trunc($module, x, /)\n" "--\n" @@ -925,4 +865,4 @@ math_ulp(PyObject *module, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=f8daa185c043a7b7 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=1eae2b3ef19568fa input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index f74b7e1a342..77e325cf0c6 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -826,36 +826,124 @@ m_log10(double x) } -/*[clinic input] -math.gcd - - x as a: object - y as b: object - / - -greatest common divisor of x and y -[clinic start generated code]*/ - static PyObject * -math_gcd_impl(PyObject *module, PyObject *a, PyObject *b) -/*[clinic end generated code: output=7b2e0c151bd7a5d8 input=c2691e57fb2a98fa]*/ +math_gcd(PyObject *module, PyObject * const *args, Py_ssize_t nargs) { - PyObject *g; + PyObject *res, *x; + Py_ssize_t i; - a = PyNumber_Index(a); - if (a == NULL) - return NULL; - b = PyNumber_Index(b); - if (b == NULL) { - Py_DECREF(a); + if (nargs == 0) { + return PyLong_FromLong(0); + } + res = PyNumber_Index(args[0]); + if (res == NULL) { return NULL; } - g = _PyLong_GCD(a, b); - Py_DECREF(a); - Py_DECREF(b); - return g; + if (nargs == 1) { + Py_SETREF(res, PyNumber_Absolute(res)); + return res; + } + for (i = 1; i < nargs; i++) { + x = PyNumber_Index(args[i]); + if (x == NULL) { + Py_DECREF(res); + return NULL; + } + if (res == _PyLong_One) { + /* Fast path: just check arguments. + It is okay to use identity comparison here. */ + Py_DECREF(x); + continue; + } + Py_SETREF(res, _PyLong_GCD(res, x)); + Py_DECREF(x); + if (res == NULL) { + return NULL; + } + } + return res; } +PyDoc_STRVAR(math_gcd_doc, +"gcd($module, *integers)\n" +"--\n" +"\n" +"Greatest Common Divisor."); + + +static PyObject * +long_lcm(PyObject *a, PyObject *b) +{ + PyObject *g, *m, *f, *ab; + + if (Py_SIZE(a) == 0 || Py_SIZE(b) == 0) { + return PyLong_FromLong(0); + } + g = _PyLong_GCD(a, b); + if (g == NULL) { + return NULL; + } + f = PyNumber_FloorDivide(a, g); + Py_DECREF(g); + if (f == NULL) { + return NULL; + } + m = PyNumber_Multiply(f, b); + Py_DECREF(f); + if (m == NULL) { + return NULL; + } + ab = PyNumber_Absolute(m); + Py_DECREF(m); + return ab; +} + + +static PyObject * +math_lcm(PyObject *module, PyObject * const *args, Py_ssize_t nargs) +{ + PyObject *res, *x; + Py_ssize_t i; + + if (nargs == 0) { + return PyLong_FromLong(1); + } + res = PyNumber_Index(args[0]); + if (res == NULL) { + return NULL; + } + if (nargs == 1) { + Py_SETREF(res, PyNumber_Absolute(res)); + return res; + } + for (i = 1; i < nargs; i++) { + x = PyNumber_Index(args[i]); + if (x == NULL) { + Py_DECREF(res); + return NULL; + } + if (res == _PyLong_Zero) { + /* Fast path: just check arguments. + It is okay to use identity comparison here. */ + Py_DECREF(x); + continue; + } + Py_SETREF(res, long_lcm(res, x)); + Py_DECREF(x); + if (res == NULL) { + return NULL; + } + } + return res; +} + + +PyDoc_STRVAR(math_lcm_doc, +"lcm($module, *integers)\n" +"--\n" +"\n" +"Least Common Multiple."); + /* Call is_error when errno != 0, and where x is the result libm * returned. is_error will usually set up an exception and return @@ -2017,59 +2105,6 @@ math_factorial(PyObject *module, PyObject *arg) } -/*[clinic input] -math.lcm - x as a: object - y as b: object - / -least common multiple of x and y -[clinic start generated code]*/ - -static PyObject * -math_lcm_impl(PyObject *module, PyObject *a, PyObject *b) -/*[clinic end generated code: output=6f83fb6d671074ba input=efb3d7b7334b7118]*/ -{ - PyObject *g, *m, *f, *ab; - - a = PyNumber_Index(a); - if (a == NULL) { - return NULL; - } - b = PyNumber_Index(b); - if (b == NULL) { - Py_DECREF(a); - return NULL; - } - if (_PyLong_Sign(a) == 0 || _PyLong_Sign(b) == 0) { - Py_DECREF(a); - Py_DECREF(b); - return PyLong_FromLong(0); - } - g = _PyLong_GCD(a, b); - if (g == NULL) { - Py_DECREF(a); - Py_DECREF(b); - return NULL; - } - f = PyNumber_FloorDivide(a, g); - Py_DECREF(g); - Py_DECREF(a); - if (f == NULL) { - Py_DECREF(b); - return NULL; - } - m = PyNumber_Multiply(f, b); - Py_DECREF(f); - Py_DECREF(b); - if (m == NULL) { - return NULL; - } - ab = PyNumber_Absolute(m); - Py_DECREF(m); - return ab; -} - - /*[clinic input] math.trunc @@ -3408,14 +3443,14 @@ static PyMethodDef math_methods[] = { MATH_FREXP_METHODDEF MATH_FSUM_METHODDEF {"gamma", math_gamma, METH_O, math_gamma_doc}, - MATH_GCD_METHODDEF + {"gcd", (PyCFunction)(void(*)(void))math_gcd, METH_FASTCALL, math_gcd_doc}, {"hypot", (PyCFunction)(void(*)(void))math_hypot, METH_FASTCALL, math_hypot_doc}, MATH_ISCLOSE_METHODDEF MATH_ISFINITE_METHODDEF MATH_ISINF_METHODDEF MATH_ISNAN_METHODDEF MATH_ISQRT_METHODDEF - MATH_LCM_METHODDEF + {"lcm", (PyCFunction)(void(*)(void))math_lcm, METH_FASTCALL, math_lcm_doc}, MATH_LDEXP_METHODDEF {"lgamma", math_lgamma, METH_O, math_lgamma_doc}, MATH_LOG_METHODDEF