1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11from collections.abc import Iterable, Iterator, Sequence
12from typing import Self, TypeAlias, cast, final
13
14IntervalsT: TypeAlias = tuple[tuple[int, int], ...]
15
16
17# @final makes mypy happy with the Self return annotations. We otherwise run
18# afoul of:
19# > You should not use Self as the return annotation if the method is not
20# > guaranteed to return an instance of a subclass when the class is subclassed
21# > https://docs.python.org/3/library/typing.html#typing.Self
22
23
24@final
25class IntervalSet:
26 """
27 A compact and efficient representation of a set of ``(a, b)`` intervals. Can
28 be treated like a set of integers, in that ``n in intervals`` will return
29 ``True`` if ``n`` is contained in any of the ``(a, b)`` intervals, and
30 ``False`` otherwise.
31 """
32
33 @classmethod
34 def from_string(cls, s: str) -> "Self":
35 """Return a tuple of intervals, covering the codepoints of characters in `s`.
36
37 >>> IntervalSet.from_string('abcdef0123456789')
38 ((48, 57), (97, 102))
39 """
40 x = cls([(ord(c), ord(c)) for c in sorted(s)])
41 return x.union(x)
42
43 def __init__(self, intervals: Iterable[Sequence[int]] = ()) -> None:
44 self.intervals: IntervalsT = cast(
45 IntervalsT, tuple(tuple(v) for v in intervals)
46 )
47 # cast above is validated by this length assertion. check here instead of
48 # before to not exhaust generators before we create intervals from it
49 assert all(len(v) == 2 for v in self.intervals)
50
51 self.offsets: list[int] = [0]
52 for u, v in self.intervals:
53 self.offsets.append(self.offsets[-1] + v - u + 1)
54 self.size = self.offsets.pop()
55 self._idx_of_zero = self.index_above(ord("0"))
56 self._idx_of_Z = min(self.index_above(ord("Z")), len(self) - 1)
57
58 def __len__(self) -> int:
59 return self.size
60
61 def __iter__(self) -> Iterator[int]:
62 for u, v in self.intervals:
63 yield from range(u, v + 1)
64
65 def __getitem__(self, i: int) -> int:
66 if i < 0:
67 i = self.size + i
68 if i < 0 or i >= self.size:
69 raise IndexError(f"Invalid index {i} for [0, {self.size})")
70 # Want j = maximal such that offsets[j] <= i
71
72 j = len(self.intervals) - 1
73 if self.offsets[j] > i:
74 hi = j
75 lo = 0
76 # Invariant: offsets[lo] <= i < offsets[hi]
77 while lo + 1 < hi:
78 mid = (lo + hi) // 2
79 if self.offsets[mid] <= i:
80 lo = mid
81 else:
82 hi = mid
83 j = lo
84 t = i - self.offsets[j]
85 u, v = self.intervals[j]
86 r = u + t
87 assert r <= v
88 return r
89
90 def __contains__(self, elem: str | int) -> bool:
91 if isinstance(elem, str):
92 elem = ord(elem)
93 assert 0 <= elem <= 0x10FFFF
94 return any(start <= elem <= end for start, end in self.intervals)
95
96 def __repr__(self) -> str:
97 return f"IntervalSet({self.intervals!r})"
98
99 def index(self, value: int) -> int:
100 for offset, (u, v) in zip(self.offsets, self.intervals, strict=True):
101 if u == value:
102 return offset
103 elif u > value:
104 raise ValueError(f"{value} is not in list")
105 if value <= v:
106 return offset + (value - u)
107 raise ValueError(f"{value} is not in list")
108
109 def index_above(self, value: int) -> int:
110 for offset, (u, v) in zip(self.offsets, self.intervals, strict=True):
111 if u >= value:
112 return offset
113 if value <= v:
114 return offset + (value - u)
115 return self.size
116
117 def __or__(self, other: "Self") -> "Self":
118 return self.union(other)
119
120 def __sub__(self, other: "Self") -> "Self":
121 return self.difference(other)
122
123 def __and__(self, other: "Self") -> "Self":
124 return self.intersection(other)
125
126 def __eq__(self, other: object) -> bool:
127 return isinstance(other, IntervalSet) and (other.intervals == self.intervals)
128
129 def __hash__(self) -> int:
130 return hash(self.intervals)
131
132 def union(self, other: "Self") -> "Self":
133 """Merge two sequences of intervals into a single tuple of intervals.
134
135 Any integer bounded by `x` or `y` is also bounded by the result.
136
137 >>> union([(3, 10)], [(1, 2), (5, 17)])
138 ((1, 17),)
139 """
140 assert isinstance(other, type(self))
141 x = self.intervals
142 y = other.intervals
143 if not x:
144 return IntervalSet(y)
145 if not y:
146 return IntervalSet(x)
147 intervals = sorted(x + y, reverse=True)
148 result = [intervals.pop()]
149 while intervals:
150 # 1. intervals is in descending order
151 # 2. pop() takes from the RHS.
152 # 3. (a, b) was popped 1st, then (u, v) was popped 2nd
153 # 4. Therefore: a <= u
154 # 5. We assume that u <= v and a <= b
155 # 6. So we need to handle 2 cases of overlap, and one disjoint case
156 # | u--v | u----v | u--v |
157 # | a----b | a--b | a--b |
158 u, v = intervals.pop()
159 a, b = result[-1]
160 if u <= b + 1:
161 # Overlap cases
162 result[-1] = (a, max(v, b))
163 else:
164 # Disjoint case
165 result.append((u, v))
166 return IntervalSet(result)
167
168 def difference(self, other: "Self") -> "Self":
169 """Set difference for lists of intervals. That is, returns a list of
170 intervals that bounds all values bounded by x that are not also bounded by
171 y. x and y are expected to be in sorted order.
172
173 For example difference([(1, 10)], [(2, 3), (9, 15)]) would
174 return [(1, 1), (4, 8)], removing the values 2, 3, 9 and 10 from the
175 interval.
176 """
177 assert isinstance(other, type(self))
178 x = self.intervals
179 y = other.intervals
180 if not y:
181 return IntervalSet(x)
182 x = list(map(list, x))
183 i = 0
184 j = 0
185 result: list[Iterable[int]] = []
186 while i < len(x) and j < len(y):
187 # Iterate in parallel over x and y. j stays pointing at the smallest
188 # interval in the left hand side that could still overlap with some
189 # element of x at index >= i.
190 # Similarly, i is not incremented until we know that it does not
191 # overlap with any element of y at index >= j.
192
193 xl, xr = x[i]
194 assert xl <= xr
195 yl, yr = y[j]
196 assert yl <= yr
197
198 if yr < xl:
199 # The interval at y[j] is strictly to the left of the interval at
200 # x[i], so will not overlap with it or any later interval of x.
201 j += 1
202 elif yl > xr:
203 # The interval at y[j] is strictly to the right of the interval at
204 # x[i], so all of x[i] goes into the result as no further intervals
205 # in y will intersect it.
206 result.append(x[i])
207 i += 1
208 elif yl <= xl:
209 if yr >= xr:
210 # x[i] is contained entirely in y[j], so we just skip over it
211 # without adding it to the result.
212 i += 1
213 else:
214 # The beginning of x[i] is contained in y[j], so we update the
215 # left endpoint of x[i] to remove this, and increment j as we
216 # now have moved past it. Note that this is not added to the
217 # result as is, as more intervals from y may intersect it so it
218 # may need updating further.
219 x[i][0] = yr + 1
220 j += 1
221 else:
222 # yl > xl, so the left hand part of x[i] is not contained in y[j],
223 # so there are some values we should add to the result.
224 result.append((xl, yl - 1))
225
226 if yr + 1 <= xr:
227 # If y[j] finishes before x[i] does, there may be some values
228 # in x[i] left that should go in the result (or they may be
229 # removed by a later interval in y), so we update x[i] to
230 # reflect that and increment j because it no longer overlaps
231 # with any remaining element of x.
232 x[i][0] = yr + 1
233 j += 1
234 else:
235 # Every element of x[i] other than the initial part we have
236 # already added is contained in y[j], so we move to the next
237 # interval.
238 i += 1
239 # Any remaining intervals in x do not overlap with any of y, as if they did
240 # we would not have incremented j to the end, so can be added to the result
241 # as they are.
242 result.extend(x[i:])
243 return IntervalSet(map(tuple, result))
244
245 def intersection(self, other: "Self") -> "Self":
246 """Set intersection for lists of intervals."""
247 assert isinstance(other, type(self)), other
248 intervals = []
249 i = j = 0
250 while i < len(self.intervals) and j < len(other.intervals):
251 u, v = self.intervals[i]
252 U, V = other.intervals[j]
253 if u > V:
254 j += 1
255 elif U > v:
256 i += 1
257 else:
258 intervals.append((max(u, U), min(v, V)))
259 if v < V:
260 i += 1
261 else:
262 j += 1
263 return IntervalSet(intervals)
264
265 def char_in_shrink_order(self, i: int) -> str:
266 # We would like it so that, where possible, shrinking replaces
267 # characters with simple ascii characters, so we rejig this
268 # bit so that the smallest values are 0, 1, 2, ..., Z.
269 #
270 # Imagine that numbers are laid out as abc0yyyZ...
271 # this rearranges them so that they are laid out as
272 # 0yyyZcba..., which gives a better shrinking order.
273 if i <= self._idx_of_Z:
274 # We want to rewrite the integers [0, n] inclusive
275 # to [zero_point, Z_point].
276 n = self._idx_of_Z - self._idx_of_zero
277 if i <= n:
278 i += self._idx_of_zero
279 else:
280 # We want to rewrite the integers [n + 1, Z_point] to
281 # [zero_point, 0] (reversing the order so that codepoints below
282 # zero_point shrink upwards).
283 i = self._idx_of_zero - (i - n)
284 assert i < self._idx_of_zero
285 assert 0 <= i <= self._idx_of_Z
286
287 return chr(self[i])
288
289 def index_from_char_in_shrink_order(self, c: str) -> int:
290 """
291 Inverse of char_in_shrink_order.
292 """
293 assert len(c) == 1
294 i = self.index(ord(c))
295
296 if i <= self._idx_of_Z:
297 n = self._idx_of_Z - self._idx_of_zero
298 # Rewrite [zero_point, Z_point] to [0, n].
299 if self._idx_of_zero <= i <= self._idx_of_Z:
300 i -= self._idx_of_zero
301 assert 0 <= i <= n
302 # Rewrite [zero_point, 0] to [n + 1, Z_point].
303 else:
304 i = self._idx_of_zero - i + n
305 assert n + 1 <= i <= self._idx_of_Z
306 assert 0 <= i <= self._idx_of_Z
307
308 return i