Skip to content

Commit

Permalink
Merge pull request #169 from erikrose/consecutive-groups-rebased
Browse files Browse the repository at this point in the history
Add consecutive_groups()
  • Loading branch information
bbayles committed Nov 24, 2017
2 parents b762050 + 1adde8f commit e08c285
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/api.rst
Expand Up @@ -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

----

Expand Down
38 changes: 38 additions & 0 deletions more_itertools/more.py
Expand Up @@ -29,6 +29,7 @@
'chunked',
'collapse',
'collate',
'consecutive_groups',
'consumer',
'count_cycle',
'distinct_permutations',
Expand Down Expand Up @@ -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)
33 changes: 33 additions & 0 deletions more_itertools/tests/test_more.py
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion setup.cfg
@@ -1,3 +1,3 @@
[flake8]
exclude = ./docs/conf.py
ignore = E731, F999
ignore = E731, E741, F999

0 comments on commit e08c285

Please sign in to comment.