Skip to content

Commit fc3dbd2

Browse files
committed
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.
1 parent adfae5c commit fc3dbd2

9 files changed

Lines changed: 450 additions & 1 deletion

File tree

Doc/library/cmath.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,38 @@ Classification functions
207207
and ``False`` otherwise.
208208

209209

210+
.. function:: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
211+
212+
Return ``True`` if the values *a* and *b* are close to each other and
213+
``False`` otherwise.
214+
215+
Whether or not two values are considered close is determined according to
216+
given absolute and relative tolerances.
217+
218+
*rel_tol* is the relative tolerance -- it is the maximum allowed difference
219+
between *a* and *b*, relative to the larger absolute value of *a* or *b*.
220+
For example, to set a tolerance of 5%, pass ``rel_tol=0.05``. The default
221+
tolerance is ``1e-09``, which assures that the two values are the same
222+
within about 9 decimal digits. *rel_tol* must be greater than zero.
223+
224+
*abs_tol* is the minimum absolute tolerance -- useful for comparisons near
225+
zero. *abs_tol* must be at least zero.
226+
227+
If no errors occur, the result will be:
228+
``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
229+
230+
The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be
231+
handled according to IEEE rules. Specifically, ``NaN`` is not considered
232+
close to any other value, including ``NaN``. ``inf`` and ``-inf`` are only
233+
considered close to themselves.
234+
235+
.. versionadded:: 3.5
236+
237+
.. seealso::
238+
239+
:pep:`485` -- A function for testing approximate equality
240+
241+
210242
Constants
211243
---------
212244

Doc/library/math.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,38 @@ Number-theoretic and representation functions
110110
.. versionadded:: 3.5
111111

112112

113+
.. function:: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
114+
115+
Return ``True`` if the values *a* and *b* are close to each other and
116+
``False`` otherwise.
117+
118+
Whether or not two values are considered close is determined according to
119+
given absolute and relative tolerances.
120+
121+
*rel_tol* is the relative tolerance -- it is the maximum allowed difference
122+
between *a* and *b*, relative to the larger absolute value of *a* or *b*.
123+
For example, to set a tolerance of 5%, pass ``rel_tol=0.05``. The default
124+
tolerance is ``1e-09``, which assures that the two values are the same
125+
within about 9 decimal digits. *rel_tol* must be greater than zero.
126+
127+
*abs_tol* is the minimum absolute tolerance -- useful for comparisons near
128+
zero. *abs_tol* must be at least zero.
129+
130+
If no errors occur, the result will be:
131+
``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
132+
133+
The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be
134+
handled according to IEEE rules. Specifically, ``NaN`` is not considered
135+
close to any other value, including ``NaN``. ``inf`` and ``-inf`` are only
136+
considered close to themselves.
137+
138+
.. versionadded:: 3.5
139+
140+
.. seealso::
141+
142+
:pep:`485` -- A function for testing approximate equality
143+
144+
113145
.. function:: isfinite(x)
114146

115147
Return ``True`` if *x* is neither an infinity nor a NaN, and

Doc/whatsnew/3.5.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ rather than being restricted to ASCII.
285285

286286
:pep:`488` -- Multi-phase extension module initialization
287287

288+
PEP 485: A function for testing approximate equality
289+
----------------------------------------------------
290+
291+
:pep:`485` adds the :func:`math.isclose` and :func:`cmath.isclose`
292+
functions which tell whether two values are approximately equal or
293+
"close" to each other. Whether or not two values are considered
294+
close is determined according to given absolute and relative tolerances.
295+
296+
.. seealso::
297+
298+
:pep:`485` -- A function for testing approximate equality
299+
288300
Other Language Changes
289301
======================
290302

@@ -346,6 +358,13 @@ cgi
346358
* :class:`~cgi.FieldStorage` now supports the context management protocol.
347359
(Contributed by Berker Peksag in :issue:`20289`.)
348360

361+
cmath
362+
-----
363+
364+
* :func:`cmath.isclose` function added.
365+
(Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
366+
367+
349368
code
350369
----
351370

@@ -578,6 +597,8 @@ math
578597

579598
* :data:`math.inf` and :data:`math.nan` constants added. (Contributed by Mark
580599
Dickinson in :issue:`23185`.)
600+
* :func:`math.isclose` function added.
601+
(Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
581602

582603
shutil
583604
------

Lib/test/test_cmath.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from test.support import requires_IEEE_754
22
from test.test_math import parse_testfile, test_file
3+
import test.test_math as test_math
34
import unittest
45
import cmath, math
56
from cmath import phase, polar, rect, pi
@@ -529,5 +530,46 @@ def testAtanhSign(self):
529530
self.assertComplexIdentical(cmath.atanh(z), z)
530531

531532

533+
class IsCloseTests(test_math.IsCloseTests):
534+
isclose = cmath.isclose
535+
536+
def test_reject_complex_tolerances(self):
537+
with self.assertRaises(TypeError):
538+
self.isclose(1j, 1j, rel_tol=1j)
539+
540+
with self.assertRaises(TypeError):
541+
self.isclose(1j, 1j, abs_tol=1j)
542+
543+
with self.assertRaises(TypeError):
544+
self.isclose(1j, 1j, rel_tol=1j, abs_tol=1j)
545+
546+
def test_complex_values(self):
547+
# test complex values that are close to within 12 decimal places
548+
complex_examples = [(1.0+1.0j, 1.000000000001+1.0j),
549+
(1.0+1.0j, 1.0+1.000000000001j),
550+
(-1.0+1.0j, -1.000000000001+1.0j),
551+
(1.0-1.0j, 1.0-0.999999999999j),
552+
]
553+
554+
self.assertAllClose(complex_examples, rel_tol=1e-12)
555+
self.assertAllNotClose(complex_examples, rel_tol=1e-13)
556+
557+
def test_complex_near_zero(self):
558+
# test values near zero that are near to within three decimal places
559+
near_zero_examples = [(0.001j, 0),
560+
(0.001, 0),
561+
(0.001+0.001j, 0),
562+
(-0.001+0.001j, 0),
563+
(0.001-0.001j, 0),
564+
(-0.001-0.001j, 0),
565+
]
566+
567+
self.assertAllClose(near_zero_examples, abs_tol=1.5e-03)
568+
self.assertAllNotClose(near_zero_examples, abs_tol=0.5e-03)
569+
570+
self.assertIsClose(0.001-0.001j, 0.001+0.001j, abs_tol=2e-03)
571+
self.assertIsNotClose(0.001-0.001j, 0.001+0.001j, abs_tol=1e-03)
572+
573+
532574
if __name__ == "__main__":
533575
unittest.main()

Lib/test/test_math.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,10 +1166,131 @@ def test_mtestfile(self):
11661166
'\n '.join(failures))
11671167

11681168

1169+
class IsCloseTests(unittest.TestCase):
1170+
isclose = math.isclose # sublcasses should override this
1171+
1172+
def assertIsClose(self, a, b, *args, **kwargs):
1173+
self.assertTrue(self.isclose(a, b, *args, **kwargs),
1174+
msg="%s and %s should be close!" % (a, b))
1175+
1176+
def assertIsNotClose(self, a, b, *args, **kwargs):
1177+
self.assertFalse(self.isclose(a, b, *args, **kwargs),
1178+
msg="%s and %s should not be close!" % (a, b))
1179+
1180+
def assertAllClose(self, examples, *args, **kwargs):
1181+
for a, b in examples:
1182+
self.assertIsClose(a, b, *args, **kwargs)
1183+
1184+
def assertAllNotClose(self, examples, *args, **kwargs):
1185+
for a, b in examples:
1186+
self.assertIsNotClose(a, b, *args, **kwargs)
1187+
1188+
def test_negative_tolerances(self):
1189+
# ValueError should be raised if either tolerance is less than zero
1190+
with self.assertRaises(ValueError):
1191+
self.assertIsClose(1, 1, rel_tol=-1e-100)
1192+
with self.assertRaises(ValueError):
1193+
self.assertIsClose(1, 1, rel_tol=1e-100, abs_tol=-1e10)
1194+
1195+
def test_identical(self):
1196+
# identical values must test as close
1197+
identical_examples = [(2.0, 2.0),
1198+
(0.1e200, 0.1e200),
1199+
(1.123e-300, 1.123e-300),
1200+
(12345, 12345.0),
1201+
(0.0, -0.0),
1202+
(345678, 345678)]
1203+
self.assertAllClose(identical_examples, rel_tol=0.0, abs_tol=0.0)
1204+
1205+
def test_eight_decimal_places(self):
1206+
# examples that are close to 1e-8, but not 1e-9
1207+
eight_decimal_places_examples = [(1e8, 1e8 + 1),
1208+
(-1e-8, -1.000000009e-8),
1209+
(1.12345678, 1.12345679)]
1210+
self.assertAllClose(eight_decimal_places_examples, rel_tol=1e-8)
1211+
self.assertAllNotClose(eight_decimal_places_examples, rel_tol=1e-9)
1212+
1213+
def test_near_zero(self):
1214+
# values close to zero
1215+
near_zero_examples = [(1e-9, 0.0),
1216+
(-1e-9, 0.0),
1217+
(-1e-150, 0.0)]
1218+
# these should not be close to any rel_tol
1219+
self.assertAllNotClose(near_zero_examples, rel_tol=0.9)
1220+
# these should be close to abs_tol=1e-8
1221+
self.assertAllClose(near_zero_examples, abs_tol=1e-8)
1222+
1223+
def test_identical_infinite(self):
1224+
# these are close regardless of tolerance -- i.e. they are equal
1225+
self.assertIsClose(INF, INF)
1226+
self.assertIsClose(INF, INF, abs_tol=0.0)
1227+
self.assertIsClose(NINF, NINF)
1228+
self.assertIsClose(NINF, NINF, abs_tol=0.0)
1229+
1230+
def test_inf_ninf_nan(self):
1231+
# these should never be close (following IEEE 754 rules for equality)
1232+
not_close_examples = [(NAN, NAN),
1233+
(NAN, 1e-100),
1234+
(1e-100, NAN),
1235+
(INF, NAN),
1236+
(NAN, INF),
1237+
(INF, NINF),
1238+
(INF, 1.0),
1239+
(1.0, INF),
1240+
(INF, 1e308),
1241+
(1e308, INF)]
1242+
# use largest reasonable tolerance
1243+
self.assertAllNotClose(not_close_examples, abs_tol=0.999999999999999)
1244+
1245+
def test_zero_tolerance(self):
1246+
# test with zero tolerance
1247+
zero_tolerance_close_examples = [(1.0, 1.0),
1248+
(-3.4, -3.4),
1249+
(-1e-300, -1e-300)]
1250+
self.assertAllClose(zero_tolerance_close_examples, rel_tol=0.0)
1251+
1252+
zero_tolerance_not_close_examples = [(1.0, 1.000000000000001),
1253+
(0.99999999999999, 1.0),
1254+
(1.0e200, .999999999999999e200)]
1255+
self.assertAllNotClose(zero_tolerance_not_close_examples, rel_tol=0.0)
1256+
1257+
def test_assymetry(self):
1258+
# test the assymetry example from PEP 485
1259+
self.assertAllClose([(9, 10), (10, 9)], rel_tol=0.1)
1260+
1261+
def test_integers(self):
1262+
# test with integer values
1263+
integer_examples = [(100000001, 100000000),
1264+
(123456789, 123456788)]
1265+
1266+
self.assertAllClose(integer_examples, rel_tol=1e-8)
1267+
self.assertAllNotClose(integer_examples, rel_tol=1e-9)
1268+
1269+
def test_decimals(self):
1270+
# test with Decimal values
1271+
from decimal import Decimal
1272+
1273+
decimal_examples = [(Decimal('1.00000001'), Decimal('1.0')),
1274+
(Decimal('1.00000001e-20'), Decimal('1.0e-20')),
1275+
(Decimal('1.00000001e-100'), Decimal('1.0e-100'))]
1276+
self.assertAllClose(decimal_examples, rel_tol=1e-8)
1277+
self.assertAllNotClose(decimal_examples, rel_tol=1e-9)
1278+
1279+
def test_fractions(self):
1280+
# test with Fraction values
1281+
from fractions import Fraction
1282+
1283+
# could use some more examples here!
1284+
fraction_examples = [(Fraction(1, 100000000) + 1, Fraction(1))]
1285+
self.assertAllClose(fraction_examples, rel_tol=1e-8)
1286+
self.assertAllNotClose(fraction_examples, rel_tol=1e-9)
1287+
1288+
11691289
def test_main():
11701290
from doctest import DocFileSuite
11711291
suite = unittest.TestSuite()
11721292
suite.addTest(unittest.makeSuite(MathTests))
1293+
suite.addTest(unittest.makeSuite(IsCloseTests))
11731294
suite.addTest(DocFileSuite("ieee754.txt"))
11741295
run_unittest(suite)
11751296

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ Library
273273
- Issue #24298: Fix inspect.signature() to correctly unwrap wrappers
274274
around bound methods.
275275

276+
- Issue #24270: Add math.isclose() and cmath.isclose() functions as per PEP 485.
277+
Contributed by Chris Barker and Tal Einat.
278+
276279
IDLE
277280
----
278281

Modules/clinic/cmathmodule.c.h

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,4 +806,55 @@ cmath_isinf(PyModuleDef *module, PyObject *arg)
806806
exit:
807807
return return_value;
808808
}
809-
/*[clinic end generated code: output=274f59792cf4f418 input=a9049054013a1b77]*/
809+
810+
PyDoc_STRVAR(cmath_isclose__doc__,
811+
"isclose($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)\n"
812+
"--\n"
813+
"\n"
814+
"Determine whether two complex numbers are close in value.\n"
815+
"\n"
816+
" rel_tol\n"
817+
" maximum difference for being considered \"close\", relative to the\n"
818+
" magnitude of the input values\n"
819+
" abs_tol\n"
820+
" maximum difference for being considered \"close\", regardless of the\n"
821+
" magnitude of the input values\n"
822+
"\n"
823+
"Return True if a is close in value to b, and False otherwise.\n"
824+
"\n"
825+
"For the values to be considered close, the difference between them must be\n"
826+
"smaller than at least one of the tolerances.\n"
827+
"\n"
828+
"-inf, inf and NaN behave similarly to the IEEE 754 Standard. That is, NaN is\n"
829+
"not close to anything, even itself. inf and -inf are only close to themselves.");
830+
831+
#define CMATH_ISCLOSE_METHODDEF \
832+
{"isclose", (PyCFunction)cmath_isclose, METH_VARARGS|METH_KEYWORDS, cmath_isclose__doc__},
833+
834+
static int
835+
cmath_isclose_impl(PyModuleDef *module, Py_complex a, Py_complex b,
836+
double rel_tol, double abs_tol);
837+
838+
static PyObject *
839+
cmath_isclose(PyModuleDef *module, PyObject *args, PyObject *kwargs)
840+
{
841+
PyObject *return_value = NULL;
842+
static char *_keywords[] = {"a", "b", "rel_tol", "abs_tol", NULL};
843+
Py_complex a;
844+
Py_complex b;
845+
double rel_tol = 1e-09;
846+
double abs_tol = 0.0;
847+
int _return_value;
848+
849+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "DD|$dd:isclose", _keywords,
850+
&a, &b, &rel_tol, &abs_tol))
851+
goto exit;
852+
_return_value = cmath_isclose_impl(module, a, b, rel_tol, abs_tol);
853+
if ((_return_value == -1) && PyErr_Occurred())
854+
goto exit;
855+
return_value = PyBool_FromLong((long)_return_value);
856+
857+
exit:
858+
return return_value;
859+
}
860+
/*[clinic end generated code: output=229e9c48c9d27362 input=a9049054013a1b77]*/

0 commit comments

Comments
 (0)