summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTal Einat <taleinat@gmail.com>2015-05-31 19:05:00 (GMT)
committerTal Einat <taleinat@gmail.com>2015-05-31 19:05:00 (GMT)
commitd5519ed7f4889060363673ec802177250299920e (patch)
tree90bf7cc72a340c9512bcf7b4d0837ac845347c6a
parent439c5fe3ae62741f01da7e78a9c198375e837857 (diff)
downloadcpython-d5519ed7f4889060363673ec802177250299920e.zip
cpython-d5519ed7f4889060363673ec802177250299920e.tar.gz
cpython-d5519ed7f4889060363673ec802177250299920e.tar.bz2
Issue #19543: Implementation of isclose as per PEP 485
For details, see: PEP 0485 -- A Function for testing approximate equality Functions added: math.isclose() and cmath.isclose(). Original code by Chris Barker. Patch by Tal Einat.
-rw-r--r--Doc/library/cmath.rst32
-rw-r--r--Doc/library/math.rst32
-rw-r--r--Doc/whatsnew/3.5.rst21
-rw-r--r--Lib/test/test_cmath.py42
-rw-r--r--Lib/test/test_math.py121
-rw-r--r--Misc/NEWS3
-rw-r--r--Modules/clinic/cmathmodule.c.h53
-rw-r--r--Modules/cmathmodule.c68
-rw-r--r--Modules/mathmodule.c79
9 files changed, 450 insertions, 1 deletions
diff --git a/Doc/library/cmath.rst b/Doc/library/cmath.rst
index a981d94..ab619a0 100644
--- a/Doc/library/cmath.rst
+++ b/Doc/library/cmath.rst
@@ -207,6 +207,38 @@ Classification functions
and ``False`` otherwise.
+.. 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-09``, 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
+
+
Constants
---------
diff --git a/Doc/library/math.rst b/Doc/library/math.rst
index a88d1ac..244663e 100644
--- a/Doc/library/math.rst
+++ b/Doc/library/math.rst
@@ -110,6 +110,38 @@ Number-theoretic and representation functions
.. 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-09``, 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
index ee0e5d1..085ade7 100644
--- a/Doc/whatsnew/3.5.rst
+++ b/Doc/whatsnew/3.5.rst
@@ -285,6 +285,18 @@ rather than being restricted to ASCII.
:pep:`488` -- Multi-phase extension module initialization
+PEP 485: A function for testing approximate equality
+----------------------------------------------------
+
+:pep:`485` adds the :func:`math.isclose` and :func:`cmath.isclose`
+functions which tell 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.
+
+.. seealso::
+
+ :pep:`485` -- A function for testing approximate equality
+
Other Language Changes
======================
@@ -346,6 +358,13 @@ cgi
* :class:`~cgi.FieldStorage` now supports the context management protocol.
(Contributed by Berker Peksag in :issue:`20289`.)
+cmath
+-----
+
+* :func:`cmath.isclose` function added.
+ (Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
+
+
code
----
@@ -578,6 +597,8 @@ math
* :data:`math.inf` and :data:`math.nan` constants added. (Contributed by Mark
Dickinson in :issue:`23185`.)
+* :func:`math.isclose` function added.
+ (Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
shutil
------
diff --git a/Lib/test/test_cmath.py b/Lib/test/test_cmath.py
index 78ec85a..25ab7c1 100644
--- a/Lib/test/test_cmath.py
+++ b/Lib/test/test_cmath.py
@@ -1,5 +1,6 @@
from test.support import requires_IEEE_754
from test.test_math import parse_testfile, test_file
+import test.test_math as test_math
import unittest
import cmath, math
from cmath import phase, polar, rect, pi
@@ -529,5 +530,46 @@ class CMathTests(unittest.TestCase):
self.assertComplexIdentical(cmath.atanh(z), z)
+class IsCloseTests(test_math.IsCloseTests):
+ isclose = cmath.isclose
+
+ def test_reject_complex_tolerances(self):
+ with self.assertRaises(TypeError):
+ self.isclose(1j, 1j, rel_tol=1j)
+
+ with self.assertRaises(TypeError):
+ self.isclose(1j, 1j, abs_tol=1j)
+
+ with self.assertRaises(TypeError):
+ self.isclose(1j, 1j, rel_tol=1j, abs_tol=1j)
+
+ def test_complex_values(self):
+ # test complex values that are close to within 12 decimal places
+ complex_examples = [(1.0+1.0j, 1.000000000001+1.0j),
+ (1.0+1.0j, 1.0+1.000000000001j),
+ (-1.0+1.0j, -1.000000000001+1.0j),
+ (1.0-1.0j, 1.0-0.999999999999j),
+ ]
+
+ self.assertAllClose(complex_examples, rel_tol=1e-12)
+ self.assertAllNotClose(complex_examples, rel_tol=1e-13)
+
+ def test_complex_near_zero(self):
+ # test values near zero that are near to within three decimal places
+ near_zero_examples = [(0.001j, 0),
+ (0.001, 0),
+ (0.001+0.001j, 0),
+ (-0.001+0.001j, 0),
+ (0.001-0.001j, 0),
+ (-0.001-0.001j, 0),
+ ]
+
+ self.assertAllClose(near_zero_examples, abs_tol=1.5e-03)
+ self.assertAllNotClose(near_zero_examples, abs_tol=0.5e-03)
+
+ self.assertIsClose(0.001-0.001j, 0.001+0.001j, abs_tol=2e-03)
+ self.assertIsNotClose(0.001-0.001j, 0.001+0.001j, abs_tol=1e-03)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py
index fcd78d5..6c7b99d 100644
--- a/Lib/test/test_math.py
+++ b/Lib/test/test_math.py
@@ -1166,10 +1166,131 @@ class MathTests(unittest.TestCase):
'\n '.join(failures))
+class IsCloseTests(unittest.TestCase):
+ isclose = math.isclose # sublcasses should override this
+
+ def assertIsClose(self, a, b, *args, **kwargs):
+ self.assertTrue(self.isclose(a, b, *args, **kwargs),
+ msg="%s and %s should be close!" % (a, b))
+
+ def assertIsNotClose(self, a, b, *args, **kwargs):
+ self.assertFalse(self.isclose(a, b, *args, **kwargs),
+ msg="%s and %s should not be close!" % (a, b))
+
+ def assertAllClose(self, examples, *args, **kwargs):
+ for a, b in examples:
+ self.assertIsClose(a, b, *args, **kwargs)
+
+ def assertAllNotClose(self, examples, *args, **kwargs):
+ for a, b in examples:
+ self.assertIsNotClose(a, b, *args, **kwargs)
+
+ def test_negative_tolerances(self):
+ # ValueError should be raised if either tolerance is less than zero
+ with self.assertRaises(ValueError):
+ self.assertIsClose(1, 1, rel_tol=-1e-100)
+ with self.assertRaises(ValueError):
+ self.assertIsClose(1, 1, rel_tol=1e-100, abs_tol=-1e10)
+
+ def test_identical(self):
+ # 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)]
+ self.assertAllClose(identical_examples, rel_tol=0.0, abs_tol=0.0)
+
+ def test_eight_decimal_places(self):
+ # examples that are close to 1e-8, but not 1e-9
+ eight_decimal_places_examples = [(1e8, 1e8 + 1),
+ (-1e-8, -1.000000009e-8),
+ (1.12345678, 1.12345679)]
+ self.assertAllClose(eight_decimal_places_examples, rel_tol=1e-8)
+ self.assertAllNotClose(eight_decimal_places_examples, rel_tol=1e-9)
+
+ def test_near_zero(self):
+ # values close to zero
+ near_zero_examples = [(1e-9, 0.0),
+ (-1e-9, 0.0),
+ (-1e-150, 0.0)]
+ # these should not be close to any rel_tol
+ self.assertAllNotClose(near_zero_examples, rel_tol=0.9)
+ # these should be close to abs_tol=1e-8
+ self.assertAllClose(near_zero_examples, abs_tol=1e-8)
+
+ def test_identical_infinite(self):
+ # these are close regardless of tolerance -- i.e. they are equal
+ self.assertIsClose(INF, INF)
+ self.assertIsClose(INF, INF, abs_tol=0.0)
+ self.assertIsClose(NINF, NINF)
+ self.assertIsClose(NINF, NINF, abs_tol=0.0)
+
+ def test_inf_ninf_nan(self):
+ # 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
+ self.assertAllNotClose(not_close_examples, abs_tol=0.999999999999999)
+
+ def test_zero_tolerance(self):
+ # test with zero tolerance
+ zero_tolerance_close_examples = [(1.0, 1.0),
+ (-3.4, -3.4),
+ (-1e-300, -1e-300)]
+ self.assertAllClose(zero_tolerance_close_examples, rel_tol=0.0)
+
+ zero_tolerance_not_close_examples = [(1.0, 1.000000000000001),
+ (0.99999999999999, 1.0),
+ (1.0e200, .999999999999999e200)]
+ self.assertAllNotClose(zero_tolerance_not_close_examples, rel_tol=0.0)
+
+ def test_assymetry(self):
+ # test the assymetry example from PEP 485
+ self.assertAllClose([(9, 10), (10, 9)], rel_tol=0.1)
+
+ def test_integers(self):
+ # test with integer values
+ integer_examples = [(100000001, 100000000),
+ (123456789, 123456788)]
+
+ self.assertAllClose(integer_examples, rel_tol=1e-8)
+ self.assertAllNotClose(integer_examples, rel_tol=1e-9)
+
+ def test_decimals(self):
+ # 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'))]
+ self.assertAllClose(decimal_examples, rel_tol=1e-8)
+ self.assertAllNotClose(decimal_examples, rel_tol=1e-9)
+
+ def test_fractions(self):
+ # test with Fraction values
+ from fractions import Fraction
+
+ # could use some more examples here!
+ fraction_examples = [(Fraction(1, 100000000) + 1, Fraction(1))]
+ self.assertAllClose(fraction_examples, rel_tol=1e-8)
+ self.assertAllNotClose(fraction_examples, rel_tol=1e-9)
+
+
def test_main():
from doctest import DocFileSuite
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MathTests))
+ suite.addTest(unittest.makeSuite(IsCloseTests))
suite.addTest(DocFileSuite("ieee754.txt"))
run_unittest(suite)
diff --git a/Misc/NEWS b/Misc/NEWS
index b56cb49..aa3eaae 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -273,6 +273,9 @@ Library
- Issue #24298: Fix inspect.signature() to correctly unwrap wrappers
around bound methods.
+- Issue #24270: Add math.isclose() and cmath.isclose() functions as per PEP 485.
+ Contributed by Chris Barker and Tal Einat.
+
IDLE
----
diff --git a/Modules/clinic/cmathmodule.c.h b/Modules/clinic/cmathmodule.c.h
index e8fa6cb..7d61649 100644
--- a/Modules/clinic/cmathmodule.c.h
+++ b/Modules/clinic/cmathmodule.c.h
@@ -806,4 +806,55 @@ cmath_isinf(PyModuleDef *module, PyObject *arg)
exit:
return return_value;
}
-/*[clinic end generated code: output=274f59792cf4f418 input=a9049054013a1b77]*/
+
+PyDoc_STRVAR(cmath_isclose__doc__,
+"isclose($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)\n"
+"--\n"
+"\n"
+"Determine whether two complex numbers are close in value.\n"
+"\n"
+" rel_tol\n"
+" maximum difference for being considered \"close\", relative to the\n"
+" magnitude of the input values\n"
+" abs_tol\n"
+" maximum difference for being considered \"close\", regardless of the\n"
+" magnitude of the input values\n"
+"\n"
+"Return True if a is close in value to b, and False otherwise.\n"
+"\n"
+"For the values to be considered close, the difference between them must be\n"
+"smaller than at least one of the tolerances.\n"
+"\n"
+"-inf, inf and NaN behave similarly to the IEEE 754 Standard. That is, NaN is\n"
+"not close to anything, even itself. inf and -inf are only close to themselves.");
+
+#define CMATH_ISCLOSE_METHODDEF \
+ {"isclose", (PyCFunction)cmath_isclose, METH_VARARGS|METH_KEYWORDS, cmath_isclose__doc__},
+
+static int
+cmath_isclose_impl(PyModuleDef *module, Py_complex a, Py_complex b,
+ double rel_tol, double abs_tol);
+
+static PyObject *
+cmath_isclose(PyModuleDef *module, PyObject *args, PyObject *kwargs)
+{
+ PyObject *return_value = NULL;
+ static char *_keywords[] = {"a", "b", "rel_tol", "abs_tol", NULL};
+ Py_complex a;
+ Py_complex b;
+ double rel_tol = 1e-09;
+ double abs_tol = 0.0;
+ int _return_value;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "DD|$dd:isclose", _keywords,
+ &a, &b, &rel_tol, &abs_tol))
+ goto exit;
+ _return_value = cmath_isclose_impl(module, a, b, rel_tol, abs_tol);
+ if ((_return_value == -1) && PyErr_Occurred())
+ goto exit;
+ return_value = PyBool_FromLong((long)_return_value);
+
+exit:
+ return return_value;
+}
+/*[clinic end generated code: output=229e9c48c9d27362 input=a9049054013a1b77]*/
diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c
index 921eaaa..d12e4c5 100644
--- a/Modules/cmathmodule.c
+++ b/Modules/cmathmodule.c
@@ -1114,6 +1114,73 @@ cmath_isinf_impl(PyModuleDef *module, Py_complex z)
Py_IS_INFINITY(z.imag));
}
+/*[clinic input]
+cmath.isclose -> bool
+
+ a: Py_complex
+ b: Py_complex
+ *
+ rel_tol: double = 1e-09
+ maximum difference for being considered "close", relative to the
+ magnitude of the input values
+ abs_tol: double = 0.0
+ maximum difference for being considered "close", regardless of the
+ magnitude of the input values
+
+Determine whether two complex numbers are close in value.
+
+Return True if a is close in value to b, and False otherwise.
+
+For the values to be considered close, the difference between them must be
+smaller than at least one of the tolerances.
+
+-inf, inf and NaN behave similarly to the IEEE 754 Standard. That is, NaN is
+not close to anything, even itself. inf and -inf are only close to themselves.
+[clinic start generated code]*/
+
+static int
+cmath_isclose_impl(PyModuleDef *module, Py_complex a, Py_complex b,
+ double rel_tol, double abs_tol)
+/*[clinic end generated code: output=da0c535fb54e2310 input=df9636d7de1d4ac3]*/
+{
+ double diff;
+
+ /* sanity check on the inputs */
+ if (rel_tol < 0.0 || abs_tol < 0.0 ) {
+ PyErr_SetString(PyExc_ValueError,
+ "tolerances must be non-negative");
+ return -1;
+ }
+
+ if ( (a.real == b.real) && (a.imag == b.imag) ) {
+ /* short circuit exact equality -- needed to catch two infinities of
+ the same sign. And perhaps speeds things up a bit sometimes.
+ */
+ return 1;
+ }
+
+ /* 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.real) || Py_IS_INFINITY(a.imag) ||
+ Py_IS_INFINITY(b.real) || Py_IS_INFINITY(b.imag)) {
+ return 0;
+ }
+
+ /* now do the regular computation
+ this is essentially the "weak" test from the Boost library
+ */
+
+ diff = _Py_c_abs(_Py_c_diff(a, b));
+
+ return (((diff <= rel_tol * _Py_c_abs(b)) ||
+ (diff <= rel_tol * _Py_c_abs(a))) ||
+ (diff <= abs_tol));
+}
PyDoc_STRVAR(module_doc,
"This module is always available. It provides access to mathematical\n"
@@ -1129,6 +1196,7 @@ static PyMethodDef cmath_methods[] = {
CMATH_COS_METHODDEF
CMATH_COSH_METHODDEF
CMATH_EXP_METHODDEF
+ CMATH_ISCLOSE_METHODDEF
CMATH_ISFINITE_METHODDEF
CMATH_ISINF_METHODDEF
CMATH_ISNAN_METHODDEF
diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c
index a65de47..9359eb2 100644
--- a/Modules/mathmodule.c
+++ b/Modules/mathmodule.c
@@ -1990,6 +1990,83 @@ PyDoc_STRVAR(math_isinf_doc,
"isinf(x) -> bool\n\n\
Return True if x is a positive or negative infinity, and False otherwise.");
+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"
+" rel_tol\n"
+" maximum difference for being considered \"close\", relative to the\n"
+" magnitude of the input values\n"
+" abs_tol\n"
+" maximum difference for being considered \"close\", regardless of the\n"
+" magnitude of the input values\n"
+"\n"
+"Return True if a is close in value to b, and False otherwise.\n"
+"\n"
+"For the values to be considered close, the difference between them\n"
+"must be smaller than at least one of the tolerances.\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.");
+
static PyMethodDef math_methods[] = {
{"acos", math_acos, METH_O, math_acos_doc},
{"acosh", math_acosh, METH_O, math_acosh_doc},
@@ -2016,6 +2093,8 @@ static PyMethodDef math_methods[] = {
{"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},