Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions Doc/library/isPnz.rst
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions Lib/test/test_isPnz/test_isPnz.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions Misc/NEWS.d/next/Core_and_Builtins/2024-12-21-01.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add isPnz() as a new built-in function to determine if a number is positive, negative, or zero.
79 changes: 79 additions & 0 deletions Modules/isPnzmodule.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include <Python.h>
#include <math.h>
#include <float.h> // 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);
}
1 change: 1 addition & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
94 changes: 93 additions & 1 deletion Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <Python.h>
#include <math.h>
#include <float.h> // 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

Expand Down Expand Up @@ -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},
};

Expand Down
Loading