pythonunit-testingcollections

assertAlmostEqual in Python unit-test for collections of floats


The assertAlmostEqual(x, y) method in Python's unit testing framework tests whether x and y are approximately equal assuming they are floats.

The problem with assertAlmostEqual() is that it only works on floats. I'm looking for a method like assertAlmostEqual() which works on lists of floats, sets of floats, dictionaries of floats, tuples of floats, lists of tuples of floats, sets of lists of floats, etc.

For instance, let x = 0.1234567890, y = 0.1234567891. x and y are almost equal because they agree on each and every digit except for the last one. Therefore self.assertAlmostEqual(x, y) is True because assertAlmostEqual() works for floats.

I'm looking for a more generic assertAlmostEquals() which also evaluates the following calls to True:

Is there such a method or do I have to implement it myself?

Clarifications:


Solution

  • Here's how I've implemented a generic is_almost_equal(first, second) function:

    First, duplicate the objects you need to compare (first and second), but don't make an exact copy: cut the insignificant decimal digits of any float you encounter inside the object.

    Now that you have copies of first and second for which the insignificant decimal digits are gone, just compare first and second using the == operator.

    Let's assume we have a cut_insignificant_digits_recursively(obj, places) function which duplicates obj but leaves only the places most significant decimal digits of each float in the original obj. Here's a working implementation of is_almost_equals(first, second, places):

    from insignificant_digit_cutter import cut_insignificant_digits_recursively
    
    def is_almost_equal(first, second, places):
        '''returns True if first and second equal. 
        returns true if first and second aren't equal but have exactly the same
        structure and values except for a bunch of floats which are just almost
        equal (floats are almost equal if they're equal when we consider only the
        [places] most significant digits of each).'''
        if first == second: return True
        cut_first = cut_insignificant_digits_recursively(first, places)
        cut_second = cut_insignificant_digits_recursively(second, places)
        return cut_first == cut_second
    

    And here's a working implementation of cut_insignificant_digits_recursively(obj, places):

    def cut_insignificant_digits(number, places):
        '''cut the least significant decimal digits of a number, 
        leave only [places] decimal digits'''
        if  type(number) != float: return number
        number_as_str = str(number)
        end_of_number = number_as_str.find('.')+places+1
        if end_of_number > len(number_as_str): return number
        return float(number_as_str[:end_of_number])
    
    def cut_insignificant_digits_lazy(iterable, places):
        for obj in iterable:
            yield cut_insignificant_digits_recursively(obj, places)
    
    def cut_insignificant_digits_recursively(obj, places):
        '''return a copy of obj except that every float loses its least significant 
        decimal digits remaining only [places] decimal digits'''
        t = type(obj)
        if t == float: return cut_insignificant_digits(obj, places)
        if t in (list, tuple, set):
            return t(cut_insignificant_digits_lazy(obj, places))
        if t == dict:
            return {cut_insignificant_digits_recursively(key, places):
                    cut_insignificant_digits_recursively(val, places)
                    for key,val in obj.items()}
        return obj
    

    The code and its unit tests are available here: https://github.com/snakile/approximate_comparator. I welcome any improvement and bug fix.