Replace hand-rolled binary search with bisect_left (#50410)

The `bisect` module exposes a `bisect_left` function which does
basically what the bulk of `_lower_bound` does. From my tests, it is
slightly faster (~5%) in the probably common ideal case where `arr` is short.
In the worst case scenario, `bisect.bisect_left` is *much* faster.

```
>>> arr = list(range(60))
>>> cmp = 59
>>> %timeit _lower_bound(arr, cmp)
736 ns ± 6.24 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> %timeit bisect_lower_bound(arr, cmp)
290 ns ± 7.77 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
```

I doubt this is a huge bottleneck or anything, but I think it's a bit
more readable, and it's more efficient, so it seems like it's mostly a
win.

This commit *will* add a new unconditional import for `bisect` when
importing `util.dt`, and `bisect` is not currently imported for any of
the standard library modules. It is possible to make this conditional by
placing `import bisect` in the _lower_bound function, or in the function
it's nested in.
This commit is contained in:
Paul Ganssle 2021-05-11 11:18:20 -04:00 committed by GitHub
parent f5541a468e
commit efa5c59559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,6 +1,7 @@
"""Helper methods to handle the time in Home Assistant."""
from __future__ import annotations
import bisect
from contextlib import suppress
import datetime as dt
import re
@ -265,15 +266,7 @@ def find_next_time_expression_time(
Return None if no such value exists.
"""
left = 0
right = len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] < cmp:
left = mid + 1
else:
right = mid
left = bisect.bisect_left(arr, cmp)
if left == len(arr):
return None
return arr[left]