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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy