Skip to content

bpo-4356: Add key parameter to functions in bisect module #11781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
55 changes: 35 additions & 20 deletions Doc/library/bisect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,36 @@ example of the algorithm (the boundary conditions are already right!).
The following functions are provided:


.. function:: bisect_left(a, x, lo=0, hi=len(a))
.. function:: bisect_left(a, x, lo=0, hi=len(a), *, key=None, reverse=False)

Locate the insertion point for *x* in *a* to maintain sorted order.
The parameters *lo* and *hi* may be used to specify a subset of the list
which should be considered; by default the entire list is used. If *x* is
already present in *a*, the insertion point will be before (to the left of)
any existing entries. The return value is suitable for use as the first
parameter to ``list.insert()`` assuming that *a* is already sorted.
which should be considered; by default the entire list is used.

The parameter *key* specifies a function of one argument that is used to
extract a comparison key from each element in *a* and from *x* (for example,
``key=str.lower``). The default value is ``None`` (compare the elements
directly).

.. note::

When specifying a custom *key* function, you should wrap it with
:func:`functools.lru_cache` if the *key* function is not already fast.

The parameter *reverse* is a boolean value. If set to ``True``, the list is
supposed to be sorted in descending order.

If *x* is already present in *a*, the insertion point will be before
(to the left of) any existing entries. The return value is suitable for
use as the first parameter to ``list.insert()`` assuming that *a* is
already sorted according to *key*.

The returned insertion point *i* partitions the array *a* into two halves so
that ``all(val < x for val in a[lo:i])`` for the left side and
``all(val >= x for val in a[i:hi])`` for the right side.

.. function:: bisect_right(a, x, lo=0, hi=len(a))
bisect(a, x, lo=0, hi=len(a))
.. function:: bisect_right(a, x, lo=0, hi=len(a), *, key=None, reverse=False)
bisect(a, x, lo=0, hi=len(a), *, key=None, reverse=False)

Similar to :func:`bisect_left`, but returns an insertion point which comes
after (to the right of) any existing entries of *x* in *a*.
Expand All @@ -44,15 +59,15 @@ The following functions are provided:
that ``all(val <= x for val in a[lo:i])`` for the left side and
``all(val > x for val in a[i:hi])`` for the right side.

.. function:: insort_left(a, x, lo=0, hi=len(a))
.. function:: insort_left(a, x, lo=0, hi=len(a), *, key=None, reverse=False)

Insert *x* in *a* in sorted order. This is equivalent to
``a.insert(bisect.bisect_left(a, x, lo, hi), x)`` assuming that *a* is
already sorted. Keep in mind that the O(log n) search is dominated by
the slow O(n) insertion step.

.. function:: insort_right(a, x, lo=0, hi=len(a))
insort(a, x, lo=0, hi=len(a))
.. function:: insort_right(a, x, lo=0, hi=len(a), *, key=None, reverse=False)
insort(a, x, lo=0, hi=len(a), *, key=None, reverse=False)

Similar to :func:`insort_left`, but inserting *x* in *a* after any existing
entries of *x*.
Expand All @@ -74,37 +89,37 @@ can be tricky or awkward to use for common searching tasks. The following five
functions show how to transform them into the standard lookups for sorted
lists::

def index(a, x):
def index(a, x, *, key=None, reverse=False):
'Locate the leftmost value exactly equal to x'
i = bisect_left(a, x)
i = bisect_left(a, x, key=key)
if i != len(a) and a[i] == x:
return i
raise ValueError

def find_lt(a, x):
def find_lt(a, x, *, key=None, reverse=False):
'Find rightmost value less than x'
i = bisect_left(a, x)
i = bisect_left(a, x, key=key)
if i:
return a[i-1]
raise ValueError

def find_le(a, x):
def find_le(a, x, *, key=None, reverse=False):
'Find rightmost value less than or equal to x'
i = bisect_right(a, x)
i = bisect_right(a, x, key=key)
if i:
return a[i-1]
raise ValueError

def find_gt(a, x):
def find_gt(a, x, *, key=None, reverse=False):
'Find leftmost value greater than x'
i = bisect_right(a, x)
i = bisect_right(a, x, key=key)
if i != len(a):
return a[i]
raise ValueError

def find_ge(a, x):
def find_ge(a, x, *, key=None, reverse=False):
'Find leftmost item greater than or equal to x'
i = bisect_left(a, x)
i = bisect_left(a, x, key=key)
if i != len(a):
return a[i]
raise ValueError
Expand Down
4 changes: 2 additions & 2 deletions Doc/tools/susp-ignored.csv
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ howto/regex,,::,
howto/regex,,:foo,(?:foo)
howto/urllib2,,:password,"""joe:password@example.com"""
library/audioop,,:ipos,"# factor = audioop.findfactor(in_test[ipos*2:ipos*2+len(out_test)],"
library/bisect,32,:hi,all(val >= x for val in a[i:hi])
library/bisect,42,:hi,all(val > x for val in a[i:hi])
library/bisect,,:hi,all(val >= x for val in a[i:hi])
library/bisect,,:hi,all(val > x for val in a[i:hi])
library/configparser,,:home,my_dir: ${Common:home_dir}/twosheds
library/configparser,,:option,${section:option}
library/configparser,,:path,python_dir: ${Frameworks:path}/Python/Versions/${Frameworks:Python}
Expand Down
96 changes: 86 additions & 10 deletions Lib/bisect.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
"""Bisection algorithms."""

def insort_right(a, x, lo=0, hi=None):
def insort_right(a, x, lo=0, hi=None, *, key=None, reverse=False):
"""Insert item x in list a, and keep it sorted assuming a is sorted.

If x is already in a, insert it to the right of the rightmost x.

Optional args lo (default 0) and hi (default len(a)) bound the
slice of a to be searched.

Optional argument key is a function of one argument used to
customize the order.
"""

lo = bisect_right(a, x, lo, hi)
lo = bisect_right(a, x, lo, hi, key=key, reverse=reverse)
a.insert(lo, x)

def bisect_right(a, x, lo=0, hi=None):
def bisect_right(a, x, lo=0, hi=None, *, key=None, reverse=False):
"""Return the index where to insert item x in list a, assuming a is sorted.

The return value i is such that all e in a[:i] have e <= x, and all e in
Expand All @@ -21,32 +24,70 @@ def bisect_right(a, x, lo=0, hi=None):

Optional args lo (default 0) and hi (default len(a)) bound the
slice of a to be searched.

Optional argument key is a function of one argument used to
customize the order.
"""

if lo < 0:
raise ValueError('lo must be non-negative')
if hi is None:
hi = len(a)

if key is None:
if reverse:
while lo < hi:
mid = (lo+hi)//2
if x > a[mid]:
hi = mid
else:
lo = mid+1
return lo

while lo < hi:
mid = (lo+hi)//2
if x < a[mid]:
hi = mid
else:
lo = mid+1
return lo

x_value = key(x)

if reverse:
while lo < hi:
mid = (lo+hi)//2
if x_value > key(a[mid]):
hi = mid
else:
lo = mid+1
return lo

while lo < hi:
mid = (lo+hi)//2
if x < a[mid]: hi = mid
else: lo = mid+1
if x_value < key(a[mid]):
hi = mid
else:
lo = mid+1
return lo

def insort_left(a, x, lo=0, hi=None):
def insort_left(a, x, lo=0, hi=None, *, key=None, reverse=False):
"""Insert item x in list a, and keep it sorted assuming a is sorted.

If x is already in a, insert it to the left of the leftmost x.

Optional args lo (default 0) and hi (default len(a)) bound the
slice of a to be searched.

Optional argument key is a function of one argument used to
customize the order.
"""

lo = bisect_left(a, x, lo, hi)
lo = bisect_left(a, x, lo, hi, key=key, reverse=reverse)
a.insert(lo, x)


def bisect_left(a, x, lo=0, hi=None):
def bisect_left(a, x, lo=0, hi=None, *, key=None, reverse=False):
"""Return the index where to insert item x in list a, assuming a is sorted.

The return value i is such that all e in a[:i] have e < x, and all e in
Expand All @@ -55,16 +96,51 @@ def bisect_left(a, x, lo=0, hi=None):

Optional args lo (default 0) and hi (default len(a)) bound the
slice of a to be searched.

Optional argument key is a function of one argument used to
customize the order.
"""

if lo < 0:
raise ValueError('lo must be non-negative')
if hi is None:
hi = len(a)

if key is None:
if reverse:
while lo < hi:
mid = (lo+hi)//2
if a[mid] > x:
lo = mid+1
else:
hi = mid
return lo

while lo < hi:
mid = (lo+hi)//2
if a[mid] < x:
lo = mid+1
else:
hi = mid
return lo

x_value = key(x)

if reverse:
while lo < hi:
mid = (lo+hi)//2
if key(a[mid]) > x_value:
lo = mid+1
else:
hi = mid
return lo

while lo < hi:
mid = (lo+hi)//2
if a[mid] < x: lo = mid+1
else: hi = mid
if key(a[mid]) < x_value:
lo = mid+1
else:
hi = mid
return lo

# Overwrite above definitions with a fast C implementation
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_bisect.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,40 @@ def test_keyword_args(self):
self.module.insort(a=data, x=25, lo=1, hi=3)
self.assertEqual(data, [10, 20, 25, 25, 25, 30, 40, 50])

for func in (self.module.bisect, self.module.bisect_right,
self.module.bisect_left, self.module.insort_left,
self.module.insort_right, self.module.insort):

with self.assertRaises(TypeError):
func(data, 25, 1, 3, lambda e: e)

def test_key(self):
data = ["z", "yy", "www"]
self.assertEqual(self.module.bisect_left(data, "xx", key=len), 1)
self.assertEqual(self.module.bisect_right(data, "xx", key=len), 2)
self.assertEqual(self.module.bisect(data, "xx", key=len), 2)
self.module.insort_left(data, "aa", key=len)
self.module.insort_right(data, "bb", key=len)
self.module.insort_right(data, "cc", key=len)
self.assertEqual(data, ['z', 'aa', 'yy', 'bb', 'cc', 'www'])

# check None is accepted
self.module.insort_right(data, "cc", key=None)
self.module.insort_left(data, "cc", key=None)
self.module.bisect_right(data, "cc", key=None)
self.module.bisect_left(data, "cc", key=None)

def test_reverse(self):
data = [50, 40, 30, 20, 10]
self.assertEqual(self.module.bisect_left(data, 15, reverse=True), 4)
self.assertEqual(self.module.bisect_right(data, 15, reverse=True), 4)
self.assertEqual(self.module.bisect(data, 15, reverse=True), 4)
self.module.insort_left(data, 15, reverse=True)
self.module.insort_right(data, 15, reverse=True)
self.module.insort(data, 15, reverse=True)
self.assertEqual(data, [50, 40, 30, 20, 15, 15, 15, 10])


class TestBisectPython(TestBisect, unittest.TestCase):
module = py_bisect

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Functions in the `bisect` module now take a key parameter to specify the
function to call before perfoming the comparison. Contributed by Rémi
Lapeyre.
Loading