Coverage for blind_charging/broken_range.py: 90%
61 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-17 20:36 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-17 20:36 +0000
1"""Data type for discontinuous ranges."""
2from typing import List, Tuple
5def _find_index(R: List[int], value: int, inside: bool = False) -> int:
6 """Find the index of the member equal to or greater than the input value.
8 :param R: List of range blocks, as [b0_s, b0_e, ... ]
9 :param value: Value to search for
10 :param inside: Use > instead of >= as comparator
11 :returns: The index of the smallest member greater than or equal to the
12 input value. The return value will be equal to the length of the list if
13 there is no member that's greater or equal to the input.
14 """
15 if not R:
16 return 0
18 low = 0
19 high = len(R)
21 while (high - low) > 1:
22 mid = low + ((high - low) >> 1)
23 if value < R[mid]:
24 high = mid
25 else:
26 low = mid
28 idx = low if value <= R[low] else high
30 if idx == len(R):
31 return idx
33 if inside and R[idx] == value:
34 return idx + 1
35 return idx
38class BrokenRange(object):
39 """Representation of a discontinuous range.
41 Includes utilities to update range and merge overlaps.
42 """
44 def __init__(self):
45 self._range: List[int] = []
47 def addspan(self, start: int, end: int):
48 """Incorporate span [start, end) into the range.
50 :param start: Index of start
51 :param end: Index of end (exclusive)
52 :returns: BrokenRange (self)
53 """
54 if end <= start:
55 raise ValueError("Invalid extent: {}".format(end - start))
57 R = self._range
59 # Find the index in the range of the start of the block where this
60 # span should go.
61 start_idx = _find_index(R, start)
62 if start_idx % 2 == 1:
63 start_idx -= 1
65 # When the span is higher than any existing block, just append it.
66 # This handles the initial condition when no ranges have been added
67 # as well.
68 if start_idx == len(R):
69 self._range += [start, end]
70 return self
72 # Find the index in the range of the end of the block where this
73 # span should go.
74 end_idx = _find_index(R, end + 1)
75 if end_idx % 2 == 0:
76 end_idx -= 1
78 # When the span precedes any existing block, prepend it.
79 if end_idx < 0:
80 self._range = [start, end] + R
81 return self
83 block_start = min(start, R[start_idx])
84 block_end = max(end, R[end_idx])
85 self._range = R[:start_idx] + [block_start, block_end] + R[end_idx + 1 :]
87 return self
89 def contains(self, value: int) -> bool:
90 """Test whether a value is contained in the range.
92 :param value: Value to test
93 :returns: Boolean indicating membership
94 """
95 idx = _find_index(self._range, value)
97 # If the index is out of range, value is not contained
98 if idx == len(self._range):
99 return False
101 # If the index is even, it must be exact match to be contained.
102 # If the index is odd, it must *not* be exact match to be contained.
103 if idx % 2 == 0:
104 return value == self._range[idx]
105 else:
106 return value != self._range[idx]
108 def overlaps(self, start: int, end: int) -> bool:
109 """Test whether given span overlaps any part of this range.
111 :param start: Start of span
112 :param end: End of span
113 :returns: Boolean indicating overlap
114 """
115 if end <= start:
116 raise ValueError("Invalid extent: {}".format(end - start))
118 start_idx = _find_index(self._range, start, inside=True)
119 end_idx = _find_index(self._range, end, inside=False)
121 # An odd index implies span starts inside block
122 if start_idx % 2 == 1:
123 return True
125 # Otherwise if the span crosses any range, there must be overlap.
126 return start_idx != end_idx
128 def __iadd__(self, span: Tuple[int, int]) -> "BrokenRange":
129 return self.addspan(span[0], span[1])
131 def __repr__(self) -> str:
132 R = self._range
133 spans = ["[{},{}]".format(R[i], R[i + 1]) for i in range(0, len(R), 2)]
134 return "BrokenRange({})".format(",".join(spans))