From 1adde8f0eeb703ea61d5af2b4a9b65deefb6b9a4 Mon Sep 17 00:00:00 2001 From: Bo Bayles Date: Mon, 20 Nov 2017 22:44:19 -0600 Subject: [PATCH] Add consecutive_groups() --- docs/api.rst | 1 + more_itertools/more.py | 38 +++++++++++++++++++++++++++++++ more_itertools/tests/test_more.py | 33 +++++++++++++++++++++++++++ setup.cfg | 2 +- 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 4138bfec..8079b8cf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -124,6 +124,7 @@ These tools return summarized or aggregated data from an iterable. .. autofunction:: one .. autofunction:: unique_to_each .. autofunction:: locate +.. autofunction:: consecutive_groups ---- diff --git a/more_itertools/more.py b/more_itertools/more.py index 1ba4d625..82fe0fd6 100644 --- a/more_itertools/more.py +++ b/more_itertools/more.py @@ -29,6 +29,7 @@ 'chunked', 'collapse', 'collate', + 'consecutive_groups', 'consumer', 'count_cycle', 'distinct_permutations', @@ -1530,3 +1531,40 @@ def islice_extended(iterable, *args): for item in cache[i::step]: yield item + + +def consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + For finding runs of adjacent letters, try using the :meth:`index` method + of a string of letters: + + >>> from string import ascii_lowercase + >>> iterable = 'abcdfgilmnop' + >>> ordering = ascii_lowercase.index + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + """ + for k, g in groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(itemgetter(1), g) diff --git a/more_itertools/tests/test_more.py b/more_itertools/tests/test_more.py index 333bb059..8a339c60 100644 --- a/more_itertools/tests/test_more.py +++ b/more_itertools/tests/test_more.py @@ -1429,3 +1429,36 @@ def test_all(self): def test_zero_step(self): with self.assertRaises(ValueError): list(mi.islice_extended([1, 2, 3], 0, 1, 0)) + + +class ConsecutiveGroupsTest(TestCase): + def test_numbers(self): + iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7] + actual = [list(g) for g in mi.consecutive_groups(iterable)] + expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]] + self.assertEqual(actual, expected) + + def test_custom_ordering(self): + iterable = ['1', '10', '11', '20', '21', '22', '30', '31'] + ordering = lambda x: int(x) + actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] + expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']] + self.assertEqual(actual, expected) + + def test_exotic_ordering(self): + iterable = [ + ('a', 'b', 'c', 'd'), + ('a', 'c', 'b', 'd'), + ('a', 'c', 'd', 'b'), + ('a', 'd', 'b', 'c'), + ('d', 'b', 'c', 'a'), + ('d', 'c', 'a', 'b'), + ] + ordering = list(permutations('abcd')).index + actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)] + expected = [ + [('a', 'b', 'c', 'd')], + [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')], + [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')], + ] + self.assertEqual(actual, expected) diff --git a/setup.cfg b/setup.cfg index 9ae44c7d..8e57c310 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [flake8] exclude = ./docs/conf.py -ignore = E731, F999 +ignore = E731, E741, F999