Skip to content

Commit

Permalink
Add itertools recipe for directly finding the n-th combination (GH-5161
Browse files Browse the repository at this point in the history
…) (#5174)

(cherry picked from commit d37258d)
  • Loading branch information
miss-islington authored and rhettinger committed Jan 13, 2018
1 parent 29b1aff commit cf4cd4b
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 0 deletions.
23 changes: 23 additions & 0 deletions Doc/library/itertools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,29 @@ which incur interpreter overhead.
indices = sorted(random.randrange(n) for i in range(r))
return tuple(pool[i] for i in indices)

def nth_combination(iterable, r, index):
'Equivalent to list(combinations(iterable, r))[index]'
pool = tuple(iterable)
n = len(pool)
if r < 0 or r > n:
raise ValueError
c = 1
k = min(r, n-r)
for i in range(1, k+1):
c = c * (n - k + i) // i
if index < 0:
index += c
if index < 0 or index >= c:
raise IndexError
result = []
while r:
c, n, r = c*r//n, n-1, r-1
while index >= c:
index -= c
c, n = c*(n-r)//n, n-1
result.append(pool[-1-n])
return tuple(result)

Note, many of the above recipes can be optimized by replacing global lookups
with local variables defined as default values. For example, the
*dotproduct* recipe can be written as::
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_itertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2229,6 +2229,30 @@ def test_permutations_sizeof(self):
... # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x
... return next(filter(pred, iterable), default)
>>> def nth_combination(iterable, r, index):
... 'Equivalent to list(combinations(iterable, r))[index]'
... pool = tuple(iterable)
... n = len(pool)
... if r < 0 or r > n:
... raise ValueError
... c = 1
... k = min(r, n-r)
... for i in range(1, k+1):
... c = c * (n - k + i) // i
... if index < 0:
... index += c
... if index < 0 or index >= c:
... raise IndexError
... result = []
... while r:
... c, n, r = c*r//n, n-1, r-1
... while index >= c:
... index -= c
... c, n = c*(n-r)//n, n-1
... result.append(pool[-1-n])
... return tuple(result)
This is not part of the examples but it tests to make sure the definitions
perform as purported.
Expand Down Expand Up @@ -2312,6 +2336,15 @@ def test_permutations_sizeof(self):
>>> first_true('ABC0DEF1', '9', str.isdigit)
'0'
>>> population = 'ABCDEFGH'
>>> for r in range(len(population) + 1):
... seq = list(combinations(population, r))
... for i in range(len(seq)):
... assert nth_combination(population, r, i) == seq[i]
... for i in range(-len(seq), 0):
... assert nth_combination(population, r, i) == seq[i]
"""

__test__ = {'libreftest' : libreftest}
Expand Down

0 comments on commit cf4cd4b

Please sign in to comment.