Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into run-length
Browse files Browse the repository at this point in the history
  • Loading branch information
bbayles committed Dec 1, 2017
2 parents 5ad28b4 + fc40483 commit 9f3bd42
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
@@ -0,0 +1,2 @@
more_itertools/more.py merge=union
more_itertools/tests/test_more.py merge=union
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -29,3 +29,4 @@ _build

# IDE files
.idea
.vscode
4 changes: 3 additions & 1 deletion docs/api.rst
Expand Up @@ -126,6 +126,7 @@ These tools return summarized or aggregated data from an iterable.
.. autofunction:: unique_to_each
.. autofunction:: locate
.. autofunction:: consecutive_groups
.. autofunction:: exactly_n
.. autoclass:: run_length

----
Expand Down Expand Up @@ -213,12 +214,13 @@ Others
.. autofunction:: numeric_range(start, stop, step)
.. autofunction:: side_effect
.. autofunction:: iterate
.. autofunction:: difference(iterable, func=operator.sub)

----

**Itertools recipes**

.. autofunction:: consume
.. autofunction:: accumulate
.. autofunction:: accumulate(iterable, func=operator.add)
.. autofunction:: tabulate
.. autofunction:: repeatfunc
60 changes: 59 additions & 1 deletion more_itertools/more.py
Expand Up @@ -14,7 +14,7 @@
takewhile,
tee
)
from operator import itemgetter, lt, gt
from operator import itemgetter, lt, gt, sub
from sys import maxsize, version_info

from six import binary_type, string_types, text_type
Expand All @@ -32,9 +32,11 @@
'consecutive_groups',
'consumer',
'count_cycle',
'difference',
'distinct_permutations',
'distribute',
'divide',
'exactly_n',
'first',
'groupby_transform',
'ilen',
Expand Down Expand Up @@ -1591,6 +1593,44 @@ def consecutive_groups(iterable, ordering=lambda x: x):
yield map(itemgetter(1), g)


def difference(iterable, func=sub):
"""By default, compute the first difference of *iterable* using
:func:`operator.sub`.
>>> iterable = [0, 1, 3, 6, 10]
>>> list(difference(iterable))
[0, 1, 2, 3, 4]
This is the opposite of :func:`accumulate`'s default behavior:
>>> from more_itertools import accumulate
>>> iterable = [0, 1, 2, 3, 4]
>>> list(accumulate(iterable))
[0, 1, 3, 6, 10]
>>> list(difference(accumulate(iterable)))
[0, 1, 2, 3, 4]
By default *func* is :func:`operator.sub`, but other functions can be
specified. They will be applied as follows::
A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ...
For example, to do progressive division:
>>> iterable = [1, 2, 6, 24, 120] # Factorial sequence
>>> func = lambda x, y: x // y
>>> list(difference(iterable, func))
[1, 2, 3, 4, 5]
"""
a, b = tee(iterable)
try:
item = next(b)
except StopIteration:
return iter([])
return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b)))


class seekable(object):
"""Wrap an iterator to allow for seeking backward and forward. This
progressively caches the items in the source iterable so they can be
Expand Down Expand Up @@ -1687,3 +1727,21 @@ def encode(iterable):
@staticmethod
def decode(iterable):
return chain.from_iterable(repeat(k, n) for k, n in iterable)


def exactly_n(iterable, n, predicate=bool):
"""Return ``True`` if exactly ``n`` items in the iterable are ``True``
according to the *predicate* function.
>>> exactly_n([True, True, False], 2)
True
>>> exactly_n([True, True, False], 1)
False
>>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3)
True
The iterable will be advanced until ``n + 1`` truthy items are encountered,
so avoid calling it on infinite iterables.
"""
return len(take(n + 1, filter(predicate, iterable))) == n
52 changes: 51 additions & 1 deletion more_itertools/tests/test_more.py
Expand Up @@ -6,7 +6,7 @@
from functools import reduce
from io import StringIO
from itertools import chain, count, groupby, permutations, product, repeat
from operator import itemgetter
from operator import add, itemgetter
from unittest import TestCase

from six.moves import filter, range, zip
Expand Down Expand Up @@ -1474,6 +1474,32 @@ def test_exotic_ordering(self):
self.assertEqual(actual, expected)


class DifferenceTest(TestCase):
def test_normal(self):
iterable = [10, 20, 30, 40, 50]
actual = list(mi.difference(iterable))
expected = [10, 10, 10, 10, 10]
self.assertEqual(actual, expected)

def test_custom(self):
iterable = [10, 20, 30, 40, 50]
actual = list(mi.difference(iterable, add))
expected = [10, 30, 50, 70, 90]
self.assertEqual(actual, expected)

def test_roundtrip(self):
original = list(range(100))
accumulated = mi.accumulate(original)
actual = list(mi.difference(accumulated))
self.assertEqual(actual, original)

def test_one(self):
self.assertEqual(list(mi.difference([0])), [0])

def test_empty(self):
self.assertEqual(list(mi.difference([])), [])


class SeekableTest(TestCase):
def test_exhaustion_reset(self):
iterable = [str(n) for n in range(10)]
Expand Down Expand Up @@ -1531,3 +1557,27 @@ def test_decode(self):
actual = ''.join(mi.run_length.decode(iterable))
expected = 'ddddcccbba'
self.assertEqual(actual, expected)


class ExactlyNTests(TestCase):
"""Tests for ``exactly_n()``"""

def test_true(self):
"""Iterable has ``n`` ``True`` elements"""
self.assertTrue(mi.exactly_n([True, False, True], 2))
self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3))
self.assertTrue(mi.exactly_n([False, False], 0))
self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10))

def test_false(self):
"""Iterable does not have ``n`` ``True`` elements"""
self.assertFalse(mi.exactly_n([True, False, False], 2))
self.assertFalse(mi.exactly_n([True, True, False], 1))
self.assertFalse(mi.exactly_n([False], 1))
self.assertFalse(mi.exactly_n([True], -1))
self.assertFalse(mi.exactly_n(repeat(True), 100))

def test_empty(self):
"""Return ``True`` if the iterable is empty and ``n`` is 0"""
self.assertTrue(mi.exactly_n([], 0))
self.assertFalse(mi.exactly_n([], 1))

0 comments on commit 9f3bd42

Please sign in to comment.