Compare two items without regard to order.

    The following rules are used to determine equivalence:
        * Items that are not of the same type can be equivalent only when ``type_insensitive=True``.
        * Mapping objects are equal iff the keys in each item exist in both items and have
          the same value (with the same ``insensitive_comparison``).
        * Other containers except for strings are equal iff every element in each item exists
          in both items (duplicate items must be present the same number of times).
        * Containers must be ``Iterable`` to be compared in this way.
        * Non-containers are equivalent if the equality operator returns ``True``.
        * Strings are treated as non-containers when ``string_insensitive=False``,
          and are treated as containers when ``string_insensitive=True``. When treated as
          containers, each (case-insensitive) character is treated as an element and
          whitespace is ignored.
        * If the items are in different categories above, they are never equivalent,
          even when ``type_insensitive=True``.

    Arguments:
        item1 (any): The first item to compare.
        item2 (any): The second item to compare.
        type_insensitive (bool): When ``True``, items of a different type are not automatically
                unequivalent. When ``False``, items must be the same type to be equivalent.
                **Default**: ``False``.
        string_insensitive (bool): When ``True``, strings are treated as containers, with each
                character being one element in the container.
                When ``False``, strings are treated as non-containers and compared directly.
                **Default**: ``False``.

    Returns:
        bool: ``True`` iff the two items are equivalent (see above).
                ``False`` otherwise.

def insensitive_comparison(item1, item2, type_insensitive=False, string_insensitive=False):
    """Compare two items without regard to order.

    The following rules are used to determine equivalence:
        * Items that are not of the same type can be equivalent only when ``type_insensitive=True``.
        * Mapping objects are equal iff the keys in each item exist in both items and have
          the same value (with the same ``insensitive_comparison``).
        * Other containers except for strings are equal iff every element in each item exists
          in both items (duplicate items must be present the same number of times).
        * Containers must be ``Iterable`` to be compared in this way.
        * Non-containers are equivalent if the equality operator returns ``True``.
        * Strings are treated as non-containers when ``string_insensitive=False``,
          and are treated as containers when ``string_insensitive=True``. When treated as
          containers, each (case-insensitive) character is treated as an element and
          whitespace is ignored.
        * If the items are in different categories above, they are never equivalent,
          even when ``type_insensitive=True``.

    Arguments:
        item1 (any): The first item to compare.
        item2 (any): The second item to compare.
        type_insensitive (bool): When ``True``, items of a different type are not automatically
                unequivalent. When ``False``, items must be the same type to be equivalent.
                **Default**: ``False``.
        string_insensitive (bool): When ``True``, strings are treated as containers, with each
                character being one element in the container.
                When ``False``, strings are treated as non-containers and compared directly.
                **Default**: ``False``.

    Returns:
        bool: ``True`` iff the two items are equivalent (see above).
                ``False`` otherwise.
    """
    # If type-sensitive, check types
    if not type_insensitive and type(item1) != type(item2):
        return False

    # Handle Mapping objects (dict)
    if isinstance(item1, Mapping):
        # Second item must be Mapping
        if not isinstance(item2, Mapping):
            return False
        # Items must have the same number of elements
        if not len(item1) == len(item2):
            return False
        # Keys must be the same
        if not insensitive_comparison(list(item1.keys()), list(item2.keys()),
                                      type_insensitive=True):
            return False
        # Each key's value must be the same
        # We can just check item1.items because the keys are the same
        for key, val in item1.items():
            if not insensitive_comparison(item1[key], item2[key],
                                          type_insensitive=type_insensitive,
                                          string_insensitive=string_insensitive):
                return False
        # Keys and values are the same
        return True
    # Handle strings
    elif isinstance(item1, str):
        # Second item must be string
        if not isinstance(item2, str):
            return False
        # Items must have the same number of elements (except string_insensitive)
        if not len(item1) == len(item2) and not string_insensitive:
            return False
        # If we're insensitive to case, spaces, and order, compare characters
        if string_insensitive:
            # If the string is one character long, skip additional comparison
            if len(item1) <= 1:
                return item1.lower() == item2.lower()
            # Make strings into containers (lists) and discard whitespace
            item1_list = [c for c in item1.lower() if not c.isspace()]
            item2_list = [c for c in item2.lower() if not c.isspace()]
            # The insensitive args shouldn't matter, but they're here just in case
            return insensitive_comparison(item1_list, item2_list,
                                          type_insensitive=type_insensitive,
                                          string_insensitive=string_insensitive)
        # Otherwise, case and order matter
        else:
            return item1 == item2
    # Handle other Iterable Containers
    elif isinstance(item1, Container) and isinstance(item1, Iterable):
        # Second item must be an Iterable Container
        if not isinstance(item2, Container) or not isinstance(item2, Iterable):
            return False
        # Items must have the same number of elements
        if not len(item1) == len(item2):
            return False
        # Every element in item1 must be in item2, and vice-versa
        # Painfully slow, but unavoidable for deep comparison
        # Each match in item1 removes the corresponding element from item2_copy
        # If they're the same, item2_copy should be empty at the end,
        #   unless a .remove() failed, in which case we have to re-match using item2
        item2_copy = list(deepcopy(item2))
        remove_failed = False
        for elem in item1:
            matched = False
            # Try every element
            for candidate in item2:
                # If comparison succeeds, flag a match, remove match from copy, and dump out
                if insensitive_comparison(elem, candidate,
                                          type_insensitive=type_insensitive,
                                          string_insensitive=string_insensitive):
                    matched = True
                    try:
                        item2_copy.remove(candidate)
                    except ValueError:  # list.remove(x): x not in list
                        remove_failed = True
                    break
            # One failure indicates unequivalence
            if not matched:
                return False
        # If all removes succeeded, we can shortcut checking all item2 elements in item1
        if not remove_failed:
            # If the Containers are equivalent, all elements in item2_copy should be removed
            # Otherwise
            return len(item2_copy) == 0
        # If something failed, we have to verify all of item2
        # We can't assume item2 != item1, because removal is comparative
        else:
            for elem in item2:
                matched = False
                # Try every element
                for candidate in item1:
                    # If comparison succeeds, flag a match, remove match from copy, and dump out
                    if insensitive_comparison(elem, candidate,
                                              type_insensitive=type_insensitive,
                                              string_insensitive=string_insensitive):
                        matched = True
                        break
                # One failure indicates unequivalence
                if not matched:
                    return False
            # All elements have a match
            return True
    # Handle otherwise unhandled type (catchall)
    else:
        return item1 == item2