diff --git a/Doc/library/math.rst b/Doc/library/math.rst --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -110,6 +110,38 @@ .. versionadded:: 3.5 +.. function:: isclose(a, b, rel_tol=1e-09, abs_tol=0.0) + + Return ``True`` if the values *a* and *b* are close to each other and + ``False`` otherwise. + + Whether or not two values are considered close is determined according to + given absolute and relative tolerances. + + *rel_tol* is the relative tolerance -- it is the maximum allowed difference + between *a* and *b*, relative to the larger absolute value of *a* or *b*. + For example, to set a tolerance of 5%, pass ``rel_tol=0.05``. The default + tolerance is ``1e-9``, which assures that the two values are the same within + about 9 decimal digits. *rel_tol* must be greater than zero. + + *abs_tol* is the minimum absolute tolerance -- useful for comparisons near + zero. *abs_tol* must be at least zero. + + If no errors occur, the result will be: + ``abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )``. + + The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be + handled according to IEEE rules. Specifically, ``NaN`` is not considered + close to any other value, including ``NaN``. ``inf`` and ``-inf`` are only + considered close to themselves. + + .. versionadded:: 3.5 + + .. seealso:: + + :pep:`485` -- A Function for testing approximate equality + + .. function:: isfinite(x) Return ``True`` if *x* is neither an infinity nor a NaN, and diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -281,6 +281,23 @@ :pep:`488` -- Multi-phase extension module initialization +PEP 485: A Function for testing approximate equality +---------------------------------------------------- + +:pep:`485` adds the :func:`math.isclose` function which tells whether two +values are approximately equal or "close" to each other. Whether or not two +values are considered close is determined according to given absolute and +relative tolerances. + +The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be handled +according to IEEE rules. Specifically, ``NaN`` is not considered close to any +other value, including ``NaN``. ``inf`` and ``-inf`` are only considered close +to themselves. + +.. seealso:: + + :pep:`485` -- A Function for testing approximate equality + Other Language Changes ====================== @@ -574,6 +591,9 @@ * :data:`math.inf` and :data:`math.nan` constants added. (Contributed by Mark Dickinson in :issue:`23185`.) +* :func:`math.isclose` function added. + (:pep:`485` written and code contributed by Chris Barker. + See also :issue:`24270`.) shutil ------ diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -1045,6 +1045,107 @@ self.assertEqual(math.inf, float("inf")) self.assertEqual(-math.inf, float("-inf")) + def testIsclose(self): + # a few helper functions + def do_close(a, b, *args, **kwargs): + self.assertTrue(math.isclose(a, b, *args, **kwargs), + msg="%s and %s should be close!" % (a, b)) + + def do_not_close(a, b, *args, **kwargs): + self.assertFalse(math.isclose(a, b, *args, **kwargs), + msg="%s and %s should not be close!" % (a, b)) + + def do_close_all(examples, *args, **kwargs): + for a, b in examples: + do_close(a, b, *args, **kwargs) + + def do_not_close_all(examples, *args, **kwargs): + for a, b in examples: + do_not_close(a, b, *args, **kwargs) + + # test error cases: + # ValueError should be raised if either tolerance is less than zero + with self.assertRaises(ValueError): + math.isclose(1, 1, -1e-100) + with self.assertRaises(ValueError): + math.isclose(1, 1, 1e-100, -1e10) + + # identical values must test as close + identical_examples = [(2.0, 2.0), + (0.1e200, 0.1e200), + (1.123e-300, 1.123e-300), + (12345, 12345.0), + (0.0, -0.0), + (345678, 345678)] + do_close_all(identical_examples, rel_tol=0.0, abs_tol=0.0) + + # examples that are close to 1e-8, but not 1e-9 + nums8 = [(1e8, 1e8 + 1), + (-1e-8, -1.000000009e-8), + (1.12345678, 1.12345679)] + do_close_all(nums8, rel_tol=1e-8) + do_not_close_all(nums8, rel_tol=1e-9) + + # values close to zero + nums0 = [(1e-9, 0.0), + (-1e-9, 0.0), + (-1e-150, 0.0)] + # these should not be close to any rel_tol + do_not_close_all(nums0, rel_tol=0.9) + # these should be close to abs_tol=1e-8 + do_close_all(nums0, abs_tol=1e-8) + + # these are close regardless of tolerance -- i.e. they are equal + do_close_all([(INF, INF), (NINF, NINF)], abs_tol=0.0) + + # these should never be close (following IEEE 754 rules for equality) + not_close_examples = [(NAN, NAN), + (NAN, 1e-100), + (1e-100, NAN), + (INF, NAN), + (NAN, INF), + (INF, NINF), + (INF, 1.0), + (1.0, INF), + (INF, 1e308), + (1e308, INF)] + # use largest reasonable tolerance + do_not_close_all(not_close_examples, abs_tol=0.999999999999999) + + # test with zero tolerance + zero_tolerance_close_examples = [(1.0, 1.0), + (-3.4, -3.4), + (-1e-300, -1e-300)] + do_close_all(zero_tolerance_close_examples, rel_tol=0.0) + zero_tolerance_not_close_examples = [(1.0, 1.000000000000001), + (0.99999999999999, 1.0), + (1.0e200, .999999999999999e200)] + do_not_close_all(zero_tolerance_not_close_examples, rel_tol=0.0) + + # test the assymetry example from PEP 485 + do_close_all([(9, 10), (10, 9)], rel_tol=0.1) + + # # test with integer values + integer_examples = [(100000001, 100000000), + (123456789, 123456788)] + do_close_all(integer_examples, rel_tol=1e-8) + do_not_close_all(integer_examples, rel_tol=1e-9) + + # test with Decimal values + from decimal import Decimal + decimal_examples = [(Decimal('1.00000001'), Decimal('1.0')), + (Decimal('1.00000001e-20'), Decimal('1.0e-20')), + (Decimal('1.00000001e-100'), Decimal('1.0e-100'))] + do_close_all(decimal_examples, rel_tol=1e-8) + do_not_close_all(decimal_examples, rel_tol=1e-9) + + # test with Fraction values + from fractions import Fraction + # could use some more examples here! + fraction_examples = [(Fraction(1, 100000000) + 1, Fraction(1))] + do_close_all(fraction_examples, rel_tol=1e-8) + do_not_close_all(fraction_examples, rel_tol=1e-9) + # RED_FLAG 16-Oct-2000 Tim # While 2.0 is more consistent about exceptions than previous releases, it # still fails this part of the test on some platforms. For now, we only diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -244,6 +244,9 @@ - Issue #23898: Fix inspect.classify_class_attrs() to support attributes with overloaded __eq__ and __bool__. Patch by Mike Bayer. +- Issue #24270: Add math.isclose() function as per PEP 485. + Original code by Chris Barker. + IDLE ---- diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -1990,6 +1990,86 @@ "isinf(x) -> bool\n\n\ Return True if x is a positive or negative infinity, and False otherwise."); +/* isclose, as proposed in PEP 485: A Function for testing approximate equality +*/ + +static PyObject * +math_isclose(PyObject *self, PyObject *args, PyObject *kwargs) +{ + double a, b; + double rel_tol = 1e-9; + double abs_tol = 0.0; + double diff = 0.0; + long result = 0; + + static char *keywords[] = {"a", "b", "rel_tol", "abs_tol", NULL}; + + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "dd|dd:isclose", + keywords, + &a, &b, &rel_tol, &abs_tol + )) + return NULL; + + /* sanity check on the inputs */ + if (rel_tol < 0.0 || abs_tol < 0.0 ) { + PyErr_SetString(PyExc_ValueError, + "tolerances must be non-negative"); + return NULL; + } + + if ( a == b ) { + /* short circuit exact equality -- needed to catch two infinities of + the same sign. And perhaps speeds things up a bit sometimes. + */ + Py_RETURN_TRUE; + } + + /* This catches the case of two infinities of opposite sign, or + one infinity and one finite number. Two infinities of opposite + sign would otherwise have an infinite relative tolerance. + Two infinities of the same sign are caught by the equality check + above. + */ + + if (Py_IS_INFINITY(a) || Py_IS_INFINITY(b)) { + Py_RETURN_FALSE; + } + + /* now do the regular computation + this is essentially the "weak" test from the Boost library + */ + + diff = fabs(b - a); + + result = (((diff <= fabs(rel_tol * b)) || + (diff <= fabs(rel_tol * a))) || + (diff <= abs_tol)); + + return PyBool_FromLong(result); +} + +PyDoc_STRVAR(math_isclose_doc, +"is_close(a, b, rel_tol=1e-09, abs_tol=0.0) -> bool\n" +"\n" +"Determine whether two floating point numbers are close in value.\n" +"\n" +":param a: one of the values to be tested\n" +":param b: the other value to be tested\n" +":param rel_tol=1e-9: The relative tolerance -- the amount of error\n" +" allowed, relative to the magnitude of the input\n" +" values.\n" +":param abs_tol=0.0: The minimum absolute tolerance level -- useful for\n" +" comparisons to zero.\n" +"\n" +"Returns True if a is close in value to b, and False otherwise.\n" +"\n" +"-inf, inf and NaN behave similarly to the IEEE 754 Standard. That\n" +"is, NaN is not close to anything, even itself. inf and -inf are\n" +"only close to themselves.\n" +"\n" +"See PEP 485 for a detailed description."); + static PyMethodDef math_methods[] = { {"acos", math_acos, METH_O, math_acos_doc}, {"acosh", math_acosh, METH_O, math_acosh_doc}, @@ -2016,6 +2096,8 @@ {"gamma", math_gamma, METH_O, math_gamma_doc}, {"gcd", math_gcd, METH_VARARGS, math_gcd_doc}, {"hypot", math_hypot, METH_VARARGS, math_hypot_doc}, + {"isclose", (PyCFunction) math_isclose, METH_VARARGS | METH_KEYWORDS, + math_isclose_doc}, {"isfinite", math_isfinite, METH_O, math_isfinite_doc}, {"isinf", math_isinf, METH_O, math_isinf_doc}, {"isnan", math_isnan, METH_O, math_isnan_doc},