From 5e4db08bb0db9017b0a442a6eb420b913f723737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:35:37 +0200 Subject: [PATCH 1/4] add `math.signbit` --- Doc/library/math.rst | 10 +++++ Doc/whatsnew/3.15.rst | 3 ++ Lib/test/test_math.py | 15 +++++++- ...-06-24-10-23-37.gh-issue-135853.6xDNOG.rst | 2 + Modules/clinic/mathmodule.c.h | 38 ++++++++++++++++++- Modules/mathmodule.c | 20 ++++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst diff --git a/Doc/library/math.rst b/Doc/library/math.rst index ecb1d4102cac31..cbef44b2022288 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -59,6 +59,7 @@ noted otherwise, all return values are floats. :func:`isnan(x) ` Check if *x* is a NaN (not a number) :func:`ldexp(x, i) ` ``x * (2**i)``, inverse of function :func:`frexp` :func:`nextafter(x, y, steps) ` Floating-point value *steps* steps after *x* towards *y* +:func:`signbit(x) ` Check if *x* is a negative number :func:`ulp(x) ` Value of the least significant bit of *x* **Power, exponential and logarithmic functions** @@ -431,6 +432,15 @@ Floating point manipulation functions Added the *steps* argument. +.. function:: signbit(x) + + Return :const:`True` if *x* is negative and :const:`False` otherwise. + + This is useful to detect the sign bit of zeroes, infinities and NaNs. + + .. versionadded:: next + + .. function:: ulp(x) Return the value of the least significant bit of the float *x*: diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..e58b4250f356c2 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -115,6 +115,9 @@ math * Add :func:`math.isnormal` and :func:`math.issubnormal` functions. (Contributed by Sergey B Kirpichev in :gh:`132908`.) +* Add :func:`math.signbit` function. + (Contributed by Bénédikt Tran in :gh:`135853`.) + os.path ------- diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 384ad5c828d9b3..c01793e24e1f05 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -17,8 +17,13 @@ eps = 1E-05 NAN = float('nan') +NNAN = float('-nan') +DNAN = decimal.Decimal("nan") +DNNAN = decimal.Decimal("-nan") INF = float('inf') NINF = float('-inf') +DINF = decimal.Decimal("inf") +DNINF = decimal.Decimal("-inf") FLOAT_MAX = sys.float_info.max FLOAT_MIN = sys.float_info.min @@ -475,6 +480,14 @@ def testCopysign(self): # similarly, copysign(2., NAN) could be 2. or -2. self.assertEqual(abs(math.copysign(2., NAN)), 2.) + def test_signbit(self): + for arg in [0, 0., 1, 1., INF, NAN, DINF, DNAN]: + with self.subTest('positive', arg=arg): + self.assertFalse(math.signbit(arg)) + for arg in [-0., -1, -1., NINF, NNAN, DNINF, DNNAN]: + with self.subTest('negative', arg=arg): + self.assertTrue(math.signbit(arg)) + def testCos(self): self.assertRaises(TypeError, math.cos) self.ftest('cos(-pi/2)', math.cos(-math.pi/2), 0, abs_tol=math.ulp(1)) @@ -1387,7 +1400,6 @@ def __rmul__(self, other): args = ((-5, -5, 10), (1.5, 4611686018427387904, 2305843009213693952)) self.assertEqual(sumprod(*args), 0.0) - @requires_IEEE_754 @unittest.skipIf(HAVE_DOUBLE_ROUNDING, "sumprod() accuracy not guaranteed on machines with double rounding") @@ -2486,7 +2498,6 @@ def test_nextafter(self): with self.assertRaises(ValueError): math.nextafter(1.0, INF, steps=-1) - @requires_IEEE_754 def test_ulp(self): self.assertEqual(math.ulp(1.0), sys.float_info.epsilon) diff --git a/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst b/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst new file mode 100644 index 00000000000000..7ed467f2eb4986 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst @@ -0,0 +1,2 @@ +:mod:`math`: expose C99 :func:`~math.signbit` function to determine whether +a floating-point value is negative. Patch by Bénédikt Tran. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index fbb012fb6dd9e1..46d1006394a17b 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -84,6 +84,42 @@ PyDoc_STRVAR(math_floor__doc__, #define MATH_FLOOR_METHODDEF \ {"floor", (PyCFunction)math_floor, METH_O, math_floor__doc__}, +PyDoc_STRVAR(math_signbit__doc__, +"signbit($module, x, /)\n" +"--\n" +"\n" +"Return True if \'x\' is negative and False otherwise.\n" +"\n" +"This is useful to detect the sign bit of zeroes, infinities and NaNs."); + +#define MATH_SIGNBIT_METHODDEF \ + {"signbit", (PyCFunction)math_signbit, METH_O, math_signbit__doc__}, + +static PyObject * +math_signbit_impl(PyObject *module, double x); + +static PyObject * +math_signbit(PyObject *module, PyObject *arg) +{ + PyObject *return_value = NULL; + double x; + + if (PyFloat_CheckExact(arg)) { + x = PyFloat_AS_DOUBLE(arg); + } + else + { + x = PyFloat_AsDouble(arg); + if (x == -1.0 && PyErr_Occurred()) { + goto exit; + } + } + return_value = math_signbit_impl(module, x); + +exit: + return return_value; +} + PyDoc_STRVAR(math_fsum__doc__, "fsum($module, seq, /)\n" "--\n" @@ -1178,4 +1214,4 @@ math_ulp(PyObject *module, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=44bba3a0a052a364 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=ae85f0ca1de2c864 input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index bbbb49115681de..352b2ed2fd6e34 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1233,6 +1233,25 @@ FUNC2(remainder, m_remainder, "Return x - n*y where n*y is the closest integer multiple of y.\n" "In the case where x is exactly halfway between two multiples of\n" "y, the nearest even value of n is used. The result is always exact.") + +/*[clinic input] +math.signbit + + x: double + / + +Return True if 'x' is negative and False otherwise. + +This is useful to detect the sign bit of zeroes, infinities and NaNs. +[clinic start generated code]*/ + +static PyObject * +math_signbit_impl(PyObject *module, double x) +/*[clinic end generated code: output=20c5f20156a9b871 input=1765b06afd3c4bd0]*/ +{ + return PyBool_FromLong(signbit(x)); +} + FUNC1D(sin, sin, 0, "sin($module, x, /)\n--\n\n" "Return the sine of x (measured in radians).", @@ -4199,6 +4218,7 @@ static PyMethodDef math_methods[] = { MATH_POW_METHODDEF MATH_RADIANS_METHODDEF {"remainder", _PyCFunction_CAST(math_remainder), METH_FASTCALL, math_remainder_doc}, + MATH_SIGNBIT_METHODDEF {"sin", math_sin, METH_O, math_sin_doc}, {"sinh", math_sinh, METH_O, math_sinh_doc}, {"sqrt", math_sqrt, METH_O, math_sqrt_doc}, From 1a2d3e4d2808c73670e315d45fb162a6e7a29577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:26:50 +0200 Subject: [PATCH 2/4] address review --- Doc/library/math.rst | 2 +- Lib/test/test_math.py | 11 +++++------ .../2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst | 2 +- Modules/mathmodule.c | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index cbef44b2022288..03da0e4713c42d 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -434,7 +434,7 @@ Floating point manipulation functions .. function:: signbit(x) - Return :const:`True` if *x* is negative and :const:`False` otherwise. + Return ``True`` if the sign of *x* is negative and ``False`` otherwise. This is useful to detect the sign bit of zeroes, infinities and NaNs. diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index c01793e24e1f05..c18d4c299c1517 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -18,12 +18,8 @@ eps = 1E-05 NAN = float('nan') NNAN = float('-nan') -DNAN = decimal.Decimal("nan") -DNNAN = decimal.Decimal("-nan") INF = float('inf') NINF = float('-inf') -DINF = decimal.Decimal("inf") -DNINF = decimal.Decimal("-inf") FLOAT_MAX = sys.float_info.max FLOAT_MIN = sys.float_info.min @@ -481,10 +477,13 @@ def testCopysign(self): self.assertEqual(abs(math.copysign(2., NAN)), 2.) def test_signbit(self): - for arg in [0, 0., 1, 1., INF, NAN, DINF, DNAN]: + self.assertRaises(TypeError, math.signbit) + self.assertRaises(TypeError, math.signbit, '1.0') + + for arg in [0, 0., 1, 1., INF, NAN]: with self.subTest('positive', arg=arg): self.assertFalse(math.signbit(arg)) - for arg in [-0., -1, -1., NINF, NNAN, DNINF, DNNAN]: + for arg in [-0., -1, -1., NINF, NNAN]: with self.subTest('negative', arg=arg): self.assertTrue(math.signbit(arg)) diff --git a/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst b/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst index 7ed467f2eb4986..3fea3bc3e7cfe8 100644 --- a/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst +++ b/Misc/NEWS.d/next/Library/2025-06-24-10-23-37.gh-issue-135853.6xDNOG.rst @@ -1,2 +1,2 @@ :mod:`math`: expose C99 :func:`~math.signbit` function to determine whether -a floating-point value is negative. Patch by Bénédikt Tran. +the sign bit of a floating-point value is set. Patch by Bénédikt Tran. diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 352b2ed2fd6e34..6db93d48b87ee5 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1240,7 +1240,7 @@ math.signbit x: double / -Return True if 'x' is negative and False otherwise. +Return True if the sign of 'x' is negative and False otherwise. This is useful to detect the sign bit of zeroes, infinities and NaNs. [clinic start generated code]*/ From f6a55ceab54c8a6030470bd934d555d941a40a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:54:54 +0200 Subject: [PATCH 3/4] address review --- Modules/clinic/mathmodule.c.h | 6 ++---- Modules/mathmodule.c | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 46d1006394a17b..a443c48faaa88a 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -88,9 +88,7 @@ PyDoc_STRVAR(math_signbit__doc__, "signbit($module, x, /)\n" "--\n" "\n" -"Return True if \'x\' is negative and False otherwise.\n" -"\n" -"This is useful to detect the sign bit of zeroes, infinities and NaNs."); +"Return True if the sign of x is negative and False otherwise."); #define MATH_SIGNBIT_METHODDEF \ {"signbit", (PyCFunction)math_signbit, METH_O, math_signbit__doc__}, @@ -1214,4 +1212,4 @@ math_ulp(PyObject *module, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=ae85f0ca1de2c864 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=4e3fa94d026f027b input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 6db93d48b87ee5..fc2b309b7626fb 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1240,14 +1240,12 @@ math.signbit x: double / -Return True if the sign of 'x' is negative and False otherwise. - -This is useful to detect the sign bit of zeroes, infinities and NaNs. +Return True if the sign of x is negative and False otherwise. [clinic start generated code]*/ static PyObject * math_signbit_impl(PyObject *module, double x) -/*[clinic end generated code: output=20c5f20156a9b871 input=1765b06afd3c4bd0]*/ +/*[clinic end generated code: output=20c5f20156a9b871 input=3d3493fbcb5bdb3e]*/ { return PyBool_FromLong(signbit(x)); } From bc362a354115d2d3b0d5dbe2c5c5c8d4cdc52518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:46:12 +0200 Subject: [PATCH 4/4] address review --- Lib/test/test_math.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index c18d4c299c1517..46cb54647b1968 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -17,7 +17,6 @@ eps = 1E-05 NAN = float('nan') -NNAN = float('-nan') INF = float('inf') NINF = float('-inf') FLOAT_MAX = sys.float_info.max @@ -480,12 +479,14 @@ def test_signbit(self): self.assertRaises(TypeError, math.signbit) self.assertRaises(TypeError, math.signbit, '1.0') - for arg in [0, 0., 1, 1., INF, NAN]: - with self.subTest('positive', arg=arg): - self.assertFalse(math.signbit(arg)) - for arg in [-0., -1, -1., NINF, NNAN]: - with self.subTest('negative', arg=arg): - self.assertTrue(math.signbit(arg)) + # C11, §7.12.3.6 requires signbit() to return a nonzero value + # if and only if the sign of its argument value is negative, + # but in practice, we are only interested in a boolean value. + self.assertIsInstance(math.signbit(1.0), bool) + + for arg in [0., 1., INF, NAN]: + self.assertFalse(math.signbit(arg)) + self.assertTrue(math.signbit(-arg)) def testCos(self): self.assertRaises(TypeError, math.cos)