From 2857b939151e9a4f0b1758685dae32d4fcd7023d Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:24:40 -0400 Subject: [PATCH 01/10] Create new function to check equality of array lengths Added a new function to `pyxx.arrays` to check whether an arbitrary number of sequence-type objects (lists, tuples, and/or strings) have equal length. Added tests for new code --- pyxx/arrays/__init__.py | 2 +- pyxx/arrays/functions/size.py | 37 +++++++++- tests/arrays/functions/test_size.py | 103 +++++++++++++++++++++++++++- 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/pyxx/arrays/__init__.py b/pyxx/arrays/__init__.py index 6aa51b7..023e371 100644 --- a/pyxx/arrays/__init__.py +++ b/pyxx/arrays/__init__.py @@ -5,4 +5,4 @@ """ from .functions.convert import convert_to_tuple -from .functions.size import max_list_item_len +from .functions.size import check_len_equal, max_list_item_len diff --git a/pyxx/arrays/functions/size.py b/pyxx/arrays/functions/size.py index 162867e..6eead66 100644 --- a/pyxx/arrays/functions/size.py +++ b/pyxx/arrays/functions/size.py @@ -1,7 +1,42 @@ """Functions for evaluating or comparing sizes of array-like objects """ -from typing import Union +from typing import Any, Union + + +def check_len_equal(item1: Any, item2: Any, *args: Any): + """Checks whether the lengths of a set of sequence-type objects + are equal + + Evaluates the lengths of a set of items (such as lists, tuples, or + strings), returning whether all items have the same length as well as + either the length of all items (if all lengths are equal) or a list + containing the lengths of each item (if they are not equal). + + Parameters + ---------- + item1 : Any + First item whose length to evaluate + item2 : Any + Second item whose length to evaluate + *args : Any, optional + Any other items whose lengths are to be evaluated + + Returns + ------- + bool + Whether all items have the same length + int or list + Returns an integer containing the length of all items (if all + lengths are equal), or a list containing the lengths of each + item (if lengths differ) + """ + lengths = [len(item1)] + [len(item2)] + [len(i) for i in args] + + if len(set(lengths)) == 1: + return True, lengths[0] + else: + return False, lengths def max_list_item_len(input_list: Union[list, tuple]): diff --git a/tests/arrays/functions/test_size.py b/tests/arrays/functions/test_size.py index 836d1cd..1eccb1b 100644 --- a/tests/arrays/functions/test_size.py +++ b/tests/arrays/functions/test_size.py @@ -1,7 +1,108 @@ from typing import List import unittest -from pyxx.arrays import max_list_item_len +from pyxx.arrays import ( + check_len_equal, + max_list_item_len, +) + + +class Test_CheckLenEqual(unittest.TestCase): + def test_check_list_equal(self): + # Verifies that lengths of lists are evaluated correctly + with self.subTest(num_lists=2): + self.assertTupleEqual( + check_len_equal(['a', 'b', 'c'], [1, 2, 3]), + (True, 3) + ) + self.assertTupleEqual(check_len_equal(['a'], [1]), (True, 1)) + + with self.subTest(num_lists=3): + self.assertTupleEqual( + check_len_equal(['a', 'b'], [1, 2], ['cd', 3]), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(['a'], [1], ['c']), (True, 1)) + + with self.subTest(num_lists=4): + self.assertTupleEqual( + check_len_equal(['a', 'b'], [1, 2], ['cd', 3], [None, None]), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(['a'], [1], ['c'], [None]), (True, 1)) + + def test_check_tuple_equal(self): + # Verifies that lengths of tuples are evaluated correctly + with self.subTest(num_tuples=2): + self.assertTupleEqual( + check_len_equal(('a', 'b', 'c'), (1, 2, 3)), + (True, 3) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,)), (True, 1)) + + with self.subTest(num_tuples=3): + self.assertTupleEqual( + check_len_equal(('a', 'b'), (1, 2), ('cd', 3)), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',)), (True, 1)) + + with self.subTest(num_tuples=4): + self.assertTupleEqual( + check_len_equal(('a', 'b'), (1, 2), ('cd', 3), (None, None)), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',), (None,)), (True, 1)) + + def test_check_str_equal(self): + # Verifies that lengths of strings are evaluated correctly + with self.subTest(num_strings=2): + self.assertTupleEqual( + check_len_equal('abc', '123'), + (True, 3) + ) + + with self.subTest(num_strings=3): + self.assertTupleEqual( + check_len_equal('ab', '12', 'c3'), + (True, 2) + ) + + with self.subTest(num_strings=4): + self.assertTupleEqual( + check_len_equal('ab', '12', 'c3', 'No'), + (True, 2) + ) + + def test_check_mixed_type_equal(self): + # Verifies that lengths of mixed list/tuple/string arguments + # are evaluated correctly + self.assertTupleEqual( + check_len_equal(['a', 'b'], (1, 2), ('cd', 3), (None, None), 'ce'), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',), (None,), 'c'), (True, 1)) + + def test_check_mixed_type_unequal(self): + # Verifies that lengths of mixed list/tuple/string arguments with + # different lengths are compared correctly + with self.subTest(num_args=2): + self.assertTupleEqual( + check_len_equal((1, 2), 'abcdefjkl'), + (False, [2, 9]) + ) + + with self.subTest(num_args=3): + self.assertTupleEqual( + check_len_equal(('cd', 3), (None, None), 'abcdefjkl'), + (False, [2, 2, 9]) + ) + + with self.subTest(num_args=5): + self.assertTupleEqual( + check_len_equal(['a', 'b', 'c'], (1, 2), ('cd', 3), (None, None), 'abcdefjkl'), + (False, [3, 2, 2, 2, 9]) + ) class Test_MaxListLength(unittest.TestCase): From 1033cd9aff4b0a1f71e2eadbe20744b10133783b Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:25:21 -0400 Subject: [PATCH 02/10] Rename documentation `pyxx.arrays.size` template --- ...reference_arrays_max_len.rst => api_reference_arrays_size.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/source/_templates/{api_reference_arrays_max_len.rst => api_reference_arrays_size.rst} (100%) diff --git a/docs/source/_templates/api_reference_arrays_max_len.rst b/docs/source/_templates/api_reference_arrays_size.rst similarity index 100% rename from docs/source/_templates/api_reference_arrays_max_len.rst rename to docs/source/_templates/api_reference_arrays_size.rst From 701be88261015013d9d53f544c4a140a44bc7232 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:25:52 -0400 Subject: [PATCH 03/10] Add `pyxx.arrays.check_len_equal()` function to documentation --- docs/source/api_reference/arrays.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/api_reference/arrays.rst b/docs/source/api_reference/arrays.rst index 25df92c..74f03fc 100644 --- a/docs/source/api_reference/arrays.rst +++ b/docs/source/api_reference/arrays.rst @@ -1,3 +1,8 @@ +.. spelling:word-list:: + + args + + pyxx.arrays =========== @@ -26,6 +31,7 @@ array-like objects. .. autosummary:: :toctree: ./api - :template: ../_templates/api_reference_arrays_max_len.rst + :template: ../_templates/api_reference_arrays_size.rst + check_len_equal max_list_item_len From 99a8a2af74a9a91afa83df2f3a7f4ee4f4d363e3 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Fri, 9 Sep 2022 08:59:59 -0400 Subject: [PATCH 04/10] Add function to check equality of NumPy arrays Added new function `pyxx.arrays.np_array_equal()` to check equality (shape and values) of numeric NumPy arrays. Also added tests and documentation for this function --- .../api_reference_arrays_equality.rst | 10 +++ docs/source/api_reference/arrays.rst | 15 ++++ pyxx/arrays/__init__.py | 1 + pyxx/arrays/functions/equality.py | 60 ++++++++++++++ requirements.txt | 2 + tests/arrays/functions/__init__.py | 1 + tests/arrays/functions/test_equality.py | 80 +++++++++++++++++++ 7 files changed, 169 insertions(+) create mode 100644 docs/source/_templates/api_reference_arrays_equality.rst create mode 100644 pyxx/arrays/functions/equality.py create mode 100644 tests/arrays/functions/test_equality.py diff --git a/docs/source/_templates/api_reference_arrays_equality.rst b/docs/source/_templates/api_reference_arrays_equality.rst new file mode 100644 index 0000000..1f137d5 --- /dev/null +++ b/docs/source/_templates/api_reference_arrays_equality.rst @@ -0,0 +1,10 @@ +.. spelling:word-list:: + + np + + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/source/api_reference/arrays.rst b/docs/source/api_reference/arrays.rst index 74f03fc..6d4842c 100644 --- a/docs/source/api_reference/arrays.rst +++ b/docs/source/api_reference/arrays.rst @@ -1,6 +1,8 @@ .. spelling:word-list:: args + np + tol pyxx.arrays @@ -23,6 +25,19 @@ convert one or more arrays of one type to a different type. convert_to_tuple +Array Equality +-------------- + +The functions in this section are intended to check whether arrays have equal +size and/or content. + +.. autosummary:: + :toctree: ./api + :template: ../_templates/api_reference_arrays_equality.rst + + np_array_equal + + Array Size ---------- diff --git a/pyxx/arrays/__init__.py b/pyxx/arrays/__init__.py index 023e371..51124a0 100644 --- a/pyxx/arrays/__init__.py +++ b/pyxx/arrays/__init__.py @@ -5,4 +5,5 @@ """ from .functions.convert import convert_to_tuple +from .functions.equality import np_array_equal from .functions.size import check_len_equal, max_list_item_len diff --git a/pyxx/arrays/functions/equality.py b/pyxx/arrays/functions/equality.py new file mode 100644 index 0000000..945b19f --- /dev/null +++ b/pyxx/arrays/functions/equality.py @@ -0,0 +1,60 @@ +"""Functions for evaluating equality of array-like objects +""" + +import numpy as np + + +def np_array_equal(array1: np.ndarray, array2: np.ndarray, *args: np.ndarray, + tol: float = 1e-16): + """Checks that NumPy arrays are equal within a given tolerance + + Returns ``True`` if the NumPy arrays passed as arguments are of the same + shape and the maximum difference between their elements is less than or + equal to ``tol``, and returns ``False`` otherwise. + + Parameters + ---------- + array1 : np.ndarray + First array to evaluate + array2 : np.ndarray + Second item to evaluate + *args : np.ndarray, optional + Any other arrays to be evaluated + tol : float, optional + Maximum difference between arrays to consider equivalent (default + is ``1e-16``) + + Returns + ------- + bool + Whether ``array1``, ``array2``, ``*args`` have the same shape and + are equal within tolerance ``tol`` + + Notes + ----- + One question that may arise is, *why is this function necessary?* NumPy + already offers functions like `numpy.array_equal() `__, `numpy.isclose() + `__, + and `numpy.allclose() `__. + + The main difference between these functions and :py:func:`np_array_equal` + is that the NumPy functions mentioned will typically throw an exception + if the array sizes being compared differ, while :py:func:`np_array_equal` + simply returns ``False``. Thus, this function eliminates the need to + catch exceptions -- it simply returns ``True`` or ``False`` directly. In + certain cases, this can simplify code. + """ + # Convert all inputs to Numpy arrays and create a list of all arrays + # to be compared with `array1` + array1 = np.array(array1) + arrays = [np.array(array2)] + [np.array(i) for i in args] + + # Check that arrays have equal shape and are equal within tolerance `tol` + for array in arrays: + if not ((array1.shape == array.shape) + and (np.max(np.abs(array - array1)) <= tol)): + return False + + return True diff --git a/requirements.txt b/requirements.txt index 4fef116..e04198b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ # that you use the `.vscode/requirements.txt` file to install dependencies. ############################################################################## + +numpy diff --git a/tests/arrays/functions/__init__.py b/tests/arrays/functions/__init__.py index a124800..15899ad 100644 --- a/tests/arrays/functions/__init__.py +++ b/tests/arrays/functions/__init__.py @@ -1,2 +1,3 @@ from .test_convert import * +from .test_equality import * from .test_size import * diff --git a/tests/arrays/functions/test_equality.py b/tests/arrays/functions/test_equality.py new file mode 100644 index 0000000..ff51f19 --- /dev/null +++ b/tests/arrays/functions/test_equality.py @@ -0,0 +1,80 @@ +import unittest + +import numpy as np + +from pyxx.arrays import np_array_equal + + +class Test_NumPyListEqual(unittest.TestCase): + def setUp(self): + self.array = np.array([[ 3, 6, -3.213, 0], + [3.23, 1, -1.42e-3, 4e6]]) + + self.array_uneq_shape = np.array([[3, 6, -3.213, 0], + [3.23, 1, -1.42e-3, 4e6], + [1, 2, 3, 4]]) + + self.array_uneq_val = np.array([[ 3, 6, -3.213, 0], + [3.23, 1, -1.41e-3, 4e6]]) + + def test_equal(self): + # Verifies that arrays of equal shape and values are evaluated as equal + with self.subTest(args=2): + self.assertTrue(np_array_equal(self.array, self.array)) + + with self.subTest(args=3): + self.assertTrue(np_array_equal(self.array, self.array, self.array)) + + with self.subTest(args=4): + self.assertTrue(np_array_equal(self.array, self.array, self.array, self.array)) + + def test_unequal_shape(self): + # Verifies that arrays of different shape are evaluated as not equal + with self.subTest(args=2): + self.assertFalse(np_array_equal(self.array, self.array_uneq_shape)) + + with self.subTest(args=3): + self.assertFalse(np_array_equal( + self.array_uneq_shape, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array_uneq_shape, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array_uneq_shape)) + + with self.subTest(args=4): + self.assertFalse(np_array_equal( + self.array_uneq_shape, self.array, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array_uneq_shape, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array_uneq_shape, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array, self.array_uneq_shape)) + + def test_unequal_values(self): + # Verifies that arrays with different values are evaluated as not equal + with self.subTest(args=2): + self.assertFalse(np_array_equal(self.array, self.array_uneq_val)) + + with self.subTest(args=3): + self.assertFalse(np_array_equal( + self.array_uneq_val, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array_uneq_val, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array_uneq_val)) + + with self.subTest(args=4): + self.assertFalse(np_array_equal( + self.array_uneq_val, self.array, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array_uneq_val, self.array, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array_uneq_val, self.array)) + self.assertFalse(np_array_equal( + self.array, self.array, self.array, self.array_uneq_val)) + + def test_tolerance(self): + # Verifies that setting tolerance for equality functions as expected + self.assertTrue( + np_array_equal(self.array, self.array_uneq_val, tol=0.000011)) From 42a4c00e75122e9f37ca6ebdafc579c88fe20bfd Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Fri, 9 Sep 2022 09:03:28 -0400 Subject: [PATCH 05/10] Add warning to `pyxx.arrays.np_array_equal()` function that arguments must use numeric types --- pyxx/arrays/functions/equality.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyxx/arrays/functions/equality.py b/pyxx/arrays/functions/equality.py index 945b19f..de978e7 100644 --- a/pyxx/arrays/functions/equality.py +++ b/pyxx/arrays/functions/equality.py @@ -6,7 +6,7 @@ def np_array_equal(array1: np.ndarray, array2: np.ndarray, *args: np.ndarray, tol: float = 1e-16): - """Checks that NumPy arrays are equal within a given tolerance + """Checks that numeric NumPy arrays are equal within a given tolerance Returns ``True`` if the NumPy arrays passed as arguments are of the same shape and the maximum difference between their elements is less than or @@ -30,6 +30,11 @@ def np_array_equal(array1: np.ndarray, array2: np.ndarray, *args: np.ndarray, Whether ``array1``, ``array2``, ``*args`` have the same shape and are equal within tolerance ``tol`` + Warnings + -------- + Since array values are compared within a tolerance, all arrays to be + compared must contain only numeric types (integer, float, etc.). + Notes ----- One question that may arise is, *why is this function necessary?* NumPy From 5c0c4470ebdeb589d9845bc1458bdf976d6ce186 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Thu, 22 Sep 2022 21:42:19 -0400 Subject: [PATCH 06/10] Add missing test for `pyxx.files.File` class --- tests/files/classes/test_file.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/files/classes/test_file.py b/tests/files/classes/test_file.py index 93f95c9..92873b1 100644 --- a/tests/files/classes/test_file.py +++ b/tests/files/classes/test_file.py @@ -30,6 +30,13 @@ def setUp(self): 'str': self.file_from_str, } + def test_file_repr_empty(self): + # Verifies that the file object string representation is computed + # correctly if `path` attribute has not been assigned a value + self.assertEqual( + self.file_empty.__repr__(), + "") + def test_file_repr_before(self): # Verifies that file object descriptor is computed correctly before # computing file hashes From 7eec70e4d77dec7f062d40bb1270fff5356c550f Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Thu, 22 Sep 2022 22:42:24 -0400 Subject: [PATCH 07/10] Create function `pyxx.arrays.is_len_equal()` to check that arrays have equal length Added new function that provides the same functionality as `pyxx.arrays.check_len_equal()` but returns only a True/False output and offers a slight performance improvement in certain cases --- docs/source/api_reference/arrays.rst | 1 + pyxx/arrays/__init__.py | 6 ++- pyxx/arrays/functions/size.py | 46 +++++++++++++++++++++ tests/arrays/functions/test_size.py | 61 ++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/source/api_reference/arrays.rst b/docs/source/api_reference/arrays.rst index 6d4842c..1a0a894 100644 --- a/docs/source/api_reference/arrays.rst +++ b/docs/source/api_reference/arrays.rst @@ -49,4 +49,5 @@ array-like objects. :template: ../_templates/api_reference_arrays_size.rst check_len_equal + is_len_equal max_list_item_len diff --git a/pyxx/arrays/__init__.py b/pyxx/arrays/__init__.py index 51124a0..adee3f4 100644 --- a/pyxx/arrays/__init__.py +++ b/pyxx/arrays/__init__.py @@ -6,4 +6,8 @@ from .functions.convert import convert_to_tuple from .functions.equality import np_array_equal -from .functions.size import check_len_equal, max_list_item_len +from .functions.size import ( + check_len_equal, + is_len_equal, + max_list_item_len, +) diff --git a/pyxx/arrays/functions/size.py b/pyxx/arrays/functions/size.py index 6eead66..71abdb6 100644 --- a/pyxx/arrays/functions/size.py +++ b/pyxx/arrays/functions/size.py @@ -30,6 +30,13 @@ def check_len_equal(item1: Any, item2: Any, *args: Any): Returns an integer containing the length of all items (if all lengths are equal), or a list containing the lengths of each item (if lengths differ) + + See Also + -------- + is_len_equal : + Identical functionality, but returns only the ``bool`` output and + may theoretically run slightly faster in cases where the length(s) + of the inputs does not need to be returned """ lengths = [len(item1)] + [len(item2)] + [len(i) for i in args] @@ -39,6 +46,45 @@ def check_len_equal(item1: Any, item2: Any, *args: Any): return False, lengths +def is_len_equal(item1: Any, item2: Any, *args: Any): + """Checks whether the lengths of a set of sequence-type objects + are equal + + Evaluates the lengths of a set of items (such as lists, tuples, or + strings), returning whether all items have the same length. This + function should be slightly faster than :py:func:`check_len_equal` + for applications where the lengths of the input arguments do not + need to be returned. + + Parameters + ---------- + item1 : Any + First item whose length to evaluate + item2 : Any + Second item whose length to evaluate + *args : Any, optional + Any other items whose lengths are to be evaluated + + Returns + ------- + bool + Whether all items have the same length + + See Also + -------- + check_len_equal : + Identical functionality, but additionally returns the length of the + input arguments + """ + length1 = len(item1) + + for item in [item2] + list(args): + if not len(item) == length1: + return False + + return True + + def max_list_item_len(input_list: Union[list, tuple]): """Finds the maximum length of any item in a list or tuple diff --git a/tests/arrays/functions/test_size.py b/tests/arrays/functions/test_size.py index 1eccb1b..c61f384 100644 --- a/tests/arrays/functions/test_size.py +++ b/tests/arrays/functions/test_size.py @@ -3,6 +3,7 @@ from pyxx.arrays import ( check_len_equal, + is_len_equal, max_list_item_len, ) @@ -105,6 +106,66 @@ def test_check_mixed_type_unequal(self): ) +class Test_IsLenEqual(unittest.TestCase): + def test_is_list_equal(self): + # Verifies that equality of lengths of lists is evaluated correctly + with self.subTest(num_lists=2): + self.assertTrue(is_len_equal(['a', 'b', 'c'], [1, 2, 3])) + self.assertTrue(is_len_equal(['a'], [1])) + + with self.subTest(num_lists=3): + self.assertTrue(is_len_equal(['a', 'b'], [1, 2], ['cd', 3])) + self.assertTrue(is_len_equal(['a'], [1], ['c'])) + + with self.subTest(num_lists=4): + self.assertTrue(is_len_equal(['a', 'b'], [1, 2], ['cd', 3], [None, None])) + self.assertTrue(is_len_equal(['a'], [1], ['c'], [None])) + + def test_is_tuple_equal(self): + # Verifies that equality of lengths of tuples is evaluated correctly + with self.subTest(num_tuples=2): + self.assertTrue(is_len_equal(('a', 'b', 'c'), (1, 2, 3))) + self.assertTrue(is_len_equal(('a',), (1,))) + + with self.subTest(num_tuples=3): + self.assertTrue(is_len_equal(('a', 'b'), (1, 2), ('cd', 3))) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',))) + + with self.subTest(num_tuples=4): + self.assertTrue(is_len_equal(('a', 'b'), (1, 2), ('cd', 3), (None, None))) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',), (None,))) + + def test_is_str_equal(self): + # Verifies that equality of lengths of strings is evaluated correctly + with self.subTest(num_strings=2): + self.assertTrue(is_len_equal('abc', '123')) + + with self.subTest(num_strings=3): + self.assertTrue(is_len_equal('ab', '12', 'c3')) + + with self.subTest(num_strings=4): + self.assertTrue(is_len_equal('ab', '12', 'c3', 'No')) + + def test_is_mixed_type_equal(self): + # Verifies that equality of lengths of mixed list/tuple/string arguments + # is evaluated correctly + self.assertTrue(is_len_equal(['a', 'b'], (1, 2), ('cd', 3), (None, None), 'ce')) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',), (None,), 'c')) + + def test_is_mixed_type_unequal(self): + # Verifies that equality of lengths of mixed list/tuple/string arguments with + # different lengths is evaluated correctly + with self.subTest(num_args=2): + self.assertFalse(is_len_equal((1, 2), 'abcdefjkl')) + + with self.subTest(num_args=3): + self.assertFalse(is_len_equal(('cd', 3), (None, None), 'abcdefjkl')) + + with self.subTest(num_args=5): + self.assertFalse(is_len_equal(['a', 'b', 'c'], (1, 2), ('cd', 3), + (None, None), 'abcdefjkl')) + + class Test_MaxListLength(unittest.TestCase): def __test_batch(self, test_cases: List[dict]): for sample in test_cases: From 4e2a9543a21b8348cc6edea7a1cf9a7dfad4cfcd Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Sat, 24 Sep 2022 12:19:01 -0400 Subject: [PATCH 08/10] Simplify program version format in `pyxx/__init__.py` --- pyxx/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyxx/__init__.py b/pyxx/__init__.py index 6909bff..1b10948 100644 --- a/pyxx/__init__.py +++ b/pyxx/__init__.py @@ -6,8 +6,4 @@ # PROGRAM VERSION ------------------------------------------------------------ -_VERSION_MAJOR = 1 -_VERSION_MINOR = 0 -_VERSION_PATCH = 0 - -__version__ = f'{_VERSION_MAJOR}.{_VERSION_MINOR}.{_VERSION_PATCH}' +__version__ = '1.0.0' From 285914c22c9b14ad86b2b7e1aad8c4f7f91a2006 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Sat, 24 Sep 2022 15:22:21 -0400 Subject: [PATCH 09/10] Replace `pyxx.arrays.np_array_equal()` function with more general `pyxx.arrays.is_array_equal()` function Rewrote array equality function to make it more general, capable of comparing a wider variety of arrays --- docs/source/api_reference/arrays.rst | 2 +- docs/source/conf.py | 6 + pyxx/arrays/__init__.py | 2 +- pyxx/arrays/functions/equality.py | 161 ++++++++++++---- tests/__init__.py | 13 ++ tests/arrays/functions/test_equality.py | 232 +++++++++++++++++++++--- 6 files changed, 355 insertions(+), 61 deletions(-) diff --git a/docs/source/api_reference/arrays.rst b/docs/source/api_reference/arrays.rst index 1a0a894..ac5cd24 100644 --- a/docs/source/api_reference/arrays.rst +++ b/docs/source/api_reference/arrays.rst @@ -35,7 +35,7 @@ size and/or content. :toctree: ./api :template: ../_templates/api_reference_arrays_equality.rst - np_array_equal + is_array_equal Array Size diff --git a/docs/source/conf.py b/docs/source/conf.py index 1842751..421162c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,7 @@ sys.path.append(str(pathlib.Path(__file__).resolve().parents[2])) import pyxx +from pyxx.arrays.functions.equality import Array_or_Number_or_String # -- Project information ----------------------------------------------------- @@ -127,6 +128,11 @@ 'show-inheritance': True, } +# Type aliases +autodoc_type_aliases = { + Array_or_Number_or_String: Array_or_Number_or_String, +} + # -- Matplotlib plotting extension options ----------------------------------- # https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html diff --git a/pyxx/arrays/__init__.py b/pyxx/arrays/__init__.py index adee3f4..5199bb8 100644 --- a/pyxx/arrays/__init__.py +++ b/pyxx/arrays/__init__.py @@ -5,7 +5,7 @@ """ from .functions.convert import convert_to_tuple -from .functions.equality import np_array_equal +from .functions.equality import is_array_equal from .functions.size import ( check_len_equal, is_len_equal, diff --git a/pyxx/arrays/functions/equality.py b/pyxx/arrays/functions/equality.py index de978e7..7454e60 100644 --- a/pyxx/arrays/functions/equality.py +++ b/pyxx/arrays/functions/equality.py @@ -1,65 +1,152 @@ """Functions for evaluating equality of array-like objects """ +from __future__ import annotations +from numbers import Number +from typing import List, Tuple, Union + import numpy as np +from .size import is_len_equal + + +# Type alias: type for each `item` argument in `is_array_equal()` +__element_types = Union[Number, np.ndarray, str] +Array_or_Number_or_String = Union[List[__element_types], + Tuple[__element_types], + __element_types] -def np_array_equal(array1: np.ndarray, array2: np.ndarray, *args: np.ndarray, + +def is_array_equal(item1: Array_or_Number_or_String, + item2: Array_or_Number_or_String, + *args: Array_or_Number_or_String, tol: float = 1e-16): - """Checks that numeric NumPy arrays are equal within a given tolerance + """Checks that arrays are equal in shape and content - Returns ``True`` if the NumPy arrays passed as arguments are of the same - shape and the maximum difference between their elements is less than or - equal to ``tol``, and returns ``False`` otherwise. + Returns ``True`` if all arrays passed as arguments are of the same shape + and all elements are equal within a given tolerance ``tol`` (for numeric + elements) or exactly equal (for string elements), and returns ``False`` + otherwise. Inputs can be lists, tuples, NumPy arrays, numbers, strings, + or nested arrays composed of any of these types. Parameters ---------- - array1 : np.ndarray + item1 : list or tuple or np.ndarray or Number or str First array to evaluate - array2 : np.ndarray + item2 : list or tuple or np.ndarray or Number or str Second item to evaluate - *args : np.ndarray, optional + *args : list or tuple or np.ndarray or Number or str, optional Any other arrays to be evaluated tol : float, optional - Maximum difference between arrays to consider equivalent (default - is ``1e-16``) + Maximum difference between numeric values to consider equivalent + (default is ``1e-16``) Returns ------- bool - Whether ``array1``, ``array2``, ``*args`` have the same shape and - are equal within tolerance ``tol`` + Whether ``item1``, ``item2``, ``*args`` have the same shape, and + all elements are equal within tolerance ``tol`` (for numeric elements) + and exactly equal (for string elements) Warnings -------- - Since array values are compared within a tolerance, all arrays to be - compared must contain only numeric types (integer, float, etc.). + - The shape of the input arrays must be identical for the arrays to be + considered equal. The shape of numbers is considered different from the + shape of lists, so observe that ``0`` and ``[0]`` are **not** considered + equal in shape. + + - By default, NumPy arrays are of homogeneous type. This means that, for + instance, ``pyxx.arrays.is_array_equal(np.array([1, 'a']), [1, 'a'])`` + evaluates to ``False`` (because the NumPy array is converted to all + strings). To avoid this issue, it is possible to create NumPy arrays + with the ``dtype=object`` argument and allow mixed types. For example, + ``pyxx.arrays.is_array_equal(np.array([1, 'a'], dtype=object), [1, 'a'])`` + evaluates to ``True``. Notes ----- - One question that may arise is, *why is this function necessary?* NumPy - already offers functions like `numpy.array_equal() `__, `numpy.isclose() - `__, - and `numpy.allclose() `__. - - The main difference between these functions and :py:func:`np_array_equal` - is that the NumPy functions mentioned will typically throw an exception - if the array sizes being compared differ, while :py:func:`np_array_equal` - simply returns ``False``. Thus, this function eliminates the need to - catch exceptions -- it simply returns ``True`` or ``False`` directly. In - certain cases, this can simplify code. + **Recursion Limit:** Internally, :py:func:`is_array_equal` is a recursive + function. It is possible that for extremely large nested arrays, Python's + recursion limit may be reached. If this occurs and it is necessary to + compare such a large array, consider increasing the recursion limit using + the `sys.setrecursionlimit() `__ function. + + **Purpose:** One question that may arise is, *why is this function + necessary?* NumPy already offers functions like `numpy.array_equal() + `__, `numpy.isclose() `__, and `numpy.allclose() + `__. + + There are several main advantages of :py:func:`is_array_equal`: + + - NumPy requires that arrays are numeric and are not "ragged" (sub-lists + must all have the same length, recursively. For example, the array + ``x = [[1,2,3], [1,2]]`` is "ragged" since ``len(x[0]) != len(x[1])``). + In contrast, :py:func:`is_array_equal` can compare arrays with a mix of + strings, numbers, lists, and tuples, as well as "ragged" arrays. + + - The NumPy functions mentioned will typically throw an exception if the + array sizes being compared differ, but :py:func:`is_array_equal` simply + returns ``False`` in this case. This can eliminate the need to catch + exceptions for certain applications. """ - # Convert all inputs to Numpy arrays and create a list of all arrays - # to be compared with `array1` - array1 = np.array(array1) - arrays = [np.array(array2)] + [np.array(i) for i in args] - - # Check that arrays have equal shape and are equal within tolerance `tol` - for array in arrays: - if not ((array1.shape == array.shape) - and (np.max(np.abs(array - array1)) <= tol)): + # Create list of array(s) to compare with `item1` + items = [item2] + list(args) + + # If inputs are numbers, directly compare them (requiring difference + # between numbers to be less than or equal to `tol` to consider the + # inputs equal) + if isinstance(item1, Number) \ + or (isinstance(item1, np.ndarray) and item1.ndim == 0): + for x in items: + # Check whether `item2` or any of `args` are an array. If so, + # this indicates that the array shapes are not equal + if isinstance(x, (list, tuple)) \ + or (isinstance(x, np.ndarray) and x.ndim > 0): + return False + + # Argument `item1` is known to be a number, so attempt to see + # whether each corresponding element in the other input arrays + # is within tolerance `tol` + try: + # Disable Mypy warnings on the following line, since errors + # will be handled with the try statement + if abs(x - item1) > tol: # type: ignore + return False + + except TypeError: + return False + + return True + + # If inputs are strings, directly compare them (requiring strings to be + # identical to consider the inputs equal) + elif isinstance(item1, str): + for x in items: + if not isinstance(x, str) or item1 != x: + return False + return True + + else: + # Verify that inputs are of expected types + if not all(map(lambda x: isinstance(x, (list, tuple, np.ndarray)), + [item1, *items])): return False - return True + # Verify that all inputs have equal length + if not is_len_equal(item1, *items): + return False + + # Check whether each sub-array's elements are equal (recursively) + for i, x in enumerate(item1): + + # Disable Mypy warnings on the following line, since we've + # already checked that the lengths of `x` and all elements + # in `items` are equal + if not is_array_equal(x, *[item[i] for item in items], # type: ignore + tol=tol): + return False + + return True diff --git a/tests/__init__.py b/tests/__init__.py index fab85a8..ae5cc46 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,8 @@ +import io import pathlib import os import shutil +import sys # Define variables available to all tests @@ -10,6 +12,17 @@ # Define context managers to facilitate testing +class CapturePrint: + """Captures text printed to the terminal when running commands""" + def __enter__(self): + self.terminal_stdout = io.StringIO() + sys.stdout = self.terminal_stdout + return self.terminal_stdout + + def __exit__(self, *args, **kwargs): + sys.stdout = sys.__stdout__ + + class CreateTempTestDir: """Sets up temporary folder for reading/writing test files""" def __enter__(self): diff --git a/tests/arrays/functions/test_equality.py b/tests/arrays/functions/test_equality.py index ff51f19..49ed0f2 100644 --- a/tests/arrays/functions/test_equality.py +++ b/tests/arrays/functions/test_equality.py @@ -1,8 +1,9 @@ import unittest +import warnings import numpy as np -from pyxx.arrays import np_array_equal +from pyxx.arrays import is_array_equal class Test_NumPyListEqual(unittest.TestCase): @@ -20,61 +21,248 @@ def setUp(self): def test_equal(self): # Verifies that arrays of equal shape and values are evaluated as equal with self.subTest(args=2): - self.assertTrue(np_array_equal(self.array, self.array)) + self.assertTrue(is_array_equal(self.array, self.array)) with self.subTest(args=3): - self.assertTrue(np_array_equal(self.array, self.array, self.array)) + self.assertTrue(is_array_equal(self.array, self.array, self.array)) with self.subTest(args=4): - self.assertTrue(np_array_equal(self.array, self.array, self.array, self.array)) + self.assertTrue(is_array_equal(self.array, self.array, self.array, self.array)) def test_unequal_shape(self): # Verifies that arrays of different shape are evaluated as not equal with self.subTest(args=2): - self.assertFalse(np_array_equal(self.array, self.array_uneq_shape)) + self.assertFalse(is_array_equal(self.array, self.array_uneq_shape)) with self.subTest(args=3): - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array_uneq_shape, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array_uneq_shape, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array_uneq_shape)) with self.subTest(args=4): - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array_uneq_shape, self.array, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array_uneq_shape, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array_uneq_shape, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array, self.array_uneq_shape)) + with self.subTest(comment='number_and_list'): + self.assertFalse(is_array_equal( + [1, [2, 3, 4]], [[1], [2, 3, 4]])) + self.assertFalse(is_array_equal( + [[1], [2, 3, 4]], [1, [2, 3, 4]])) + self.assertFalse(is_array_equal( + [[1, 2, 3], [4, [5, 6, 7]]], [[1, 2, 3], [4, [5, 7]]] + )) + + with self.subTest(comment='string_and_char_array'): + self.assertFalse(is_array_equal( + 'myString', ['m', 'y', 'S', 't', 'r', 'i', 'n', 'g'])) + self.assertFalse(is_array_equal( + ['m', 'y', 'S', 't', 'r', 'i', 'n', 'g'], 'myString')) + def test_unequal_values(self): # Verifies that arrays with different values are evaluated as not equal with self.subTest(args=2): - self.assertFalse(np_array_equal(self.array, self.array_uneq_val)) + self.assertFalse(is_array_equal(self.array, self.array_uneq_val)) with self.subTest(args=3): - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array_uneq_val, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array_uneq_val, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array_uneq_val)) with self.subTest(args=4): - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array_uneq_val, self.array, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array_uneq_val, self.array, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array_uneq_val, self.array)) - self.assertFalse(np_array_equal( + self.assertFalse(is_array_equal( self.array, self.array, self.array, self.array_uneq_val)) def test_tolerance(self): # Verifies that setting tolerance for equality functions as expected - self.assertTrue( - np_array_equal(self.array, self.array_uneq_val, tol=0.000011)) + diff = np.abs(self.array_uneq_val - self.array) + + with self.subTest(error='positive'): + self.assertTrue( + is_array_equal(self.array, self.array + diff, tol=0.000011)) + + self.assertFalse( + is_array_equal(self.array, self.array + 2*diff, tol=0.000011)) + + with self.subTest(error='negative'): + self.assertTrue( + is_array_equal(self.array, self.array - diff, tol=0.000011)) + + self.assertFalse( + is_array_equal(self.array, self.array - 2*diff, tol=0.000011)) + + def test_list(self): + # Verifies that equality can be checked for lists + with self.subTest(result='equal'): + self.assertTrue(is_array_equal( + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5] + )) + + with self.subTest(result='unequal_shape'): + self.assertFalse(is_array_equal( + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323], + [0, 4.2, 9, -0.323, 1e5] + )) + + with self.subTest(result='unequal_value'): + self.assertFalse(is_array_equal( + [0, 4.2, 9, -0.3233, 1e5], + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5] + )) + + def test_tuple(self): + # Verifies that equality can be checked for tuples + with self.subTest(result='equal'): + self.assertTrue(is_array_equal( + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5) + )) + + with self.subTest(result='unequal_shape'): + self.assertFalse(is_array_equal( + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323), + (0, 4.2, 9, -0.323, 1e5) + )) + + with self.subTest(result='unequal_value'): + self.assertFalse(is_array_equal( + (0, 4.2, 9, -0.3233, 1e5), + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5) + )) + + def test_number(self): + # Verifies that equality can be checked for numbers + with self.subTest(result='equal'): + self.assertTrue(is_array_equal(3.142, 3.142)) + self.assertTrue(is_array_equal(3.142, 3.141, tol=0.0011)) + self.assertTrue(is_array_equal(-5, -5)) + self.assertTrue(is_array_equal(-5.0, -5)) + + with self.subTest(result='unequal'): + self.assertFalse(is_array_equal(3.142, 3.1)) + self.assertFalse(is_array_equal(-5, 5)) + + def test_string(self): + # Verifies that equality can be checked for strings + with self.subTest(result='equal_str'): + self.assertTrue(is_array_equal('myString', 'myString')) + self.assertTrue(is_array_equal('ab', 'ab', 'ab')) + self.assertTrue(is_array_equal('ab\n', 'ab\n', 'ab\n', 'ab\n')) + + with self.subTest(result='equal_str_array'): + self.assertTrue(is_array_equal( + ['myString', 'a', ['bc', 'd']], ['myString', 'a', ['bc', 'd']])) + + with self.subTest(result='unequal_str'): + self.assertFalse(is_array_equal('myStrinG', 'myString')) + self.assertFalse(is_array_equal('abc', 'ab', 'ab')) + self.assertFalse(is_array_equal('ab', 'abc', 'ab')) + self.assertFalse(is_array_equal('ab', 'ab', 'abc')) + self.assertFalse(is_array_equal('ab\nc', 'ab\n', 'ab\n', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\nc', 'ab\n', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\n', 'ab\nc', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\n', 'ab\n', 'ab\nc')) + + with self.subTest(result='unequal_str_array'): + self.assertFalse(is_array_equal( + ['myString', 'a', ['b', 'c', 'd']], ['myString', 'a', ['bc', 'd']])) + + self.assertFalse(is_array_equal( + ['myString', 'a', ['bc', 'e']], ['myString', 'a', ['bc', 'd']])) + + def test_mixed_type_numeric(self): + # Verifies that equality can be checked for mixed types of arrays + + # Disable display of NumPy warnings when creating "ragged" array + warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning) + + with self.subTest(dim=1): + self.assertTrue(is_array_equal( + [3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], + (3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), + np.array([3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13]) + )) + + with self.subTest(dim=2): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [4, 3, 2, 1]]) + )) + + with self.subTest(dim=3): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [[3, 6], 9], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), [np.array([3, 6]), np.array(9)], [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), ([3, 6], 9), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], ((3, 6), 9), [4, 3, 2, 1]]) + )) + + def test_mixed_type_numeric_str(self): + # Verifies that equality can be checked for mixed types of arrays + # with both numbers and strings + + # Disable display of NumPy warnings when creating "ragged" array + warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning) + + with self.subTest(comment='no_numpy_array'): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), (4, 3, 2, 1)) + )) + + with self.subTest(comment='numpy_no_dtype_object'): + self.assertFalse(is_array_equal(np.array([1, 'a']), [1, 'a'])) + + with self.subTest(comment='numpy_dtype_object'): + self.assertTrue(is_array_equal(np.array([1, 'a'], dtype=object), [1, 'a'])) + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + dtype=object) + )) + + def test_incompatible_type(self): + # Verifies that arrays are assessed as not equal if they require + # comparing types that are not exactly equal or cannot be subtracted + with self.subTest(comment='int_str'): + self.assertFalse(is_array_equal([1.0, (2, 3)], ('1', [2, 3]))) + + with self.subTest(comment='number_type'): + self.assertFalse(is_array_equal(int, 3, 0)) + self.assertFalse(is_array_equal([1, 2, 3], [1, 2, float])) + + def test_empty(self): + # Verifies that empty arrays are evaluated as equal + self.assertTrue(is_array_equal([], [])) + self.assertTrue(is_array_equal((), ())) + self.assertTrue(is_array_equal(np.array([]), np.array([]))) + self.assertTrue(is_array_equal(np.array([]), [], ())) From 76da7d432c9743e377ac57939f7a7bad42fea156 Mon Sep 17 00:00:00 2001 From: Nathan Hess <63890205+nathan-hess@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:23:02 -0400 Subject: [PATCH 10/10] Modify `pyxx.arrays.is_array_equal()` to make function more general Revised function logic so that it is capable of handling a wider variety of input types --- pyxx/arrays/functions/equality.py | 29 +++++++++++++++---------- tests/arrays/functions/test_equality.py | 13 +++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyxx/arrays/functions/equality.py b/pyxx/arrays/functions/equality.py index 7454e60..609d521 100644 --- a/pyxx/arrays/functions/equality.py +++ b/pyxx/arrays/functions/equality.py @@ -95,6 +95,10 @@ def is_array_equal(item1: Array_or_Number_or_String, # Create list of array(s) to compare with `item1` items = [item2] + list(args) + # Check whether each of the input arguments is an array-like object + is_array = [isinstance(x, (list, tuple, np.ndarray)) + for x in [item1, *items]] + # If inputs are numbers, directly compare them (requiring difference # between numbers to be less than or equal to `tol` to consider the # inputs equal) @@ -121,18 +125,10 @@ def is_array_equal(item1: Array_or_Number_or_String, return True - # If inputs are strings, directly compare them (requiring strings to be - # identical to consider the inputs equal) - elif isinstance(item1, str): - for x in items: - if not isinstance(x, str) or item1 != x: - return False - return True - - else: - # Verify that inputs are of expected types - if not all(map(lambda x: isinstance(x, (list, tuple, np.ndarray)), - [item1, *items])): + # If inputs are array-like objects, compare their contents + elif any(is_array): + # Verify that inputs are array-like objects + if not all(is_array): return False # Verify that all inputs have equal length @@ -150,3 +146,12 @@ def is_array_equal(item1: Array_or_Number_or_String, return False return True + + else: + # Inputs are not numbers or array-like objects, so try to directly + # compare them. This allows strings, user-defined classes/types, + # or other objects to be compared + for x in items: + if item1 != x: + return False + return True diff --git a/tests/arrays/functions/test_equality.py b/tests/arrays/functions/test_equality.py index 49ed0f2..b7b72ae 100644 --- a/tests/arrays/functions/test_equality.py +++ b/tests/arrays/functions/test_equality.py @@ -162,6 +162,10 @@ def test_number(self): self.assertTrue(is_array_equal(-5, -5)) self.assertTrue(is_array_equal(-5.0, -5)) + with self.subTest(result='equal_numpy'): + self.assertTrue(is_array_equal(-5.0, np.array(-5))) + self.assertTrue(is_array_equal(-5.0, np.array(-5), np.int32(-5))) + with self.subTest(result='unequal'): self.assertFalse(is_array_equal(3.142, 3.1)) self.assertFalse(is_array_equal(-5, 5)) @@ -260,6 +264,15 @@ def test_incompatible_type(self): self.assertFalse(is_array_equal(int, 3, 0)) self.assertFalse(is_array_equal([1, 2, 3], [1, 2, float])) + def test_unspecified_type(self): + # Verifies that objects that are evaluated as equal (even if not + # numbers or strings) can be compared + with self.subTest(comment='single_value'): + self.assertTrue(is_array_equal(int, int)) + + with self.subTest(comment='array'): + self.assertTrue(is_array_equal([int, float], [int, float])) + def test_empty(self): # Verifies that empty arrays are evaluated as equal self.assertTrue(is_array_equal([], []))