diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index a7549b9bce76e2..c0ccae1d6c9a97 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -38,6 +38,7 @@ are always available. They are listed here in alphabetical order. | | :func:`dir` | | :func:`isinstance` | | | | | | | :func:`divmod` | | :func:`issubclass` | | | | **_** | | | | | :func:`iter` | | | | :func:`__import__` | +| | | | :func:`isPnz` | | | | | +-------------------------+-----------------------+-----------------------+-------------------------+ .. using :func:`dict` would create a link to another page, so local targets are @@ -1131,6 +1132,45 @@ are always available. They are listed here in alphabetical order. process_block(block) + +.. function:: isPnz(x) + + Determine if a number is positive, negative, or zero. + + Returns: + * ``True`` if *x* is positive (non-zero) + * ``False`` if *x* is negative (non-zero) or negative zero (-0.0) + * ``None`` if *x* is positive zero (+0.0) + + This function uses ``copysign`` to detect signed zeros, + ensuring compatibility with IEEE 754 floating-point standards. + + :param x: A number (integer or float) + :type x: int or float + :return: ``True``, ``False``, or ``None`` based on the sign of the number + :rtype: bool or None + :raises TypeError: If the input is not a number + :raises OverflowError: If the input is too large or too small + :raises ValueError: If the input is NaN + + Example:: + + >>> isPnz(10) + True + >>> isPnz(-5) + False + >>> isPnz(0.0) + None + >>> isPnz(-0.0) + False + + .. note:: + This function is intended for use with numbers following the IEEE 754 standard for floating-point arithmetic. + + .. versionadded:: 3.12 + + + .. function:: len(s) Return the length (the number of items) of an object. The argument may be a diff --git a/Doc/library/isPnz.rst b/Doc/library/isPnz.rst new file mode 100644 index 00000000000000..9c649478feb1ba --- /dev/null +++ b/Doc/library/isPnz.rst @@ -0,0 +1,40 @@ +.. function:: isPnz(x) + + Determine if a number is positive, negative, or zero. + + :param x: A number (integer or float) + :type x: int or float + :return: The sign of the number + :rtype: bool or None + :raises TypeError: if the input is not a number + :raises OverflowError: if the input is too large or too small + :raises ValueError: if the input is NaN + + Returns: + * ``True`` if *x* is positive (non-zero) + * ``False`` if *x* is negative (non-zero) or negative zero (-0.0) + * ``None`` if *x* is positive zero (+0.0) + + This function uses ``copysign`` to detect signed zeros, + ensuring compatibility with IEEE 754 floating-point standards. + + The function can accept both integer and floating-point values. + Integer values are automatically converted to floating-point numbers before comparison. + + Example:: + + >>> isPnz(10) + True + >>> isPnz(-5) + False + >>> isPnz(0.0) + None + >>> isPnz(-0.0) + False + >>> isPnz("text") + TypeError: Argument must be an int or float + + .. note:: + This function is intended for use with numbers following the IEEE 754 standard for floating-point arithmetic. + + .. versionadded:: 3.12 \ No newline at end of file diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 4fffc78a237791..30c40f93595903 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -1938,6 +1938,9 @@ New Features get a frame variable by its name. (Contributed by Victor Stinner in :gh:`91248`.) +* Add :func:`isPnz` as a new built-in function to determine if a number is positive, negative, or zero. + (Contributed by Novelkathor in :gh:``.) + * Add :c:func:`PyErr_GetRaisedException` and :c:func:`PyErr_SetRaisedException` for saving and restoring the current exception. These functions return and accept a single exception object, diff --git a/Lib/test/test_isPnz/test_isPnz.py b/Lib/test/test_isPnz/test_isPnz.py new file mode 100644 index 00000000000000..9ae7cf898845fa --- /dev/null +++ b/Lib/test/test_isPnz/test_isPnz.py @@ -0,0 +1,60 @@ +import unittest +import time + +class TestIsPnz(unittest.TestCase): + + def _time_execution(self, test_func): + start_time = time.time() + result = test_func() + end_time = time.time() + print(f"{test_func.__name__} took {end_time - start_time:.6f} seconds.") + return result + + # Test positive numbers + def test_positive_float(self): + self._time_execution(lambda: self.assertEqual(isPnz(43534.43534534), True)) + self._time_execution(lambda: self.assertEqual(isPnz(0.143234234), True)) + + def test_positive_int(self): + self._time_execution(lambda: self.assertEqual(isPnz(1), True)) + self._time_execution(lambda: self.assertEqual(isPnz(999999999999), True)) + + # Test negative numbers + def test_negative_float(self): + self._time_execution(lambda: self.assertEqual(isPnz(-43534.43534534), False)) + self._time_execution(lambda: self.assertEqual(isPnz(-0.23424), False)) + self._time_execution(lambda: self.assertEqual(isPnz(-23.3342354636), False)) + + def test_negative_int(self): + self._time_execution(lambda: self.assertEqual(isPnz(-1), False)) + self._time_execution(lambda: self.assertEqual(isPnz(-999999999999), False)) + + # Test zeros + def test_positive_zero(self): + self._time_execution(lambda: self.assertIs(isPnz(0.0), None)) # Positive zero with float + + def test_negative_zero(self): + self._time_execution(lambda: self.assertEqual(isPnz(-0.0), False)) # Negative zero + + # Test positive and negative non-zero floats + def test_positive_float_non_zero(self): + self._time_execution(lambda: self.assertEqual(isPnz(0.03243423), True)) # Positive float + + def test_negative_float_non_zero(self): + self._time_execution(lambda: self.assertEqual(isPnz(-0.03243423), False)) # Negative float + + # Test edge cases + def test_very_small_positive(self): + self._time_execution(lambda: self.assertEqual(isPnz(1e-10), True)) + + def test_very_small_negative(self): + self._time_execution(lambda: self.assertEqual(isPnz(-1e-10), False)) + + def test_very_large_positive(self): + self._time_execution(lambda: self.assertEqual(isPnz(1e+10), True)) + + def test_very_large_negative(self): + self._time_execution(lambda: self.assertEqual(isPnz(-1e+50), False)) + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-21-01.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-21-01.rst new file mode 100644 index 00000000000000..6b15f511650b51 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-21-01.rst @@ -0,0 +1 @@ +Add isPnz() as a new built-in function to determine if a number is positive, negative, or zero. diff --git a/Modules/isPnzmodule.c b/Modules/isPnzmodule.c new file mode 100644 index 00000000000000..5dc0aa8c21615a --- /dev/null +++ b/Modules/isPnzmodule.c @@ -0,0 +1,79 @@ +#include +#include +#include // For DBL_MAX and DBL_MIN + +// Add the docstring for the function +static char isPnz_doc[] = + "isPnz(x)\n" + "------------\n" + "Determines if a number is positive, negative, or zero.\n\n" + "Returns True if the number is positive (non-zero),\n" + "False if the number is negative (non-zero),\n" + "None if the number is positive zero (+0.0),\n" + "False if the number is negative zero (-0.0).\n\n" + "The function uses `copysign` to detect signed zeros.\n" + "This ensures compatibility with IEEE 754 floating-point standards.\n\n" + "The function accepts both integers and floating-point numbers.\n" + "It converts integers to floating-point numbers before comparison.\n"; + +// Function definition for isPnz +static PyObject * +isPnz(PyObject *self, PyObject *arg) +{ + double x; + // Fast conversion for both int and float types with overflow check + ASSIGN_NUMBER(x, arg, error); + + // Check for NaN (Not a Number) + if (isnan(x)) { + PyErr_SetString(PyExc_ValueError, "The value is NaN."); + return NULL; + } + + // Check for infinity (positive or negative) + if (isinf(x)) { + if (x > 0.0) + Py_RETURN_TRUE; // Positive infinity + else + Py_RETURN_FALSE; // Negative infinity + } + + // Check for negative zero explicitly using copysign + if (x == 0.0) { + if (copysign(1.0, x) > 0.0) { + Py_RETURN_NONE; // Positive zero + } else { + Py_RETURN_FALSE; // Negative zero + } + } + + if (x > 0.0) + Py_RETURN_TRUE; // Positive non-zero + else + Py_RETURN_FALSE; // Negative non-zero + +error: + return NULL; // Invalid input, propagate the error +} + +// Define the methods table +static PyMethodDef methods[] = { + {"isPnz", isPnz, METH_O, isPnz_doc}, // Link docstring to the function + {NULL, NULL, 0, NULL} // Sentinel +}; + +// Module definition +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "isPnzmodule", // Name of the module + "Module for isPnz function", // Module docstring + -1, + methods +}; + +// Module initialization function +PyMODINIT_FUNC +PyInit_isPnzmodule(void) +{ + return PyModule_Create(&moduledef); +} diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 29638114dd94a9..5e8102951dffca 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -324,6 +324,7 @@ m_sinpi(double x) information in e*g/y. */ + #define LANCZOS_N 13 static const double lanczos_g = 6.024680040776729583740234375; static const double lanczos_g_minus_half = 5.524680040776729583740234375; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index fb9868b3740b8c..b93fa6c2274a33 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -14,7 +14,8 @@ #include "pycore_sysmodule.h" // _PySys_GetAttr() #include "pycore_tuple.h" // _PyTuple_FromArray() #include "pycore_cell.h" // PyCell_GetRef() - +#include "math.h" +#include "float.h" #include "clinic/bltinmodule.c.h" #ifdef HAVE_UNISTD_H @@ -2902,6 +2903,96 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start) } +/* + Function: isPnz + --------------------- + Determines if a number is positive, negative, or zero. + + - Returns True if the number is positive (non-zero). + - Returns False if the number is negative (non-zero). + - Returns None if the number is positive zero (+0.0). + - Returns False if the number is negative zero (-0.0). + + The function checks for: + - **NaN (Not a Number)** and raises a `ValueError` if the input is NaN. + - **Infinity (positive or negative)**, returning True for positive infinity and False for negative infinity. + - **Signed zero** using `copysign`, which ensures accurate detection of positive and negative zeros, in line with IEEE 754 floating-point standards. + + The function accepts both integers and floating-point numbers: + - Integers are converted to floating-point numbers before comparison. + - If the number exceeds the maximum or minimum allowed double values (`DBL_MAX`), an `OverflowError` is raised. + + References: + - IEEE 754 Standard for Floating-Point Arithmetic. + - Python's handling of `NaN`, `Infinity`, and `zero` values. + +*/ + +#include +#include +#include // For DBL_MAX and DBL_MIN + +#define ASSIGN_NUMBER(x, arg, error) \ + if (PyFloat_Check(arg)) { \ + x = PyFloat_AsDouble(arg); \ + if (x > DBL_MAX || x < -DBL_MAX) { \ + PyErr_SetString(PyExc_OverflowError, "The value is too large or too small."); \ + goto error; \ + } \ + } else if (PyLong_Check(arg)) { \ + /* Convert Python long to double (can handle very large integers) */ \ + x = PyLong_AsDouble(arg); \ + if (x > DBL_MAX || x < -DBL_MAX) { \ + PyErr_SetString(PyExc_OverflowError, "The value is too large or too small."); \ + goto error; \ + } \ + } else { \ + PyErr_SetString(PyExc_TypeError, "Argument must be an int or float"); \ + goto error; \ + } + + +static PyObject * +isPnz(PyObject *self, PyObject *arg) +{ + double x; + + // Fast conversion for both int and float types with overflow check + ASSIGN_NUMBER(x, arg, error); + + // Check for NaN (Not a Number) + if (isnan(x)) { + PyErr_SetString(PyExc_ValueError, "The value is NaN."); + return NULL; + } + + // Check for infinity (positive or negative) + if (isinf(x)) { + if (x > 0.0) + Py_RETURN_TRUE; // Positive infinity + else + Py_RETURN_FALSE; // Negative infinity + } + + // Check for negative zero explicitly using copysign + if (x == 0.0) { + if (copysign(1.0, x) > 0.0) { + Py_RETURN_NONE; // Positive zero + } else { + Py_RETURN_FALSE; // Negative zero + } + } + + if (x > 0.0) + Py_RETURN_TRUE; // Positive non-zero + else + Py_RETURN_FALSE; // Negative non-zero + +error: + return NULL; // Invalid input, propagate the error +} + + /*[clinic input] isinstance as builtin_isinstance @@ -3273,6 +3364,7 @@ static PyMethodDef builtin_methods[] = { BUILTIN_SORTED_METHODDEF BUILTIN_SUM_METHODDEF {"vars", builtin_vars, METH_VARARGS, vars_doc}, + {"isPnz", isPnz, METH_O, PyDoc_STR("Determine if a number is positive, negative, or zero.")}, {NULL, NULL}, };