Skip to content

gh-135853: add math.fmax and math.fmin #135888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
22 changes: 22 additions & 0 deletions Doc/library/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ noted otherwise, all return values are floats.
:func:`fabs(x) <fabs>` Absolute value of *x*
:func:`floor(x) <floor>` Floor of *x*, the largest integer less than or equal to *x*
:func:`fma(x, y, z) <fma>` Fused multiply-add operation: ``(x * y) + z``
:func:`fmax(x, y) <fmax>` Maximum of two floating-point values
:func:`fmin(x, y) <fmin>` Minimum of two floating-point values
:func:`fmod(x, y) <fmod>` Remainder of division ``x / y``
:func:`modf(x) <modf>` Fractional and integer parts of *x*
:func:`remainder(x, y) <remainder>` Remainder of *x* with respect to *y*
Expand Down Expand Up @@ -247,6 +249,26 @@ Floating point arithmetic
.. versionadded:: 3.13


.. function:: fmax(x, y, /)

Get the larger of two floating-point values, treating NaNs as missing data.

If *x* and *y* are NaNs of same sign *s*, return ``copysign(nan, s)``.
If *x* and *y* are NaNs of different sign, return ``copysign(nan, 1)``.

.. versionadded:: next


.. function:: fmin(x, y, /)

Get the smaller of two floating-point values, treating NaNs as missing data.

If *x* and *y* are NaNs of same sign *s*, return ``copysign(nan, s)``.
If *x* and *y* are NaNs of different sign, return ``copysign(nan, -1)``.

.. versionadded:: next


.. function:: fmod(x, y)

Return the floating-point remainder of ``x / y``,
Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.fmax` and :func:`math.fmin` functions.
(Contributed by Bénédikt Tran in :gh:`135853`.)


os.path
-------
Expand Down
108 changes: 105 additions & 3 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

eps = 1E-05
NAN = float('nan')
NNAN = float('-nan')
INF = float('inf')
NINF = float('-inf')
FLOAT_MAX = sys.float_info.max
Expand All @@ -37,6 +38,11 @@
test_file = os.path.join(test_dir, 'mathdata', 'cmath_testcases.txt')


def is_signed_nan(x, x0):
"""Check if x is a NaN with the same sign as x0."""
return math.isnan(x) and math.copysign(1, x) == math.copysign(1, x0)


def to_ulps(x):
"""Convert a non-NaN float x to an integer, in such a way that
adjacent floats are converted to adjacent integers. Then
Expand Down Expand Up @@ -253,9 +259,10 @@ def ftest(self, name, got, expected, ulp_tol=5, abs_tol=0.0):
non-finite floats, exact equality is demanded. Also, nan==nan
in this function.
"""
failure = result_check(expected, got, ulp_tol, abs_tol)
if failure is not None:
self.fail("{}: {}".format(name, failure))
with self.subTest(name):
failure = result_check(expected, got, ulp_tol, abs_tol)
if failure is not None:
self.fail(failure)

def testConstants(self):
# Ref: Abramowitz & Stegun (Dover, 1965)
Expand Down Expand Up @@ -623,6 +630,101 @@ def testFmod(self):
self.assertEqual(math.fmod(0.0, NINF), 0.0)
self.assertRaises(ValueError, math.fmod, INF, INF)

def test_fmax(self):
self.assertRaises(TypeError, math.fmax)
self.assertRaises(TypeError, math.fmax, 'x', 'y')

self.assertEqual(math.fmax(0., 0.), 0.)
self.assertEqual(math.fmax(0., -0.), 0.)
self.assertEqual(math.fmax(-0., 0.), 0.)

self.assertEqual(math.fmax(1., 0.), 1.)
self.assertEqual(math.fmax(0., 1.), 1.)
self.assertEqual(math.fmax(1., -0.), 1.)
self.assertEqual(math.fmax(-0., 1.), 1.)

self.assertEqual(math.fmax(-1., 0.), 0.)
self.assertEqual(math.fmax(0., -1.), 0.)
self.assertEqual(math.fmax(-1., -0.), -0.)
self.assertEqual(math.fmax(-0., -1.), -0.)

for x in [NINF, -1., -0., 0., 1., INF]:
self.assertFalse(math.isnan(x))

with self.subTest("math.fmax(INF, x)", x=x):
self.assertEqual(math.fmax(INF, x), INF)
with self.subTest("math.fmax(x, INF)", x=x):
self.assertEqual(math.fmax(x, INF), INF)

with self.subTest("math.fmax(NINF, x)", x=x):
self.assertEqual(math.fmax(NINF, x), x)
with self.subTest("math.fmax(x, NINF)", x=x):
self.assertEqual(math.fmax(x, NINF), x)

@requires_IEEE_754
def test_fmax_nans(self):
# When exactly one operand is NaN, the other is returned.
for x in [NINF, -1., -0., 0., 1., INF]:
with self.subTest(x=x):
self.assertFalse(math.isnan(math.fmax(NAN, x)))
self.assertFalse(math.isnan(math.fmax(x, NAN)))
self.assertFalse(math.isnan(math.fmax(NNAN, x)))
self.assertFalse(math.isnan(math.fmax(x, NNAN)))
# When operands are NaNs with identical sign, return this signed NaN.
self.assertTrue(is_signed_nan(math.fmax(NAN, NAN), 1))
self.assertTrue(is_signed_nan(math.fmax(NNAN, NNAN), -1))
# When operands are NaNs of different signs, return the positive NaN.
self.assertTrue(is_signed_nan(math.fmax(NAN, NNAN), 1))
self.assertTrue(is_signed_nan(math.fmax(NNAN, NAN), 1))

def test_fmin(self):
self.assertRaises(TypeError, math.fmin)
self.assertRaises(TypeError, math.fmin, 'x', 'y')

self.assertEqual(math.fmin(0., 0.), 0.)
self.assertEqual(math.fmin(0., -0.), -0.)
self.assertEqual(math.fmin(-0., 0.), -0.)

self.assertEqual(math.fmin(1., 0.), 0.)
self.assertEqual(math.fmin(0., 1.), 0.)
self.assertEqual(math.fmin(1., -0.), -0.)
self.assertEqual(math.fmin(-0., 1.), -0.)

self.assertEqual(math.fmin(-1., 0.), -1.)
self.assertEqual(math.fmin(0., -1.), -1.)
self.assertEqual(math.fmin(-1., -0.), -1.)
self.assertEqual(math.fmin(-0., -1.), -1.)

for x in [NINF, -1., -0., 0., 1., INF]:
self.assertFalse(math.isnan(x))

with self.subTest("math.fmin(INF, x)", x=x):
self.assertEqual(math.fmin(INF, x), x)
with self.subTest("math.fmin(x, INF)", x=x):
self.assertEqual(math.fmin(x, INF), x)

with self.subTest("math.fmin(NINF, x)", x=x):
self.assertEqual(math.fmin(NINF, x), NINF)
with self.subTest("math.fmin(x, NINF)", x=x):
self.assertEqual(math.fmin(x, NINF), NINF)

@requires_IEEE_754
def test_fmin_nans(self):
# When exactly one operand is NaN, the other is returned.
for x in [NINF, -1., -0., 0., 1., INF]:
with self.subTest(x=x):
self.assertFalse(math.isnan(x))
self.assertFalse(math.isnan(math.fmin(NAN, x)))
self.assertFalse(math.isnan(math.fmin(x, NAN)))
self.assertFalse(math.isnan(math.fmin(NNAN, x)))
self.assertFalse(math.isnan(math.fmin(x, NNAN)))
# When operands are NaNs with identical sign, return this signed NaN.
self.assertTrue(is_signed_nan(math.fmin(NAN, NAN), 1))
self.assertTrue(is_signed_nan(math.fmin(NNAN, NNAN), -1))
# When operands are NaNs of different signs, return the negative NaN.
self.assertTrue(is_signed_nan(math.fmin(NAN, NNAN), -1))
self.assertTrue(is_signed_nan(math.fmin(NNAN, NAN), -1))

def testFrexp(self):
self.assertRaises(TypeError, math.frexp)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`math.fmax` and :math:`math.fmin` to get the larger and smaller of
two floating-point values. Patch by Bénédikt Tran.
108 changes: 107 additions & 1 deletion Modules/clinic/mathmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,50 @@ math_floor(PyObject *module, PyObject *number)
return PyLong_FromDouble(floor(x));
}

/*[clinic input]
math.fmax -> double

x: double
y: double
/

Returns the larger of two floating-point arguments.

[clinic start generated code]*/

static double
math_fmax_impl(PyObject *module, double x, double y)
/*[clinic end generated code: output=00692358d312fee2 input=0dcf618bb27f98c7]*/
{
if (isnan(x) && isnan(y)) {
double s = copysign(1, x);
return s == copysign(1, y) ? copysign(NAN, s) : NAN;
}
return fmax(x, y);
}

/*[clinic input]
math.fmin -> double

x: double
y: double
/

Returns the smaller of two floating-point arguments.
[clinic start generated code]*/

static double
math_fmin_impl(PyObject *module, double x, double y)
/*[clinic end generated code: output=3d5b7826bd292dd9 input=f7b5c91de01d766f]*/
{
if (isnan(x) && isnan(y)) {
double s = copysign(1, x);
// return ±NAN if both are ±NAN and -NAN otherwise.
return copysign(NAN, s == copysign(1, y) ? s : -1);
}
return fmin(x, y);
}

FUNC1AD(gamma, m_tgamma,
"gamma($module, x, /)\n--\n\n"
"Gamma function at x.",
Expand Down Expand Up @@ -4175,7 +4219,9 @@ static PyMethodDef math_methods[] = {
MATH_FACTORIAL_METHODDEF
MATH_FLOOR_METHODDEF
MATH_FMA_METHODDEF
MATH_FMAX_METHODDEF
MATH_FMOD_METHODDEF
MATH_FMIN_METHODDEF
MATH_FREXP_METHODDEF
MATH_FSUM_METHODDEF
{"gamma", math_gamma, METH_O, math_gamma_doc},
Expand Down
Loading