blob: cbb34b9cb423cc043decb89c48fcfedb962703f5 [file] [log] [blame]
Doug Zongker424296a2014-09-02 08:53:09 -07001# Copyright (C) 2014 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Doug Zongkerfc44a512014-08-26 13:10:25 -070015from __future__ import print_function
16import heapq
17import itertools
18
19__all__ = ["RangeSet"]
20
21class RangeSet(object):
22 """A RangeSet represents a set of nonoverlapping ranges on the
23 integers (ie, a set of integers, but efficient when the set contains
24 lots of runs."""
25
26 def __init__(self, data=None):
Tao Bao937847a2015-08-25 15:10:10 -070027 # TODO(tbao): monotonic is broken when passing in a tuple.
Dan Albert8b72aef2015-03-23 19:13:21 -070028 self.monotonic = False
Doug Zongker846cb3a2014-09-09 10:55:36 -070029 if isinstance(data, str):
30 self._parse_internal(data)
31 elif data:
Doug Zongkerfc44a512014-08-26 13:10:25 -070032 self.data = tuple(self._remove_pairs(data))
33 else:
34 self.data = ()
35
36 def __iter__(self):
37 for i in range(0, len(self.data), 2):
38 yield self.data[i:i+2]
39
40 def __eq__(self, other):
41 return self.data == other.data
42 def __ne__(self, other):
43 return self.data != other.data
44 def __nonzero__(self):
45 return bool(self.data)
Anthony Kingc713d762015-11-03 00:23:11 +000046 def __bool__(self):
47 return self.__nonzero__()
Doug Zongkerfc44a512014-08-26 13:10:25 -070048
49 def __str__(self):
50 if not self.data:
51 return "empty"
52 else:
53 return self.to_string()
54
Doug Zongker846cb3a2014-09-09 10:55:36 -070055 def __repr__(self):
56 return '<RangeSet("' + self.to_string() + '")>'
57
Doug Zongkerfc44a512014-08-26 13:10:25 -070058 @classmethod
59 def parse(cls, text):
60 """Parse a text string consisting of a space-separated list of
61 blocks and ranges, eg "10-20 30 35-40". Ranges are interpreted to
62 include both their ends (so the above example represents 18
63 individual blocks. Returns a RangeSet object.
64
65 If the input has all its blocks in increasing order, then returned
66 RangeSet will have an extra attribute 'monotonic' that is set to
67 True. For example the input "10-20 30" is monotonic, but the input
68 "15-20 30 10-14" is not, even though they represent the same set
69 of blocks (and the two RangeSets will compare equal with ==).
70 """
Doug Zongker846cb3a2014-09-09 10:55:36 -070071 return cls(text)
Doug Zongkerfc44a512014-08-26 13:10:25 -070072
Doug Zongker846cb3a2014-09-09 10:55:36 -070073 def _parse_internal(self, text):
Doug Zongkerfc44a512014-08-26 13:10:25 -070074 data = []
75 last = -1
76 monotonic = True
77 for p in text.split():
78 if "-" in p:
79 s, e = p.split("-")
80 data.append(int(s))
81 data.append(int(e)+1)
82 if last <= s <= e:
83 last = e
84 else:
85 monotonic = False
86 else:
87 s = int(p)
88 data.append(s)
89 data.append(s+1)
90 if last <= s:
91 last = s+1
92 else:
93 monotonic = True
94 data.sort()
Doug Zongker846cb3a2014-09-09 10:55:36 -070095 self.data = tuple(self._remove_pairs(data))
96 self.monotonic = monotonic
Doug Zongkerfc44a512014-08-26 13:10:25 -070097
98 @staticmethod
99 def _remove_pairs(source):
100 last = None
101 for i in source:
102 if i == last:
103 last = None
104 else:
105 if last is not None:
106 yield last
107 last = i
108 if last is not None:
109 yield last
110
111 def to_string(self):
112 out = []
113 for i in range(0, len(self.data), 2):
114 s, e = self.data[i:i+2]
115 if e == s+1:
116 out.append(str(s))
117 else:
118 out.append(str(s) + "-" + str(e-1))
119 return " ".join(out)
120
121 def to_string_raw(self):
122 return str(len(self.data)) + "," + ",".join(str(i) for i in self.data)
123
124 def union(self, other):
125 """Return a new RangeSet representing the union of this RangeSet
Doug Zongker846cb3a2014-09-09 10:55:36 -0700126 with the argument.
127
128 >>> RangeSet("10-19 30-34").union(RangeSet("18-29"))
129 <RangeSet("10-34")>
130 >>> RangeSet("10-19 30-34").union(RangeSet("22 32"))
131 <RangeSet("10-19 22 30-34")>
132 """
Doug Zongkerfc44a512014-08-26 13:10:25 -0700133 out = []
134 z = 0
135 for p, d in heapq.merge(zip(self.data, itertools.cycle((+1, -1))),
136 zip(other.data, itertools.cycle((+1, -1)))):
137 if (z == 0 and d == 1) or (z == 1 and d == -1):
138 out.append(p)
139 z += d
140 return RangeSet(data=out)
141
142 def intersect(self, other):
143 """Return a new RangeSet representing the intersection of this
Doug Zongker846cb3a2014-09-09 10:55:36 -0700144 RangeSet with the argument.
145
146 >>> RangeSet("10-19 30-34").intersect(RangeSet("18-32"))
147 <RangeSet("18-19 30-32")>
148 >>> RangeSet("10-19 30-34").intersect(RangeSet("22-28"))
149 <RangeSet("")>
150 """
Doug Zongkerfc44a512014-08-26 13:10:25 -0700151 out = []
152 z = 0
153 for p, d in heapq.merge(zip(self.data, itertools.cycle((+1, -1))),
154 zip(other.data, itertools.cycle((+1, -1)))):
155 if (z == 1 and d == 1) or (z == 2 and d == -1):
156 out.append(p)
157 z += d
158 return RangeSet(data=out)
159
160 def subtract(self, other):
161 """Return a new RangeSet representing subtracting the argument
Doug Zongker846cb3a2014-09-09 10:55:36 -0700162 from this RangeSet.
163
164 >>> RangeSet("10-19 30-34").subtract(RangeSet("18-32"))
165 <RangeSet("10-17 33-34")>
166 >>> RangeSet("10-19 30-34").subtract(RangeSet("22-28"))
167 <RangeSet("10-19 30-34")>
168 """
Doug Zongkerfc44a512014-08-26 13:10:25 -0700169
170 out = []
171 z = 0
172 for p, d in heapq.merge(zip(self.data, itertools.cycle((+1, -1))),
173 zip(other.data, itertools.cycle((-1, +1)))):
174 if (z == 0 and d == 1) or (z == 1 and d == -1):
175 out.append(p)
176 z += d
177 return RangeSet(data=out)
178
179 def overlaps(self, other):
180 """Returns true if the argument has a nonempty overlap with this
Doug Zongker846cb3a2014-09-09 10:55:36 -0700181 RangeSet.
182
183 >>> RangeSet("10-19 30-34").overlaps(RangeSet("18-32"))
184 True
185 >>> RangeSet("10-19 30-34").overlaps(RangeSet("22-28"))
186 False
187 """
Doug Zongkerfc44a512014-08-26 13:10:25 -0700188
189 # This is like intersect, but we can stop as soon as we discover the
190 # output is going to be nonempty.
191 z = 0
Dan Albert8b72aef2015-03-23 19:13:21 -0700192 for _, d in heapq.merge(zip(self.data, itertools.cycle((+1, -1))),
Doug Zongkerfc44a512014-08-26 13:10:25 -0700193 zip(other.data, itertools.cycle((+1, -1)))):
194 if (z == 1 and d == 1) or (z == 2 and d == -1):
195 return True
196 z += d
197 return False
198
199 def size(self):
200 """Returns the total size of the RangeSet (ie, how many integers
Doug Zongker846cb3a2014-09-09 10:55:36 -0700201 are in the set).
202
203 >>> RangeSet("10-19 30-34").size()
204 15
205 """
Doug Zongkerfc44a512014-08-26 13:10:25 -0700206
207 total = 0
208 for i, p in enumerate(self.data):
209 if i % 2:
210 total += p
211 else:
212 total -= p
213 return total
Doug Zongker62338182014-09-08 08:29:55 -0700214
215 def map_within(self, other):
216 """'other' should be a subset of 'self'. Returns a RangeSet
217 representing what 'other' would get translated to if the integers
218 of 'self' were translated down to be contiguous starting at zero.
219
Doug Zongker846cb3a2014-09-09 10:55:36 -0700220 >>> RangeSet("0-9").map_within(RangeSet("3-4"))
221 <RangeSet("3-4")>
222 >>> RangeSet("10-19").map_within(RangeSet("13-14"))
223 <RangeSet("3-4")>
224 >>> RangeSet("10-19 30-39").map_within(RangeSet("17-19 30-32"))
225 <RangeSet("7-12")>
226 >>> RangeSet("10-19 30-39").map_within(RangeSet("12-13 17-19 30-32"))
227 <RangeSet("2-3 7-12")>
Doug Zongker62338182014-09-08 08:29:55 -0700228 """
229
230 out = []
231 offset = 0
232 start = None
233 for p, d in heapq.merge(zip(self.data, itertools.cycle((-5, +5))),
234 zip(other.data, itertools.cycle((-1, +1)))):
235 if d == -5:
236 start = p
237 elif d == +5:
238 offset += p-start
239 start = None
240 else:
241 out.append(offset + p - start)
242 return RangeSet(data=out)
243
Tao Bao2fd2c9b2015-07-09 17:37:49 -0700244 def extend(self, n):
245 """Extend the RangeSet by 'n' blocks.
246
247 The lower bound is guaranteed to be non-negative.
248
249 >>> RangeSet("0-9").extend(1)
250 <RangeSet("0-10")>
251 >>> RangeSet("10-19").extend(15)
252 <RangeSet("0-34")>
253 >>> RangeSet("10-19 30-39").extend(4)
254 <RangeSet("6-23 26-43")>
255 >>> RangeSet("10-19 30-39").extend(10)
256 <RangeSet("0-49")>
257 """
258 out = self
259 for i in range(0, len(self.data), 2):
260 s, e = self.data[i:i+2]
261 s1 = max(0, s - n)
262 e1 = e + n
263 out = out.union(RangeSet(str(s1) + "-" + str(e1-1)))
264 return out
265
Tao Bao937847a2015-08-25 15:10:10 -0700266 def first(self, n):
267 """Return the RangeSet that contains at most the first 'n' integers.
268
269 >>> RangeSet("0-9").first(1)
270 <RangeSet("0")>
271 >>> RangeSet("10-19").first(5)
272 <RangeSet("10-14")>
273 >>> RangeSet("10-19").first(15)
274 <RangeSet("10-19")>
275 >>> RangeSet("10-19 30-39").first(3)
276 <RangeSet("10-12")>
277 >>> RangeSet("10-19 30-39").first(15)
278 <RangeSet("10-19 30-34")>
279 >>> RangeSet("10-19 30-39").first(30)
280 <RangeSet("10-19 30-39")>
281 >>> RangeSet("0-9").first(0)
282 <RangeSet("")>
283 """
284
285 if self.size() <= n:
286 return self
287
288 out = []
289 for s, e in self:
290 if e - s >= n:
291 out += (s, s+n)
292 break
293 else:
294 out += (s, e)
295 n -= e - s
296 return RangeSet(data=out)
297
Doug Zongker62338182014-09-08 08:29:55 -0700298
299if __name__ == "__main__":
300 import doctest
301 doctest.testmod()