From af5c998f37ddc62abfd36e0b8be511c392fc25d8 Mon Sep 17 00:00:00 2001
From: stijn <stijn@ignitron.net>
Date: Tue, 2 Jul 2019 10:28:44 +0200
Subject: [PATCH] py/modmath: Implement math.isclose() for non-complex numbers.

As per PEP 485, this function appeared in for Python 3.5.  Configured via
MICROPY_PY_MATH_ISCLOSE which is disabled by default, but enabled for the
ports which already have MICROPY_PY_MATH_SPECIAL_FUNCTIONS enabled.
---
 ports/esp32/mpconfigport.h      |  1 +
 ports/javascript/mpconfigport.h |  1 +
 ports/stm32/mpconfigport.h      |  1 +
 ports/unix/mpconfigport.h       |  1 +
 ports/windows/mpconfigport.h    |  1 +
 py/modmath.c                    | 39 +++++++++++++++++++++++++++
 py/mpconfig.h                   |  5 ++++
 tests/float/math_isclose.py     | 47 +++++++++++++++++++++++++++++++++
 tests/float/math_isclose.py.exp | 27 +++++++++++++++++++
 9 files changed, 123 insertions(+)
 create mode 100644 tests/float/math_isclose.py
 create mode 100644 tests/float/math_isclose.py.exp

diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h
index 364b2de5c..cba319245 100644
--- a/ports/esp32/mpconfigport.h
+++ b/ports/esp32/mpconfigport.h
@@ -98,6 +98,7 @@
 #define MICROPY_PY_COLLECTIONS_ORDEREDDICT  (1)
 #define MICROPY_PY_MATH                     (1)
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS   (1)
+#define MICROPY_PY_MATH_ISCLOSE             (1)
 #define MICROPY_PY_CMATH                    (1)
 #define MICROPY_PY_GC                       (1)
 #define MICROPY_PY_IO                       (1)
diff --git a/ports/javascript/mpconfigport.h b/ports/javascript/mpconfigport.h
index 228113c48..02d83f402 100644
--- a/ports/javascript/mpconfigport.h
+++ b/ports/javascript/mpconfigport.h
@@ -73,6 +73,7 @@
 #define MICROPY_PY_COLLECTIONS      (1)
 #define MICROPY_PY_MATH             (1)
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1)
+#define MICROPY_PY_MATH_ISCLOSE     (1)
 #define MICROPY_PY_CMATH            (1)
 #define MICROPY_PY_IO               (1)
 #define MICROPY_PY_STRUCT           (1)
diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h
index dbb6fa2d5..5eb44bd46 100644
--- a/ports/stm32/mpconfigport.h
+++ b/ports/stm32/mpconfigport.h
@@ -114,6 +114,7 @@
 #define MICROPY_PY_COLLECTIONS_DEQUE (1)
 #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (1)
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1)
+#define MICROPY_PY_MATH_ISCLOSE     (1)
 #define MICROPY_PY_MATH_FACTORIAL   (1)
 #define MICROPY_PY_CMATH            (1)
 #define MICROPY_PY_IO               (1)
diff --git a/ports/unix/mpconfigport.h b/ports/unix/mpconfigport.h
index 123cad2bc..23c562e5a 100644
--- a/ports/unix/mpconfigport.h
+++ b/ports/unix/mpconfigport.h
@@ -104,6 +104,7 @@
 #ifndef MICROPY_PY_MATH_SPECIAL_FUNCTIONS
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1)
 #endif
+#define MICROPY_PY_MATH_ISCLOSE     (MICROPY_PY_MATH_SPECIAL_FUNCTIONS)
 #define MICROPY_PY_CMATH            (1)
 #define MICROPY_PY_IO_IOBASE        (1)
 #define MICROPY_PY_IO_FILEIO        (1)
diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h
index ffe7ae144..1a9842609 100644
--- a/ports/windows/mpconfigport.h
+++ b/ports/windows/mpconfigport.h
@@ -86,6 +86,7 @@
 #define MICROPY_PY_COLLECTIONS_DEQUE (1)
 #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (1)
 #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1)
+#define MICROPY_PY_MATH_ISCLOSE     (1)
 #define MICROPY_PY_CMATH            (1)
 #define MICROPY_PY_IO_FILEIO        (1)
 #define MICROPY_PY_GC_COLLECT_RETVAL (1)
diff --git a/py/modmath.c b/py/modmath.c
index d106f240c..35bb44bea 100644
--- a/py/modmath.c
+++ b/py/modmath.c
@@ -171,6 +171,42 @@ MATH_FUN_1(lgamma, lgamma)
 #endif
 //TODO: fsum
 
+#if MICROPY_PY_MATH_ISCLOSE
+STATIC mp_obj_t mp_math_isclose(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+    enum { ARG_a, ARG_b, ARG_rel_tol, ARG_abs_tol };
+    static const mp_arg_t allowed_args[] = {
+        {MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ},
+        {MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ},
+        {MP_QSTR_rel_tol, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
+        {MP_QSTR_abs_tol, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(0)}},
+    };
+    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
+    const mp_float_t a = mp_obj_get_float(args[ARG_a].u_obj);
+    const mp_float_t b = mp_obj_get_float(args[ARG_b].u_obj);
+    const mp_float_t rel_tol = args[ARG_rel_tol].u_obj == MP_OBJ_NULL
+        ? (mp_float_t)1e-9 : mp_obj_get_float(args[ARG_rel_tol].u_obj);
+    const mp_float_t abs_tol = mp_obj_get_float(args[ARG_abs_tol].u_obj);
+    if (rel_tol < (mp_float_t)0.0 || abs_tol < (mp_float_t)0.0) {
+        math_error();
+    }
+    if (a == b) {
+        return mp_const_true;
+    }
+    const mp_float_t difference = MICROPY_FLOAT_C_FUN(fabs)(a - b);
+    if (isinf(difference)) { // Either a or b is inf
+        return mp_const_false;
+    }
+    if ((difference <= abs_tol) ||
+        (difference <= MICROPY_FLOAT_C_FUN(fabs)(rel_tol * a)) ||
+        (difference <= MICROPY_FLOAT_C_FUN(fabs)(rel_tol * b))) {
+        return mp_const_true;
+    }
+    return mp_const_false;
+}
+MP_DEFINE_CONST_FUN_OBJ_KW(mp_math_isclose_obj, 2, mp_math_isclose);
+#endif
+
 // Function that takes a variable number of arguments
 
 // log(x[, base])
@@ -335,6 +371,9 @@ STATIC const mp_rom_map_elem_t mp_module_math_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_isfinite), MP_ROM_PTR(&mp_math_isfinite_obj) },
     { MP_ROM_QSTR(MP_QSTR_isinf), MP_ROM_PTR(&mp_math_isinf_obj) },
     { MP_ROM_QSTR(MP_QSTR_isnan), MP_ROM_PTR(&mp_math_isnan_obj) },
+    #if MICROPY_PY_MATH_ISCLOSE
+    { MP_ROM_QSTR(MP_QSTR_isclose), MP_ROM_PTR(&mp_math_isclose_obj) },
+    #endif
     { MP_ROM_QSTR(MP_QSTR_trunc), MP_ROM_PTR(&mp_math_trunc_obj) },
     { MP_ROM_QSTR(MP_QSTR_radians), MP_ROM_PTR(&mp_math_radians_obj) },
     { MP_ROM_QSTR(MP_QSTR_degrees), MP_ROM_PTR(&mp_math_degrees_obj) },
diff --git a/py/mpconfig.h b/py/mpconfig.h
index bded9da9f..a21a6c707 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -1079,6 +1079,11 @@ typedef double mp_float_t;
 #define MICROPY_PY_MATH_FACTORIAL (0)
 #endif
 
+// Whether to provide math.isclose function
+#ifndef MICROPY_PY_MATH_ISCLOSE
+#define MICROPY_PY_MATH_ISCLOSE (0)
+#endif
+
 // Whether to provide "cmath" module
 #ifndef MICROPY_PY_CMATH
 #define MICROPY_PY_CMATH (0)
diff --git a/tests/float/math_isclose.py b/tests/float/math_isclose.py
new file mode 100644
index 000000000..13dfff75f
--- /dev/null
+++ b/tests/float/math_isclose.py
@@ -0,0 +1,47 @@
+# test math.isclose (appeared in Python 3.5)
+
+try:
+    from math import isclose
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+def test(a, b, **kwargs):
+    print(isclose(a, b, **kwargs))
+
+def test_combinations(a, b, **kwargs):
+    test(a, a, **kwargs)
+    test(a, b, **kwargs)
+    test(b, a, **kwargs)
+    test(b, b, **kwargs)
+
+# Special numbers
+test_combinations(float('nan'), 1)
+test_combinations(float('inf'), 1)
+test_combinations(float('-inf'), 1)
+
+# Equality
+test(1.0, 1.0, rel_tol=0.0, abs_tol=0.0)
+test(2.35e-100, 2.35e-100, rel_tol=0.0, abs_tol=0.0)
+test(2.1234e100, 2.1234e100, rel_tol=0.0, abs_tol=0.0)
+
+# Relative tolerance
+test(1000.0, 1001.0, rel_tol=1e-3)
+test(1000.0, 1001.0, rel_tol=1e-4)
+test(1000, 1001, rel_tol=1e-3)
+test(1000, 1001, rel_tol=1e-4)
+test_combinations(0, 1, rel_tol=1.0)
+
+# Absolute tolerance
+test(0.0, 1e-10, abs_tol=1e-10, rel_tol=0.1)
+test(0.0, 1e-10, abs_tol=0.0, rel_tol=0.1)
+
+# Bad parameters
+try:
+    isclose(0, 0, abs_tol=-1)
+except ValueError:
+    print('ValueError')
+try:
+    isclose(0, 0, rel_tol=-1)
+except ValueError:
+    print('ValueError')
diff --git a/tests/float/math_isclose.py.exp b/tests/float/math_isclose.py.exp
new file mode 100644
index 000000000..02974666c
--- /dev/null
+++ b/tests/float/math_isclose.py.exp
@@ -0,0 +1,27 @@
+False
+False
+False
+True
+True
+False
+False
+True
+True
+False
+False
+True
+True
+True
+True
+True
+False
+True
+False
+True
+True
+True
+True
+True
+False
+ValueError
+ValueError
-- 
GitLab