diff --git a/karaokatalog/util/__init__.py b/karaokatalog/util/__init__.py new file mode 100644 index 0000000..0176f57 --- /dev/null +++ b/karaokatalog/util/__init__.py @@ -0,0 +1,4 @@ +""" +Utility functions completely independent of the root project (i.e., you could copy +them to any other code project and they'd immediately work). +""" diff --git a/karaokatalog/util/get_equivalence_classes.py b/karaokatalog/util/get_equivalence_classes.py new file mode 100644 index 0000000..5abadd7 --- /dev/null +++ b/karaokatalog/util/get_equivalence_classes.py @@ -0,0 +1,32 @@ +from collections.abc import Iterable, Sequence, Callable + +type EquivalenceClass[T] = Sequence[T] + + +def get_equivalence_classes[T]( + items: Iterable[T], is_equivalent: Callable[[T, T], bool] +) -> Sequence[EquivalenceClass[T]]: + """ + Partition an iterable of items into equivalence classes under the given equivalence relation. + The order of `items` is kept within the equivalence classes. + + >>> from math import floor + >>> get_equivalence_classes([1.0, 2.1, 1.1, 1.0, 2.2, 2.1, 3.3], lambda a, b: floor(a) == floor(b)) + ((1.0, 1.1, 1.0), (2.1, 2.2, 2.1), (3.3,)) + """ + equivalence_classes: list[list[T]] = [] + + for item in items: + # Check if the item belongs into an already existing equivalence class + for equivalence_class in equivalence_classes: + # Pick an arbitrary representative of the equivalence class (we are allowed to do this + # because it is an equivalence class) + representative = equivalence_class[0] + if is_equivalent(item, representative): + equivalence_class.append(item) + break + else: + # This item forms a new equivalence class, so we create one containing only the item and append it + equivalence_classes.append([item]) + + return tuple(tuple(equivalence_class) for equivalence_class in equivalence_classes)