From a82d2984b06b204ac398838eb63ce738ce87c003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:36:10 +0200 Subject: [PATCH 01/11] add math.fmax and math.fmin --- Doc/library/math.rst | 22 ++++ Doc/whatsnew/3.15.rst | 3 + Lib/test/test_math.py | 108 +++++++++++++++++- ...-06-24-13-30-47.gh-issue-135853.7ejTvK.rst | 2 + Modules/clinic/mathmodule.c.h | 108 +++++++++++++++++- Modules/mathmodule.c | 46 ++++++++ 6 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst diff --git a/Doc/library/math.rst b/Doc/library/math.rst index ecb1d4102cac31..e0a612397c8c85 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -42,6 +42,8 @@ noted otherwise, all return values are floats. :func:`fabs(x) ` Absolute value of *x* :func:`floor(x) ` Floor of *x*, the largest integer less than or equal to *x* :func:`fma(x, y, z) ` Fused multiply-add operation: ``(x * y) + z`` +:func:`fmax(x, y) ` Maximum of two floating-point values +:func:`fmin(x, y) ` Minimum of two floating-point values :func:`fmod(x, y) ` Remainder of division ``x / y`` :func:`modf(x) ` Fractional and integer parts of *x* :func:`remainder(x, y) ` Remainder of *x* with respect to *y* @@ -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``, diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..5cc3110fa74727 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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 ------- diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 384ad5c828d9b3..4ee6e442d45313 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -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 @@ -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 @@ -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) @@ -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) diff --git a/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst b/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst new file mode 100644 index 00000000000000..d5a57d56c6e802 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst @@ -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. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index fbb012fb6dd9e1..8c7a2d91dbd863 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -84,6 +84,112 @@ PyDoc_STRVAR(math_floor__doc__, #define MATH_FLOOR_METHODDEF \ {"floor", (PyCFunction)math_floor, METH_O, math_floor__doc__}, +PyDoc_STRVAR(math_fmax__doc__, +"fmax($module, x, y, /)\n" +"--\n" +"\n" +"Returns the larger of two floating-point arguments."); + +#define MATH_FMAX_METHODDEF \ + {"fmax", _PyCFunction_CAST(math_fmax), METH_FASTCALL, math_fmax__doc__}, + +static double +math_fmax_impl(PyObject *module, double x, double y); + +static PyObject * +math_fmax(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + double x; + double y; + double _return_value; + + if (!_PyArg_CheckPositional("fmax", nargs, 2, 2)) { + goto exit; + } + if (PyFloat_CheckExact(args[0])) { + x = PyFloat_AS_DOUBLE(args[0]); + } + else + { + x = PyFloat_AsDouble(args[0]); + if (x == -1.0 && PyErr_Occurred()) { + goto exit; + } + } + if (PyFloat_CheckExact(args[1])) { + y = PyFloat_AS_DOUBLE(args[1]); + } + else + { + y = PyFloat_AsDouble(args[1]); + if (y == -1.0 && PyErr_Occurred()) { + goto exit; + } + } + _return_value = math_fmax_impl(module, x, y); + if ((_return_value == -1.0) && PyErr_Occurred()) { + goto exit; + } + return_value = PyFloat_FromDouble(_return_value); + +exit: + return return_value; +} + +PyDoc_STRVAR(math_fmin__doc__, +"fmin($module, x, y, /)\n" +"--\n" +"\n" +"Returns the smaller of two floating-point arguments."); + +#define MATH_FMIN_METHODDEF \ + {"fmin", _PyCFunction_CAST(math_fmin), METH_FASTCALL, math_fmin__doc__}, + +static double +math_fmin_impl(PyObject *module, double x, double y); + +static PyObject * +math_fmin(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + double x; + double y; + double _return_value; + + if (!_PyArg_CheckPositional("fmin", nargs, 2, 2)) { + goto exit; + } + if (PyFloat_CheckExact(args[0])) { + x = PyFloat_AS_DOUBLE(args[0]); + } + else + { + x = PyFloat_AsDouble(args[0]); + if (x == -1.0 && PyErr_Occurred()) { + goto exit; + } + } + if (PyFloat_CheckExact(args[1])) { + y = PyFloat_AS_DOUBLE(args[1]); + } + else + { + y = PyFloat_AsDouble(args[1]); + if (y == -1.0 && PyErr_Occurred()) { + goto exit; + } + } + _return_value = math_fmin_impl(module, x, y); + if ((_return_value == -1.0) && PyErr_Occurred()) { + goto exit; + } + return_value = PyFloat_FromDouble(_return_value); + +exit: + return return_value; +} + PyDoc_STRVAR(math_fsum__doc__, "fsum($module, seq, /)\n" "--\n" @@ -1178,4 +1284,4 @@ math_ulp(PyObject *module, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=44bba3a0a052a364 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d5c3d9b9b47ad54e input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index bbbb49115681de..f6506060385ba2 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -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.", @@ -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}, From afb0a9195b51a8e8cc0427a20fe61396c1742c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:26:47 +0200 Subject: [PATCH 02/11] address review --- Doc/library/math.rst | 8 ++++---- Lib/test/test_math.py | 27 ++++++++++----------------- Modules/mathmodule.c | 12 +----------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index e0a612397c8c85..4de9fc5257ff8b 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -253,8 +253,8 @@ Floating point arithmetic 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)``. + When both operands are NaNs, return ``nan`` (the sign of the result is + implementation-defined). .. versionadded:: next @@ -263,8 +263,8 @@ Floating point arithmetic 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)``. + When both operands are NaNs, return ``nan`` (the sign of the result is + implementation-defined). .. versionadded:: next diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 4ee6e442d45313..70a3a70c28c903 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -38,11 +38,6 @@ 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 @@ -670,12 +665,11 @@ def test_fmax_nans(self): 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)) + # When both operands are NaNs, fmax() returns NaN (see C11, F.10.9.2). + self.assertTrue(math.isnan(math.fmax(NAN, NAN))) + self.assertTrue(math.isnan(math.fmax(NNAN, NNAN))) + self.assertTrue(math.isnan(math.fmax(NAN, NNAN))) + self.assertTrue(math.isnan(math.fmax(NNAN, NAN))) def test_fmin(self): self.assertRaises(TypeError, math.fmin) @@ -718,12 +712,11 @@ def test_fmin_nans(self): 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)) + # When both operands are NaNs, fmax() returns NaN (see C11, F.10.9.2). + self.assertTrue(math.isnan(math.fmin(NAN, NAN))) + self.assertTrue(math.isnan(math.fmin(NNAN, NNAN))) + self.assertTrue(math.isnan(math.fmin(NAN, NNAN))) + self.assertTrue(math.isnan(math.fmin(NNAN, NAN))) def testFrexp(self): self.assertRaises(TypeError, math.frexp) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index f6506060385ba2..30c0271dfdc36c 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1222,17 +1222,12 @@ math.fmax -> 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]*/ +/*[clinic end generated code: output=00692358d312fee2 input=e64ab9f40a60f4f1]*/ { - if (isnan(x) && isnan(y)) { - double s = copysign(1, x); - return s == copysign(1, y) ? copysign(NAN, s) : NAN; - } return fmax(x, y); } @@ -1250,11 +1245,6 @@ 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); } From 62597d6e21900e80234539afdcdb89886ab48701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:31:32 +0200 Subject: [PATCH 03/11] revert un-necessary change --- Lib/test/test_math.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 70a3a70c28c903..af77e69f0c6dab 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -254,10 +254,9 @@ 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. """ - with self.subTest(name): - failure = result_check(expected, got, ulp_tol, abs_tol) - if failure is not None: - self.fail(failure) + failure = result_check(expected, got, ulp_tol, abs_tol) + if failure is not None: + self.fail("{}: {}".format(name, failure)) def testConstants(self): # Ref: Abramowitz & Stegun (Dover, 1965) From a858aa12a726ec1e080a0e13844493d28734a7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:34:40 +0200 Subject: [PATCH 04/11] Update Lib/test/test_math.py --- Lib/test/test_math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index af77e69f0c6dab..5e0ffd623fbd01 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -711,7 +711,7 @@ def test_fmin_nans(self): 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 both operands are NaNs, fmax() returns NaN (see C11, F.10.9.2). + # When both operands are NaNs, fmin() returns NaN (see C11, F.10.9.3). self.assertTrue(math.isnan(math.fmin(NAN, NAN))) self.assertTrue(math.isnan(math.fmin(NNAN, NNAN))) self.assertTrue(math.isnan(math.fmin(NAN, NNAN))) From f23a33965651c80864465f5c398e7cefead345fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:05:31 +0200 Subject: [PATCH 05/11] Update Lib/test/test_math.py --- Lib/test/test_math.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 5e0ffd623fbd01..c94a5ad15033ff 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -706,7 +706,6 @@ 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))) From a61eca5c7f715fe7eaec662b1c2490087c383749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:24:07 +0200 Subject: [PATCH 06/11] address review and align tests with C11 standard --- Doc/library/math.rst | 12 ++++--- Lib/test/test_math.py | 32 ++++++++----------- ...-06-24-13-30-47.gh-issue-135853.7ejTvK.rst | 2 +- Modules/clinic/mathmodule.c.h | 6 ++-- Modules/mathmodule.c | 8 ++--- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index 4de9fc5257ff8b..4083638bb4c31a 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -253,8 +253,10 @@ Floating point arithmetic Get the larger of two floating-point values, treating NaNs as missing data. - When both operands are NaNs, return ``nan`` (the sign of the result is - implementation-defined). + When both operands are (signed) NaNs or zeroes, return ``nan`` and ``0`` + respectively and the sign of the result is implementation-defined, that + is, :func:`!fmax` is not required to be sensitive to the sign of such + operands (see ISO C11, Annexes F.10.0.3 and F.10.9.2). .. versionadded:: next @@ -263,8 +265,10 @@ Floating point arithmetic Get the smaller of two floating-point values, treating NaNs as missing data. - When both operands are NaNs, return ``nan`` (the sign of the result is - implementation-defined). + When both operands are (signed) NaNs or zeroes, return ``nan`` and ``0`` + respectively and the sign of the result is implementation-defined, that + is, :func:`!fmin` is not required to be sensitive to the sign of such + operands (see ISO C11, Annexes F.10.0.3 and F.10.9.3). .. versionadded:: next diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index c94a5ad15033ff..e6cd190961955e 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -629,8 +629,9 @@ def test_fmax(self): 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.) + # fmax() does not need to be sensitive to the sign of 0 (F.10.9.2.3). + self.assertIn(math.fmax(0., -0.), {-0., 0.}) + self.assertIn(math.fmax(-0., 0.), {-0., 0.}) self.assertEqual(math.fmax(1., 0.), 1.) self.assertEqual(math.fmax(0., 1.), 1.) @@ -645,26 +646,23 @@ def test_fmax(self): for x in [NINF, -1., -0., 0., 1., INF]: self.assertFalse(math.isnan(x)) - with self.subTest("math.fmax(INF, x)", x=x): + with self.subTest(x=x, is_negative=math.copysign(1, x) < 0): 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): + with self.subTest(x=x, is_negative=math.copysign(1, x) < 0): 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 both operands are NaNs, fmax() returns NaN (see C11, F.10.9.2). + # When both operands are NaNs, fmax() returns NaN (see C11, F.10.9.2) + # whose sign is implementation-defined (see C11, F.10.0.3). self.assertTrue(math.isnan(math.fmax(NAN, NAN))) self.assertTrue(math.isnan(math.fmax(NNAN, NNAN))) self.assertTrue(math.isnan(math.fmax(NAN, NNAN))) @@ -675,8 +673,9 @@ def test_fmin(self): 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.) + # fmin() does not need to be sensitive to the sign of 0 (F.10.9.3.1). + self.assertIn(math.fmin(0., -0.), {-0., 0.}) + self.assertIn(math.fmin(-0., 0.), {-0., 0.}) self.assertEqual(math.fmin(1., 0.), 0.) self.assertEqual(math.fmin(0., 1.), 0.) @@ -691,26 +690,23 @@ def test_fmin(self): for x in [NINF, -1., -0., 0., 1., INF]: self.assertFalse(math.isnan(x)) - with self.subTest("math.fmin(INF, x)", x=x): + with self.subTest(x=x, is_negative=math.copysign(1, x) < 0): 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): + with self.subTest(x=x, is_negative=math.copysign(1, x) < 0): 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 both operands are NaNs, fmin() returns NaN (see C11, F.10.9.3). + # When both operands are NaNs, fmin() returns NaN (see C11, F.10.9.2) + # whose sign is implementation-defined (see C11, F.10.0.3). self.assertTrue(math.isnan(math.fmin(NAN, NAN))) self.assertTrue(math.isnan(math.fmin(NNAN, NNAN))) self.assertTrue(math.isnan(math.fmin(NAN, NNAN))) diff --git a/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst b/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst index d5a57d56c6e802..240ea72c69fa6c 100644 --- a/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst +++ b/Misc/NEWS.d/next/Library/2025-06-24-13-30-47.gh-issue-135853.7ejTvK.rst @@ -1,2 +1,2 @@ -Add :func:`math.fmax` and :math:`math.fmin` to get the larger and smaller of +Add :func:`math.fmax` and :func:`math.fmin` to get the larger and smaller of two floating-point values. Patch by Bénédikt Tran. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 8c7a2d91dbd863..9a821b744d3414 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -88,7 +88,7 @@ PyDoc_STRVAR(math_fmax__doc__, "fmax($module, x, y, /)\n" "--\n" "\n" -"Returns the larger of two floating-point arguments."); +"Return the larger of two floating-point arguments."); #define MATH_FMAX_METHODDEF \ {"fmax", _PyCFunction_CAST(math_fmax), METH_FASTCALL, math_fmax__doc__}, @@ -141,7 +141,7 @@ PyDoc_STRVAR(math_fmin__doc__, "fmin($module, x, y, /)\n" "--\n" "\n" -"Returns the smaller of two floating-point arguments."); +"Return the smaller of two floating-point arguments."); #define MATH_FMIN_METHODDEF \ {"fmin", _PyCFunction_CAST(math_fmin), METH_FASTCALL, math_fmin__doc__}, @@ -1284,4 +1284,4 @@ math_ulp(PyObject *module, PyObject *arg) exit: return return_value; } -/*[clinic end generated code: output=d5c3d9b9b47ad54e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3e4fd119a2006b3a input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 30c0271dfdc36c..0ffe84b1052cf7 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1221,12 +1221,12 @@ math.fmax -> double y: double / -Returns the larger of two floating-point arguments. +Return 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=e64ab9f40a60f4f1]*/ +/*[clinic end generated code: output=00692358d312fee2 input=021596c027336ffe]*/ { return fmax(x, y); } @@ -1238,12 +1238,12 @@ math.fmin -> double y: double / -Returns the smaller of two floating-point arguments. +Return 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]*/ +/*[clinic end generated code: output=3d5b7826bd292dd9 input=d12e64ccc33f878a]*/ { return fmin(x, y); } From 92732bb47053d93e53c8aaf5af716e9279010d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:04:40 +0200 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Sergey B Kirpichev --- Doc/library/math.rst | 4 ++-- Lib/test/test_math.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index 4083638bb4c31a..90f2378e5976af 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -256,7 +256,7 @@ Floating point arithmetic When both operands are (signed) NaNs or zeroes, return ``nan`` and ``0`` respectively and the sign of the result is implementation-defined, that is, :func:`!fmax` is not required to be sensitive to the sign of such - operands (see ISO C11, Annexes F.10.0.3 and F.10.9.2). + operands (see Annex F of the C11 standard, §F.10.0.3 and §F.10.9.2). .. versionadded:: next @@ -268,7 +268,7 @@ Floating point arithmetic When both operands are (signed) NaNs or zeroes, return ``nan`` and ``0`` respectively and the sign of the result is implementation-defined, that is, :func:`!fmin` is not required to be sensitive to the sign of such - operands (see ISO C11, Annexes F.10.0.3 and F.10.9.3). + operands (see Annex F of the C11 standard, §F.10.0.3 and §F.10.9.3). .. versionadded:: next diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index e6cd190961955e..e71b4878a5817b 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -629,7 +629,7 @@ def test_fmax(self): self.assertRaises(TypeError, math.fmax, 'x', 'y') self.assertEqual(math.fmax(0., 0.), 0.) - # fmax() does not need to be sensitive to the sign of 0 (F.10.9.2.3). + # fmax() does not need to be sensitive to the sign of 0 (§F.10.9.2). self.assertIn(math.fmax(0., -0.), {-0., 0.}) self.assertIn(math.fmax(-0., 0.), {-0., 0.}) @@ -673,7 +673,7 @@ def test_fmin(self): self.assertRaises(TypeError, math.fmin, 'x', 'y') self.assertEqual(math.fmin(0., 0.), 0.) - # fmin() does not need to be sensitive to the sign of 0 (F.10.9.3.1). + # fmin() does not need to be sensitive to the sign of 0 (§F.10.9.3). self.assertIn(math.fmin(0., -0.), {-0., 0.}) self.assertIn(math.fmin(-0., 0.), {-0., 0.}) From e1bc27277a556e72c4c17ff552afd0bc0b7729d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:06:04 +0200 Subject: [PATCH 08/11] Update Lib/test/test_math.py --- Lib/test/test_math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index e71b4878a5817b..1c13ae8a3a46e2 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -705,7 +705,7 @@ def test_fmin_nans(self): 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 both operands are NaNs, fmin() returns NaN (see C11, F.10.9.2) + # When both operands are NaNs, fmin() returns NaN (see C11, F.10.9.3) # whose sign is implementation-defined (see C11, F.10.0.3). self.assertTrue(math.isnan(math.fmin(NAN, NAN))) self.assertTrue(math.isnan(math.fmin(NNAN, NNAN))) From 4e8277ff2aa8bff5d8a444b423d34f65743b7c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:19:31 +0200 Subject: [PATCH 09/11] improve test coverage --- Lib/test/test_math.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 1c13ae8a3a46e2..3e126eaf7397af 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -629,19 +629,22 @@ def test_fmax(self): self.assertRaises(TypeError, math.fmax, 'x', 'y') self.assertEqual(math.fmax(0., 0.), 0.) + self.assertEqual(math.fmax(1., 2.), 2.) + self.assertEqual(math.fmax(2., 1.), 2.) + # fmax() does not need to be sensitive to the sign of 0 (§F.10.9.2). - self.assertIn(math.fmax(0., -0.), {-0., 0.}) - self.assertIn(math.fmax(-0., 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.), 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.) + 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)) @@ -673,17 +676,20 @@ def test_fmin(self): self.assertRaises(TypeError, math.fmin, 'x', 'y') self.assertEqual(math.fmin(0., 0.), 0.) + self.assertEqual(math.fmin(1., 2.), 1.) + self.assertEqual(math.fmin(2., 1.), 1.) + # fmin() does not need to be sensitive to the sign of 0 (§F.10.9.3). - self.assertIn(math.fmin(0., -0.), {-0., 0.}) - self.assertIn(math.fmin(-0., 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.), 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.) self.assertEqual(math.fmin(-1., -0.), -1.) self.assertEqual(math.fmin(-0., -1.), -1.) From ba2e77c33d313ceda6272096c9d43a7fac657578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 27 Jun 2025 18:23:19 +0200 Subject: [PATCH 10/11] remove misleading test --- Lib/test/test_math.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 3e126eaf7397af..c5d7fa36913238 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -632,10 +632,6 @@ def test_fmax(self): self.assertEqual(math.fmax(1., 2.), 2.) self.assertEqual(math.fmax(2., 1.), 2.) - # fmax() does not need to be sensitive to the sign of 0 (§F.10.9.2). - 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.) @@ -679,10 +675,6 @@ def test_fmin(self): self.assertEqual(math.fmin(1., 2.), 1.) self.assertEqual(math.fmin(2., 1.), 1.) - # fmin() does not need to be sensitive to the sign of 0 (§F.10.9.3). - 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.) From c1742a68d070101a2303f7d55acab4922a3426c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:17:28 +0200 Subject: [PATCH 11/11] align signature style Co-authored-by: Sergey B Kirpichev --- Doc/library/math.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index 90f2378e5976af..24efdfaa1e3bc3 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -249,7 +249,7 @@ Floating point arithmetic .. versionadded:: 3.13 -.. function:: fmax(x, y, /) +.. function:: fmax(x, y) Get the larger of two floating-point values, treating NaNs as missing data. @@ -261,7 +261,7 @@ Floating point arithmetic .. versionadded:: next -.. function:: fmin(x, y, /) +.. function:: fmin(x, y) Get the smaller of two floating-point values, treating NaNs as missing data.