Skip to content

Commit 6e39fa1

Browse files
gh-94906: Support multiple steps in math.nextafter (#103881)
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>
1 parent c3f43bf commit 6e39fa1

File tree

10 files changed

+223
-18
lines changed

10 files changed

+223
-18
lines changed

Doc/library/math.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,11 @@ Number-theoretic and representation functions
224224
of *x* and are floats.
225225

226226

227-
.. function:: nextafter(x, y)
227+
.. function:: nextafter(x, y, steps=1)
228228

229-
Return the next floating-point value after *x* towards *y*.
229+
Return the floating-point value *steps* steps after *x* towards *y*.
230230

231-
If *x* is equal to *y*, return *y*.
231+
If *x* is equal to *y*, return *y*, unless *steps* is zero.
232232

233233
Examples:
234234

@@ -239,6 +239,9 @@ Number-theoretic and representation functions
239239

240240
See also :func:`math.ulp`.
241241

242+
.. versionchanged:: 3.12
243+
Added the *steps* argument.
244+
242245
.. versionadded:: 3.9
243246

244247
.. function:: perm(n, k=None)

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,7 @@ struct _Py_global_strings {
681681
STRUCT_FOR_ID(stdin)
682682
STRUCT_FOR_ID(stdout)
683683
STRUCT_FOR_ID(step)
684+
STRUCT_FOR_ID(steps)
684685
STRUCT_FOR_ID(store_name)
685686
STRUCT_FOR_ID(strategy)
686687
STRUCT_FOR_ID(strftime)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_math.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,11 +2296,20 @@ def test_nextafter(self):
22962296
float.fromhex('0x1.fffffffffffffp-1'))
22972297
self.assertEqual(math.nextafter(1.0, INF),
22982298
float.fromhex('0x1.0000000000001p+0'))
2299+
self.assertEqual(math.nextafter(1.0, -INF, steps=1),
2300+
float.fromhex('0x1.fffffffffffffp-1'))
2301+
self.assertEqual(math.nextafter(1.0, INF, steps=1),
2302+
float.fromhex('0x1.0000000000001p+0'))
2303+
self.assertEqual(math.nextafter(1.0, -INF, steps=3),
2304+
float.fromhex('0x1.ffffffffffffdp-1'))
2305+
self.assertEqual(math.nextafter(1.0, INF, steps=3),
2306+
float.fromhex('0x1.0000000000003p+0'))
22992307

23002308
# x == y: y is returned
2301-
self.assertEqual(math.nextafter(2.0, 2.0), 2.0)
2302-
self.assertEqualSign(math.nextafter(-0.0, +0.0), +0.0)
2303-
self.assertEqualSign(math.nextafter(+0.0, -0.0), -0.0)
2309+
for steps in range(1, 5):
2310+
self.assertEqual(math.nextafter(2.0, 2.0, steps=steps), 2.0)
2311+
self.assertEqualSign(math.nextafter(-0.0, +0.0, steps=steps), +0.0)
2312+
self.assertEqualSign(math.nextafter(+0.0, -0.0, steps=steps), -0.0)
23042313

23052314
# around 0.0
23062315
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
@@ -2325,6 +2334,11 @@ def test_nextafter(self):
23252334
self.assertIsNaN(math.nextafter(1.0, NAN))
23262335
self.assertIsNaN(math.nextafter(NAN, NAN))
23272336

2337+
self.assertEqual(1.0, math.nextafter(1.0, INF, steps=0))
2338+
with self.assertRaises(ValueError):
2339+
math.nextafter(1.0, INF, steps=-1)
2340+
2341+
23282342
@requires_IEEE_754
23292343
def test_ulp(self):
23302344
self.assertEqual(math.ulp(1.0), sys.float_info.epsilon)

Lib/test/test_math_property.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import functools
2+
import unittest
3+
from math import isnan, nextafter
4+
from test.support import requires_IEEE_754
5+
from test.support.hypothesis_helper import hypothesis
6+
7+
floats = hypothesis.strategies.floats
8+
integers = hypothesis.strategies.integers
9+
10+
11+
def assert_equal_float(x, y):
12+
assert isnan(x) and isnan(y) or x == y
13+
14+
15+
def via_reduce(x, y, steps):
16+
return functools.reduce(nextafter, [y] * steps, x)
17+
18+
19+
class NextafterTests(unittest.TestCase):
20+
@requires_IEEE_754
21+
@hypothesis.given(
22+
x=floats(),
23+
y=floats(),
24+
steps=integers(min_value=0, max_value=2**16))
25+
def test_count(self, x, y, steps):
26+
assert_equal_float(via_reduce(x, y, steps),
27+
nextafter(x, y, steps=steps))
28+
29+
@requires_IEEE_754
30+
@hypothesis.given(
31+
x=floats(),
32+
y=floats(),
33+
a=integers(min_value=0),
34+
b=integers(min_value=0))
35+
def test_addition_commutes(self, x, y, a, b):
36+
first = nextafter(x, y, steps=a)
37+
second = nextafter(first, y, steps=b)
38+
combined = nextafter(x, y, steps=a+b)
39+
hypothesis.note(f"{first} -> {second} == {combined}")
40+
41+
assert_equal_float(second, combined)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support multiple steps in :func:`math.nextafter`. Patch by Shantanu Jain and Matthias Gorgens.

Modules/clinic/mathmodule.c.h

Lines changed: 47 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/mathmodule.c

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3864,13 +3864,20 @@ math.nextafter
38643864
x: double
38653865
y: double
38663866
/
3867+
*
3868+
steps: object = None
3869+
3870+
Return the floating-point value the given number of steps after x towards y.
3871+
3872+
If steps is not specified or is None, it defaults to 1.
38673873
3868-
Return the next floating-point value after x towards y.
3874+
Raises a TypeError, if x or y is not a double, or if steps is not an integer.
3875+
Raises ValueError if steps is negative.
38693876
[clinic start generated code]*/
38703877

38713878
static PyObject *
3872-
math_nextafter_impl(PyObject *module, double x, double y)
3873-
/*[clinic end generated code: output=750c8266c1c540ce input=02b2d50cd1d9f9b6]*/
3879+
math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps)
3880+
/*[clinic end generated code: output=cc6511f02afc099e input=7f2a5842112af2b4]*/
38743881
{
38753882
#if defined(_AIX)
38763883
if (x == y) {
@@ -3885,7 +3892,101 @@ math_nextafter_impl(PyObject *module, double x, double y)
38853892
return PyFloat_FromDouble(y);
38863893
}
38873894
#endif
3888-
return PyFloat_FromDouble(nextafter(x, y));
3895+
if (steps == Py_None) {
3896+
// fast path: we default to one step.
3897+
return PyFloat_FromDouble(nextafter(x, y));
3898+
}
3899+
steps = PyNumber_Index(steps);
3900+
if (steps == NULL) {
3901+
return NULL;
3902+
}
3903+
assert(PyLong_CheckExact(steps));
3904+
if (_PyLong_IsNegative((PyLongObject *)steps)) {
3905+
PyErr_SetString(PyExc_ValueError,
3906+
"steps must be a non-negative integer");
3907+
Py_DECREF(steps);
3908+
return NULL;
3909+
}
3910+
3911+
unsigned long long usteps_ull = PyLong_AsUnsignedLongLong(steps);
3912+
// Conveniently, uint64_t and double have the same number of bits
3913+
// on all the platforms we care about.
3914+
// So if an overflow occurs, we can just use UINT64_MAX.
3915+
Py_DECREF(steps);
3916+
if (usteps_ull >= UINT64_MAX) {
3917+
// This branch includes the case where an error occurred, since
3918+
// (unsigned long long)(-1) = ULLONG_MAX >= UINT64_MAX. Note that
3919+
// usteps_ull can be strictly larger than UINT64_MAX on a machine
3920+
// where unsigned long long has width > 64 bits.
3921+
if (PyErr_Occurred()) {
3922+
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
3923+
PyErr_Clear();
3924+
}
3925+
else {
3926+
return NULL;
3927+
}
3928+
}
3929+
usteps_ull = UINT64_MAX;
3930+
}
3931+
assert(usteps_ull <= UINT64_MAX);
3932+
uint64_t usteps = (uint64_t)usteps_ull;
3933+
3934+
if (usteps == 0) {
3935+
return PyFloat_FromDouble(x);
3936+
}
3937+
if (Py_IS_NAN(x)) {
3938+
return PyFloat_FromDouble(x);
3939+
}
3940+
if (Py_IS_NAN(y)) {
3941+
return PyFloat_FromDouble(y);
3942+
}
3943+
3944+
// We assume that double and uint64_t have the same endianness.
3945+
// This is not guaranteed by the C-standard, but it is true for
3946+
// all platforms we care about. (The most likely form of violation
3947+
// would be a "mixed-endian" double.)
3948+
union pun {double f; uint64_t i;};
3949+
union pun ux = {x}, uy = {y};
3950+
if (ux.i == uy.i) {
3951+
return PyFloat_FromDouble(x);
3952+
}
3953+
3954+
const uint64_t sign_bit = 1ULL<<63;
3955+
3956+
uint64_t ax = ux.i & ~sign_bit;
3957+
uint64_t ay = uy.i & ~sign_bit;
3958+
3959+
// opposite signs
3960+
if (((ux.i ^ uy.i) & sign_bit)) {
3961+
// NOTE: ax + ay can never overflow, because their most significant bit
3962+
// ain't set.
3963+
if (ax + ay <= usteps) {
3964+
return PyFloat_FromDouble(uy.f);
3965+
// This comparison has to use <, because <= would get +0.0 vs -0.0
3966+
// wrong.
3967+
} else if (ax < usteps) {
3968+
union pun result = {.i = (uy.i & sign_bit) | (usteps - ax)};
3969+
return PyFloat_FromDouble(result.f);
3970+
} else {
3971+
ux.i -= usteps;
3972+
return PyFloat_FromDouble(ux.f);
3973+
}
3974+
// same sign
3975+
} else if (ax > ay) {
3976+
if (ax - ay >= usteps) {
3977+
ux.i -= usteps;
3978+
return PyFloat_FromDouble(ux.f);
3979+
} else {
3980+
return PyFloat_FromDouble(uy.f);
3981+
}
3982+
} else {
3983+
if (ay - ax >= usteps) {
3984+
ux.i += usteps;
3985+
return PyFloat_FromDouble(ux.f);
3986+
} else {
3987+
return PyFloat_FromDouble(uy.f);
3988+
}
3989+
}
38893990
}
38903991

38913992

0 commit comments

Comments
 (0)