diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ebc47a457e74ec618c78b6499200bae3abbed7..a657417b4f7f551691736f43566fda84e0483691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # v2.7.15 -* _TBA_ +* added `include_same_pairs` to `half_cartesian` # v2.7.14 diff --git a/satella/__init__.py b/satella/__init__.py index af4d027b6f15c43c0df1194d91f17261c1b0374b..c0072c611c5712f63501b206d0df83c008d5aad0 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.7.15_a1' +__version__ = '2.7.15' diff --git a/satella/coding/sequences/sequences.py b/satella/coding/sequences/sequences.py index b9cd799706351afeb84f12bbd3888d82a0f6b057..bec7676c89c48722f32fb59faad3999f614ef0bc 100644 --- a/satella/coding/sequences/sequences.py +++ b/satella/coding/sequences/sequences.py @@ -1 +1 @@ -import typing as tp __all__ = ['is_last', 'add_next', 'half_cartesian', 'group_quantity'] T = tp.TypeVar('T') U = tp.TypeVar('U') # shamelessly copied from https://medium.com/better-programming/is-this-the-last-element-of-my-python-for-loop-784f5ff90bb5 def is_last(lst: tp.Iterable[T]) -> tp.Generator[tp.Tuple[bool, T], None, None]: """ Return every element of the list, alongside a flag telling is this the last element. Use like: >>> for is_last, element in is_last(my_list): >>> if is_last: >>> ... :param lst: list to iterate thru :return: a generator returning (bool, T) """ iterable = iter(lst) ret_var = next(iterable) for val in iterable: yield False, ret_var ret_var = val yield True, ret_var def add_next(lst: tp.Iterable[T], wrap_over: bool = False, skip_last: bool = False) -> tp.Iterator[ tp.Tuple[T, tp.Optional[T]]]: """ Yields a 2-tuple of given iterable, presenting the next element as second element of the tuple. The last element will be the last element alongside with a None, if wrap_over is False, or the first element if wrap_over was True Example: >>> list(add_next([1, 2, 3, 4, 5])) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] >>> list(add_next([1, 2, 3, 4, 5], True)) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)] :param lst: iterable to iterate over :param wrap_over: whether to attach the first element to the pair of the last element instead of None :param skip_last: if this is True, then last element, alongside with a None, won't be output """ iterator = iter(lst) try: first_val = prev_val = next(iterator) except StopIteration: return for val in iterator: yield prev_val, val prev_val = val if wrap_over: yield prev_val, first_val else: if not skip_last: yield prev_val, None def half_cartesian(seq1: tp.Iterable[T]) -> tp.Iterator[tp.Tuple[T, T]]: """ Generate half of the Cartesian product of both sequences. Useful when you have a commutative operation that you'd like to execute on both elements (eg. checking for collisions). Example: >>> list(half_cartesian([1, 2, 3], [1, 2, 3])) == \ >>> [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] :param seq1: First sequence :param seq2: Second sequence """ for i, elem1 in enumerate(seq1): for j, elem2 in enumerate(seq1): if j >= i: yield elem1, elem2 def group_quantity(length: int, seq: tp.Iterable[T]) -> tp.Generator[tp.List[T], None, None]: """ Slice an iterable into lists containing at most len entries. Eg. >>> assert list(group_quantity(3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) == [[1, 2, 3], [4, 5, 6], >>> [7, 8, 9], [10]] :param length: length for the returning sequences :param seq: sequence to split """ entries = [] for elem in seq: if len(entries) < length: entries.append(elem) else: yield entries entries = [elem] if entries: yield entries \ No newline at end of file +import typing as tp __all__ = ['is_last', 'add_next', 'half_cartesian', 'group_quantity'] T = tp.TypeVar('T') U = tp.TypeVar('U') # shamelessly copied from https://medium.com/better-programming/is-this-the-last-element-of-my-python-for-loop-784f5ff90bb5 def is_last(lst: tp.Iterable[T]) -> tp.Generator[tp.Tuple[bool, T], None, None]: """ Return every element of the list, alongside a flag telling is this the last element. Use like: >>> for is_last, element in is_last(my_list): >>> if is_last: >>> ... :param lst: list to iterate thru :return: a generator returning (bool, T) """ iterable = iter(lst) ret_var = next(iterable) for val in iterable: yield False, ret_var ret_var = val yield True, ret_var def add_next(lst: tp.Iterable[T], wrap_over: bool = False, skip_last: bool = False) -> tp.Iterator[ tp.Tuple[T, tp.Optional[T]]]: """ Yields a 2-tuple of given iterable, presenting the next element as second element of the tuple. The last element will be the last element alongside with a None, if wrap_over is False, or the first element if wrap_over was True Example: >>> list(add_next([1, 2, 3, 4, 5])) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] >>> list(add_next([1, 2, 3, 4, 5], True)) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)] :param lst: iterable to iterate over :param wrap_over: whether to attach the first element to the pair of the last element instead of None :param skip_last: if this is True, then last element, alongside with a None, won't be output """ iterator = iter(lst) try: first_val = prev_val = next(iterator) except StopIteration: return for val in iterator: yield prev_val, val prev_val = val if wrap_over: yield prev_val, first_val else: if not skip_last: yield prev_val, None def half_cartesian(seq1: tp.Iterable[T], include_same_pairs: bool = True) -> tp.Iterator[tp.Tuple[T, T]]: """ Generate half of the Cartesian product of both sequences. Useful when you have a commutative operation that you'd like to execute on both elements (eg. checking for collisions). Example: >>> list(half_cartesian([1, 2, 3], [1, 2, 3])) == \ >>> [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] :param seq1: First sequence :param seq2: Second sequence :param include_same_pairs: if True, then pairs returning two of the same objects will be returned. Ie. if False, the following will be true: >>> list(half_cartesian([1, 2, 3], [1, 2, 3], include_same_pairs=False)) == \ >>> [(1, 2), (1, 3), (2, 3)] """ for i, elem1 in enumerate(seq1): for j, elem2 in enumerate(seq1): if include_same_pairs: if j >= i: yield elem1, elem2 else: if j > i: yield elem1, elem2 def group_quantity(length: int, seq: tp.Iterable[T]) -> tp.Generator[tp.List[T], None, None]: """ Slice an iterable into lists containing at most len entries. Eg. >>> assert list(group_quantity(3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) == [[1, 2, 3], [4, 5, 6], >>> [7, 8, 9], [10]] :param length: length for the returning sequences :param seq: sequence to split """ entries = [] for elem in seq: if len(entries) < length: entries.append(elem) else: yield entries entries = [elem] if entries: yield entries \ No newline at end of file diff --git a/tests/test_coding/test_sequences.py b/tests/test_coding/test_sequences.py index fa2b2e559868da263887ccdd53a3eb731fbae996..0784b87ca338b5c26f738f5bc0f0a5aaee6dde79 100644 --- a/tests/test_coding/test_sequences.py +++ b/tests/test_coding/test_sequences.py @@ -58,6 +58,11 @@ class TestSequences(unittest.TestCase): b = {(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)} self.assertEqual(a, b) + def test_half_cartesian_include_same_pairs_false(self): + a = set(half_cartesian([1, 2, 3], include_same_pairs=False)) + b = {(1, 2), (1, 3), (2, 3)} + self.assertEqual(a, b) + def test_add_next(self): self.assertEqual(list(add_next([1, 2, 3, 4, 5])), [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)])