Skip to content
Merged
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