This PR updates `math.nextafter` to add a new `steps` argument. The behaviour is as though `math.nextafter` had been called `steps` times in succession.
---------
Co-authored-by: Mark Dickinson <mdickinson@enthought.com>
of *x* and are floats.
-.. function:: nextafter(x, y)
+.. function:: nextafter(x, y, steps=1)
- Return the next floating-point value after *x* towards *y*.
+ Return the floating-point value *steps* steps after *x* towards *y*.
- If *x* is equal to *y*, return *y*.
+ If *x* is equal to *y*, return *y*, unless *steps* is zero.
Examples:
See also :func:`math.ulp`.
+ .. versionchanged:: 3.12
+ Added the *steps* argument.
+
.. versionadded:: 3.9
.. function:: perm(n, k=None)
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdin));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdout));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(step));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(steps));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(store_name));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strategy));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strftime));
STRUCT_FOR_ID(stdin)
STRUCT_FOR_ID(stdout)
STRUCT_FOR_ID(step)
+ STRUCT_FOR_ID(steps)
STRUCT_FOR_ID(store_name)
STRUCT_FOR_ID(strategy)
STRUCT_FOR_ID(strftime)
INIT_ID(stdin), \
INIT_ID(stdout), \
INIT_ID(step), \
+ INIT_ID(steps), \
INIT_ID(store_name), \
INIT_ID(strategy), \
INIT_ID(strftime), \
string = &_Py_ID(step);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
+ string = &_Py_ID(steps);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ _PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(store_name);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
float.fromhex('0x1.fffffffffffffp-1'))
self.assertEqual(math.nextafter(1.0, INF),
float.fromhex('0x1.0000000000001p+0'))
+ self.assertEqual(math.nextafter(1.0, -INF, steps=1),
+ float.fromhex('0x1.fffffffffffffp-1'))
+ self.assertEqual(math.nextafter(1.0, INF, steps=1),
+ float.fromhex('0x1.0000000000001p+0'))
+ self.assertEqual(math.nextafter(1.0, -INF, steps=3),
+ float.fromhex('0x1.ffffffffffffdp-1'))
+ self.assertEqual(math.nextafter(1.0, INF, steps=3),
+ float.fromhex('0x1.0000000000003p+0'))
# x == y: y is returned
- self.assertEqual(math.nextafter(2.0, 2.0), 2.0)
- self.assertEqualSign(math.nextafter(-0.0, +0.0), +0.0)
- self.assertEqualSign(math.nextafter(+0.0, -0.0), -0.0)
+ for steps in range(1, 5):
+ self.assertEqual(math.nextafter(2.0, 2.0, steps=steps), 2.0)
+ self.assertEqualSign(math.nextafter(-0.0, +0.0, steps=steps), +0.0)
+ self.assertEqualSign(math.nextafter(+0.0, -0.0, steps=steps), -0.0)
# around 0.0
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
self.assertIsNaN(math.nextafter(1.0, NAN))
self.assertIsNaN(math.nextafter(NAN, NAN))
+ self.assertEqual(1.0, math.nextafter(1.0, INF, steps=0))
+ 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)
--- /dev/null
+import functools
+import unittest
+from math import isnan, nextafter
+from test.support import requires_IEEE_754
+from test.support.hypothesis_helper import hypothesis
+
+floats = hypothesis.strategies.floats
+integers = hypothesis.strategies.integers
+
+
+def assert_equal_float(x, y):
+ assert isnan(x) and isnan(y) or x == y
+
+
+def via_reduce(x, y, steps):
+ return functools.reduce(nextafter, [y] * steps, x)
+
+
+class NextafterTests(unittest.TestCase):
+ @requires_IEEE_754
+ @hypothesis.given(
+ x=floats(),
+ y=floats(),
+ steps=integers(min_value=0, max_value=2**16))
+ def test_count(self, x, y, steps):
+ assert_equal_float(via_reduce(x, y, steps),
+ nextafter(x, y, steps=steps))
+
+ @requires_IEEE_754
+ @hypothesis.given(
+ x=floats(),
+ y=floats(),
+ a=integers(min_value=0),
+ b=integers(min_value=0))
+ def test_addition_commutes(self, x, y, a, b):
+ first = nextafter(x, y, steps=a)
+ second = nextafter(first, y, steps=b)
+ combined = nextafter(x, y, steps=a+b)
+ hypothesis.note(f"{first} -> {second} == {combined}")
+
+ assert_equal_float(second, combined)
--- /dev/null
+Support multiple steps in :func:`math.nextafter`. Patch by Shantanu Jain and Matthias Gorgens.
}
PyDoc_STRVAR(math_nextafter__doc__,
-"nextafter($module, x, y, /)\n"
+"nextafter($module, x, y, /, *, steps=None)\n"
"--\n"
"\n"
-"Return the next floating-point value after x towards y.");
+"Return the floating-point value the given number of steps after x towards y.\n"
+"\n"
+"If steps is not specified or is None, it defaults to 1.\n"
+"\n"
+"Raises a TypeError, if x or y is not a double, or if steps is not an integer.\n"
+"Raises ValueError if steps is negative.");
#define MATH_NEXTAFTER_METHODDEF \
- {"nextafter", _PyCFunction_CAST(math_nextafter), METH_FASTCALL, math_nextafter__doc__},
+ {"nextafter", _PyCFunction_CAST(math_nextafter), METH_FASTCALL|METH_KEYWORDS, math_nextafter__doc__},
static PyObject *
-math_nextafter_impl(PyObject *module, double x, double y);
+math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps);
static PyObject *
-math_nextafter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+math_nextafter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 1
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_item = { &_Py_ID(steps), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"", "", "steps", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "nextafter",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[3];
+ Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2;
double x;
double y;
+ PyObject *steps = Py_None;
- if (!_PyArg_CheckPositional("nextafter", nargs, 2, 2)) {
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
+ if (!args) {
goto exit;
}
if (PyFloat_CheckExact(args[0])) {
goto exit;
}
}
- return_value = math_nextafter_impl(module, x, y);
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ steps = args[2];
+skip_optional_kwonly:
+ return_value = math_nextafter_impl(module, x, y, steps);
exit:
return return_value;
exit:
return return_value;
}
-/*[clinic end generated code: output=a6437a3ba18c486a input=a9049054013a1b77]*/
+/*[clinic end generated code: output=91a0357265a2a553 input=a9049054013a1b77]*/
x: double
y: double
/
+ *
+ steps: object = None
+
+Return the floating-point value the given number of steps after x towards y.
+
+If steps is not specified or is None, it defaults to 1.
-Return the next floating-point value after x towards y.
+Raises a TypeError, if x or y is not a double, or if steps is not an integer.
+Raises ValueError if steps is negative.
[clinic start generated code]*/
static PyObject *
-math_nextafter_impl(PyObject *module, double x, double y)
-/*[clinic end generated code: output=750c8266c1c540ce input=02b2d50cd1d9f9b6]*/
+math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps)
+/*[clinic end generated code: output=cc6511f02afc099e input=7f2a5842112af2b4]*/
{
#if defined(_AIX)
if (x == y) {
return PyFloat_FromDouble(y);
}
#endif
- return PyFloat_FromDouble(nextafter(x, y));
+ if (steps == Py_None) {
+ // fast path: we default to one step.
+ return PyFloat_FromDouble(nextafter(x, y));
+ }
+ steps = PyNumber_Index(steps);
+ if (steps == NULL) {
+ return NULL;
+ }
+ assert(PyLong_CheckExact(steps));
+ if (_PyLong_IsNegative((PyLongObject *)steps)) {
+ PyErr_SetString(PyExc_ValueError,
+ "steps must be a non-negative integer");
+ Py_DECREF(steps);
+ return NULL;
+ }
+
+ unsigned long long usteps_ull = PyLong_AsUnsignedLongLong(steps);
+ // Conveniently, uint64_t and double have the same number of bits
+ // on all the platforms we care about.
+ // So if an overflow occurs, we can just use UINT64_MAX.
+ Py_DECREF(steps);
+ if (usteps_ull >= UINT64_MAX) {
+ // This branch includes the case where an error occurred, since
+ // (unsigned long long)(-1) = ULLONG_MAX >= UINT64_MAX. Note that
+ // usteps_ull can be strictly larger than UINT64_MAX on a machine
+ // where unsigned long long has width > 64 bits.
+ if (PyErr_Occurred()) {
+ if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
+ PyErr_Clear();
+ }
+ else {
+ return NULL;
+ }
+ }
+ usteps_ull = UINT64_MAX;
+ }
+ assert(usteps_ull <= UINT64_MAX);
+ uint64_t usteps = (uint64_t)usteps_ull;
+
+ if (usteps == 0) {
+ return PyFloat_FromDouble(x);
+ }
+ if (Py_IS_NAN(x)) {
+ return PyFloat_FromDouble(x);
+ }
+ if (Py_IS_NAN(y)) {
+ return PyFloat_FromDouble(y);
+ }
+
+ // We assume that double and uint64_t have the same endianness.
+ // This is not guaranteed by the C-standard, but it is true for
+ // all platforms we care about. (The most likely form of violation
+ // would be a "mixed-endian" double.)
+ union pun {double f; uint64_t i;};
+ union pun ux = {x}, uy = {y};
+ if (ux.i == uy.i) {
+ return PyFloat_FromDouble(x);
+ }
+
+ const uint64_t sign_bit = 1ULL<<63;
+
+ uint64_t ax = ux.i & ~sign_bit;
+ uint64_t ay = uy.i & ~sign_bit;
+
+ // opposite signs
+ if (((ux.i ^ uy.i) & sign_bit)) {
+ // NOTE: ax + ay can never overflow, because their most significant bit
+ // ain't set.
+ if (ax + ay <= usteps) {
+ return PyFloat_FromDouble(uy.f);
+ // This comparison has to use <, because <= would get +0.0 vs -0.0
+ // wrong.
+ } else if (ax < usteps) {
+ union pun result = {.i = (uy.i & sign_bit) | (usteps - ax)};
+ return PyFloat_FromDouble(result.f);
+ } else {
+ ux.i -= usteps;
+ return PyFloat_FromDouble(ux.f);
+ }
+ // same sign
+ } else if (ax > ay) {
+ if (ax - ay >= usteps) {
+ ux.i -= usteps;
+ return PyFloat_FromDouble(ux.f);
+ } else {
+ return PyFloat_FromDouble(uy.f);
+ }
+ } else {
+ if (ay - ax >= usteps) {
+ ux.i += usteps;
+ return PyFloat_FromDouble(ux.f);
+ } else {
+ return PyFloat_FromDouble(uy.f);
+ }
+ }
}