Skip to content

k3rangeset

Action-CI Documentation Status Package

Segmented range represented as sorted interleaving intervals.

A range set can be thought of as: [[1, 2], [5, 7]].

k3rangeset is a component of pykit3 project: a python3 toolkit set.

Installation

pip install k3rangeset

Quick Start

import k3rangeset

# Create a RangeSet and perform operations
rs = k3rangeset.RangeSet([[1, 5], [10, 20]])

# Union two range sets
rs2 = k3rangeset.union(rs, [[3, 12]])
print(rs2)  # [[1, 20]]

# Intersection
rs3 = k3rangeset.intersect([[0, 10]], [[5, 15]])
print(rs3)  # [[5, 10]]

API Reference

k3rangeset

Segmented range which is represented in a list of sorted interleaving range.

A range set can be thought as: [[1, 2], [5, 7]].

IntIncRange

Bases: Range

IntIncRange is similiar to Range and shares the same set of API, except it limits value types to int or long, and its right boundary is closed, thus unlike Range(right boundary is open), 2 is in [1, 2].

Source code in k3rangeset/rangeset.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class IntIncRange(Range):
    """
    `IntIncRange` is similiar to `Range` and shares the same set of API, except it
    limits value types to int or long, and its right boundary is closed, thus unlike
    `Range`(right boundary is open), 2 is in `[1, 2]`.
    """

    def __init__(self, left, right):
        if left is not None and type(left) not in int_types:
            raise TypeError("{l} {ltyp} is not int or None".format(l=left, ltyp=type(left)))

        if right is not None and type(right) not in int_types:
            raise TypeError("{r} {rtyp} is not int or None".format(r=right, rtyp=type(right)))

        if cmp_boundary(left, right) > 0:
            raise ValueError("left not smaller or equal right: {left}, {right}".format(left=left, right=right))

        list.__init__(self, [left, right])

    def cmp(self, b):
        # if a and b can be merged into one range, we say a == b

        if None not in (self[0], b[1]) and self[0] - b[1] > 1:
            return 1

        if None not in (b[0], self[1]) and b[0] - self[1] > 1:
            return -1

        # incomparable: overlapping or adjacent ranges
        return 0

    def is_adjacent(self, b):
        return None not in (b[0], self[1]) and self[1] + 1 == b[0]

    def has(self, pos):
        return (
            cmp_val(self[0], pos, none_cmp_finite=-1) <= 0
            and cmp_val(pos, self[1], none_cmp_finite=1) <= 0
            and type(pos) in int_types
        )

    def length(self):
        if None in self:
            return float("inf")

        return self[1] - self[0] + 1

    def next_left(self):
        return self[1] + 1

    def prev_right(self):
        return self[0] - 1

IntIncRangeSet

Bases: RangeSet

It is similiar to RangeSet and shares the same set of API, except the default class for element in it is IntIncRange, not Range.

Source code in k3rangeset/rangeset.py
541
542
543
544
545
546
547
class IntIncRangeSet(RangeSet):
    """
    It is similiar to `RangeSet` and shares the same set of API, except the default
    class for element in it is `IntIncRange`, not `Range`.
    """

    default_range_clz = IntIncRange

Range

Bases: ValueRange

A continuous range. Range is left-close and right-open. E.g. a range [1, 3] has 2 elements 1 and 2, but 3 is not in this range.

Source code in k3rangeset/rangeset.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class Range(ValueRange):
    """
    A continuous range.
    Range is left-close and right-open.
    E.g. a range `[1, 3]` has 2 elements `1` and `2`, but `3` is not in this range.
    """

    def __init__(self, left, right):
        """
        :param left: specifies the left close boundary, which means `left` is in in this range.
        :param right: specifies the right open boundary, which means `right` is **NOT** in this range.
        """
        assert_compatible(left, right)

        if cmp_boundary(left, right) > 0:
            raise ValueError(
                "left not smaller or equal right: {left}, {right}".format(left=repr(left), right=repr(right))
            )

        super(ValueRange, self).__init__([left, right])

    def cmp(self, b):
        """
        Compare this range with `other`.
        :param b: is another `Range` instance.
        :return: `1` if this range is on the right of `other`.
                 `-1` if this range is on the left of `other`.
                 `0` if this range overlaps with `other` or they are adjacent ranges.
        """
        # if a and b can be merged into one range, we say a == b
        #
        # Different from ValueRange: adjacent Range-s are equal,
        # because they can be merged into one.

        if cmp_boundary(self[0], b[1]) > 0:
            return 1

        if cmp_boundary(b[0], self[1]) > 0:
            return -1

        # incomparable: overlapping or adjacent ranges
        return 0

__init__(left, right)

:param left: specifies the left close boundary, which means left is in in this range. :param right: specifies the right open boundary, which means right is NOT in this range.

Source code in k3rangeset/rangeset.py
190
191
192
193
194
195
196
197
198
199
200
201
202
def __init__(self, left, right):
    """
    :param left: specifies the left close boundary, which means `left` is in in this range.
    :param right: specifies the right open boundary, which means `right` is **NOT** in this range.
    """
    assert_compatible(left, right)

    if cmp_boundary(left, right) > 0:
        raise ValueError(
            "left not smaller or equal right: {left}, {right}".format(left=repr(left), right=repr(right))
        )

    super(ValueRange, self).__init__([left, right])

cmp(b)

Compare this range with other. :param b: is another Range instance. :return: 1 if this range is on the right of other. -1 if this range is on the left of other. 0 if this range overlaps with other or they are adjacent ranges.

Source code in k3rangeset/rangeset.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def cmp(self, b):
    """
    Compare this range with `other`.
    :param b: is another `Range` instance.
    :return: `1` if this range is on the right of `other`.
             `-1` if this range is on the left of `other`.
             `0` if this range overlaps with `other` or they are adjacent ranges.
    """
    # if a and b can be merged into one range, we say a == b
    #
    # Different from ValueRange: adjacent Range-s are equal,
    # because they can be merged into one.

    if cmp_boundary(self[0], b[1]) > 0:
        return 1

    if cmp_boundary(b[0], self[1]) > 0:
        return -1

    # incomparable: overlapping or adjacent ranges
    return 0

RangeDict

Bases: list

RangeDict defines a mapping from ranges to values.

Adjacent ranges such as (0, 1), (1, 2) can exist in RangeDict but can not exist in RangeSet. Because in RangeDict each range there is a value bound.

Difference from RangeSet: Adjacent ranges such as (0, 1), (1, 2) can exist in RangeDict but can not exist in RangeSet. Because in RangeDict each range there is a value bound.

Source code in k3rangeset/rangeset.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
class RangeDict(list):
    """
    `RangeDict` defines a mapping from ranges to values.

    Adjacent ranges such as `(0, 1), (1, 2)` can exist in `RangeDict`
    but can not exist in `RangeSet`.
    Because in `RangeDict` each range there is a value bound.

    Difference from `RangeSet`:
    Adjacent ranges such as `(0, 1), (1, 2)` can exist in `RangeDict`
    but can not exist in `RangeSet`.
    Because in `RangeDict` each range there is a value bound.
    """

    default_range_clz = ValueRange

    # dimension = 1 indicates the value is a RangeDict whose value is any type.
    # dimension = 2 indicates the value is a RangeDict thus to represent a 2D
    # range dict.
    dimension = 1

    def __init__(self, iterable=None, range_clz=None, dimension=None):
        """
        are same as `RangeSet` except the default value for `range_clz` is `ValueRange` instead of `Range`.
        :param iterable:
        :param range_clz:

        :param dimension: specifies if to convert the value in it into a nested `RangeDict`.
        It is used to create multi dimension RangeDict.
        By default it is `1`.
        """
        if iterable is None:
            iterable = []

        if dimension is not None:
            self.dimension = int(dimension)

        if self.dimension < 1:
            raise ValueError("dimension must >= 1, but: {d}".format(d=self.dimension))

        self.range_clz = range_clz or self.default_range_clz

        super(RangeDict, self).__init__([self.range_clz(*x) for x in iterable])

        for i in range(0, len(self) - 1):
            if self[i].cmp(self[i + 1]) != -1:
                raise ValueError(
                    "range[{i}] {ri} does not smaller than range[{j}] {ripp}".format(
                        i=i,
                        j=i + 1,
                        ri=self[i],
                        ripp=self[i + 1],
                    )
                )

        if self.dimension > 1:
            for rng in self:
                v = rng.val()
                if v is not None:
                    v = self.__class__(v, range_clz=self.range_clz, dimension=self.dimension - 1)
                    rng.set(v)

    def add(self, rng, val=None):
        """
        Add a mapping from range to value into `RangeDict`.
        :param rng: a two element iterable that defines a range.
        :param val: value of any data type.
        :return: Nothing
        """
        if val is not None and self.dimension > 1:
            val = self.__class__(val, range_clz=self.range_clz, dimension=self.dimension - 1)

        rng = _to_range(self.range_clz, list(rng) + [val])

        i = bisect_left(self, rng)

        while i < len(self):
            if rng.cmp(self[i]) == 0:
                left, right = substract_range(self[i], rng)
                if left is None:
                    if right is None:
                        self.pop(i)
                    else:
                        self[i] = right
                        i += 1
                else:
                    if right is None:
                        self[i] = left
                        i += 1
                    else:
                        self.pop(i)
                        self.insert(i, right)
                        self.insert(i, left)
                        i += 2
            else:
                break

        if rng[0] is None:
            self.insert(0, rng)
        else:
            for i in range(len(self)):
                if self[i].cmp_left(rng[0]) > 0:
                    self.insert(i, rng)
                    break
            else:
                self.append(rng)

        self.normalize()

    def get(self, pos, *positions):
        """
        :param pos: position in `RangeDict`
        :param positions: the nested position to get if this `RangeDict.dimension > 1`.
        :return: the value of range that `pos` is in.
        If `pos` is not in any ranges, `KeyError` is raised.
        """
        rng = [pos, None]
        i = bisect_left(self, rng)

        if i == len(self) or not self[i].has(pos):
            raise KeyError("not in range: " + repr(pos))

        v = self[i].val()

        if len(positions) > 0:
            if len(positions) + 1 <= self.dimension:
                v = v.get(*positions)
            else:
                raise TypeError("too many position to get")
        return v

    def get_min(self, is_lt=None):
        """
        Get range of the minimum value in the range dict. If minmum value has more than one range, then get
        the first one.
        :param is_lt: is a function that receives 2 arguments `a` and `b`, returns `True` if `a` is "smaller" than `b`,
        otherwise return `False`.
        Example:
        ```
        def is_lt(a, b):
            return a < b
        ```
        If `is_lt` is `None`, use `a < b` to decide 'a' is smaller than 'b'.
        :return:
        - `i`:the index of the minimum value in the range dict.

        - `rng`:a `ValueRange`, which is the range of the minimum value in the range dict.

        - `val`: the minimum value in the range dict.
        """
        if len(self) == 0:
            raise ValueError("range dict is empty")

        def default_is_lt(a, b):
            return a < b

        if is_lt is None:
            is_lt = default_is_lt

        min_val_idx = 0
        for i in range(1, len(self)):
            if is_lt(self[i].val(), self[min_val_idx].val()):
                min_val_idx = i

        min_val_rng = self[min_val_idx]

        return min_val_idx, min_val_rng, min_val_rng.val()

    def has(self, pos):
        rng = [pos, None]
        i = bisect_left(self, rng)

        if i == len(self):
            return False

        return self[i].has(pos)

    def length(self):
        rst = 0
        for rng in self:
            rst += rng.length()

        return rst

    def normalize(self):
        """
        Reduce number of elements by merging adjacent ranges those have the same value into one continuous range.
        :return: Nothing.
        """
        i = 0
        while i < len(self) - 1:
            curr = self[i]
            nxt = self[i + 1]

            if not curr.is_adjacent(nxt):
                i += 1
                continue

            # compare value if there is
            if curr[2:] == nxt[2:]:
                o = [curr[0], nxt[1]] + curr[2:]
                self[i] = self.range_clz(*o)
                self.pop(i + 1)
            else:
                i += 1
                continue

    def find_overlapped(self, rng):
        """
        Find all ranges those overlaps with `rng`.
        :param rng: a range iterable with at least 2 elements, such `list`, `tuple`, `Range` or `ValueRange`.
        :return: a instance of `self.__class__` with `Range` or `ValueRange` elements those overlaps with `rng`.
        """
        rng = list(rng)
        rst = []

        i = bisect_left(self, rng)
        while i < len(self):
            if self[i].cmp(rng) == 0:
                if self[i].intersect(rng) is not None:
                    rst.append(self.range_clz(*self[i]))
                else:
                    # adjacent ranges
                    pass
                i += 1
            else:
                break

        return self.__class__(rst)

__init__(iterable=None, range_clz=None, dimension=None)

are same as RangeSet except the default value for range_clz is ValueRange instead of Range. :param iterable: :param range_clz:

:param dimension: specifies if to convert the value in it into a nested RangeDict. It is used to create multi dimension RangeDict. By default it is 1.

Source code in k3rangeset/rangeset.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def __init__(self, iterable=None, range_clz=None, dimension=None):
    """
    are same as `RangeSet` except the default value for `range_clz` is `ValueRange` instead of `Range`.
    :param iterable:
    :param range_clz:

    :param dimension: specifies if to convert the value in it into a nested `RangeDict`.
    It is used to create multi dimension RangeDict.
    By default it is `1`.
    """
    if iterable is None:
        iterable = []

    if dimension is not None:
        self.dimension = int(dimension)

    if self.dimension < 1:
        raise ValueError("dimension must >= 1, but: {d}".format(d=self.dimension))

    self.range_clz = range_clz or self.default_range_clz

    super(RangeDict, self).__init__([self.range_clz(*x) for x in iterable])

    for i in range(0, len(self) - 1):
        if self[i].cmp(self[i + 1]) != -1:
            raise ValueError(
                "range[{i}] {ri} does not smaller than range[{j}] {ripp}".format(
                    i=i,
                    j=i + 1,
                    ri=self[i],
                    ripp=self[i + 1],
                )
            )

    if self.dimension > 1:
        for rng in self:
            v = rng.val()
            if v is not None:
                v = self.__class__(v, range_clz=self.range_clz, dimension=self.dimension - 1)
                rng.set(v)

add(rng, val=None)

Add a mapping from range to value into RangeDict. :param rng: a two element iterable that defines a range. :param val: value of any data type. :return: Nothing

Source code in k3rangeset/rangeset.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
def add(self, rng, val=None):
    """
    Add a mapping from range to value into `RangeDict`.
    :param rng: a two element iterable that defines a range.
    :param val: value of any data type.
    :return: Nothing
    """
    if val is not None and self.dimension > 1:
        val = self.__class__(val, range_clz=self.range_clz, dimension=self.dimension - 1)

    rng = _to_range(self.range_clz, list(rng) + [val])

    i = bisect_left(self, rng)

    while i < len(self):
        if rng.cmp(self[i]) == 0:
            left, right = substract_range(self[i], rng)
            if left is None:
                if right is None:
                    self.pop(i)
                else:
                    self[i] = right
                    i += 1
            else:
                if right is None:
                    self[i] = left
                    i += 1
                else:
                    self.pop(i)
                    self.insert(i, right)
                    self.insert(i, left)
                    i += 2
        else:
            break

    if rng[0] is None:
        self.insert(0, rng)
    else:
        for i in range(len(self)):
            if self[i].cmp_left(rng[0]) > 0:
                self.insert(i, rng)
                break
        else:
            self.append(rng)

    self.normalize()

find_overlapped(rng)

Find all ranges those overlaps with rng. :param rng: a range iterable with at least 2 elements, such list, tuple, Range or ValueRange. :return: a instance of self.__class__ with Range or ValueRange elements those overlaps with rng.

Source code in k3rangeset/rangeset.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def find_overlapped(self, rng):
    """
    Find all ranges those overlaps with `rng`.
    :param rng: a range iterable with at least 2 elements, such `list`, `tuple`, `Range` or `ValueRange`.
    :return: a instance of `self.__class__` with `Range` or `ValueRange` elements those overlaps with `rng`.
    """
    rng = list(rng)
    rst = []

    i = bisect_left(self, rng)
    while i < len(self):
        if self[i].cmp(rng) == 0:
            if self[i].intersect(rng) is not None:
                rst.append(self.range_clz(*self[i]))
            else:
                # adjacent ranges
                pass
            i += 1
        else:
            break

    return self.__class__(rst)

get(pos, *positions)

:param pos: position in RangeDict :param positions: the nested position to get if this RangeDict.dimension > 1. :return: the value of range that pos is in. If pos is not in any ranges, KeyError is raised.

Source code in k3rangeset/rangeset.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
def get(self, pos, *positions):
    """
    :param pos: position in `RangeDict`
    :param positions: the nested position to get if this `RangeDict.dimension > 1`.
    :return: the value of range that `pos` is in.
    If `pos` is not in any ranges, `KeyError` is raised.
    """
    rng = [pos, None]
    i = bisect_left(self, rng)

    if i == len(self) or not self[i].has(pos):
        raise KeyError("not in range: " + repr(pos))

    v = self[i].val()

    if len(positions) > 0:
        if len(positions) + 1 <= self.dimension:
            v = v.get(*positions)
        else:
            raise TypeError("too many position to get")
    return v

get_min(is_lt=None)

Get range of the minimum value in the range dict. If minmum value has more than one range, then get the first one. :param is_lt: is a function that receives 2 arguments a and b, returns True if a is "smaller" than b, otherwise return False. Example:

def is_lt(a, b):
    return a < b
If is_lt is None, use a < b to decide 'a' is smaller than 'b'. :return: - i:the index of the minimum value in the range dict.

  • rng:a ValueRange, which is the range of the minimum value in the range dict.

  • val: the minimum value in the range dict.

Source code in k3rangeset/rangeset.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
def get_min(self, is_lt=None):
    """
    Get range of the minimum value in the range dict. If minmum value has more than one range, then get
    the first one.
    :param is_lt: is a function that receives 2 arguments `a` and `b`, returns `True` if `a` is "smaller" than `b`,
    otherwise return `False`.
    Example:
    ```
    def is_lt(a, b):
        return a < b
    ```
    If `is_lt` is `None`, use `a < b` to decide 'a' is smaller than 'b'.
    :return:
    - `i`:the index of the minimum value in the range dict.

    - `rng`:a `ValueRange`, which is the range of the minimum value in the range dict.

    - `val`: the minimum value in the range dict.
    """
    if len(self) == 0:
        raise ValueError("range dict is empty")

    def default_is_lt(a, b):
        return a < b

    if is_lt is None:
        is_lt = default_is_lt

    min_val_idx = 0
    for i in range(1, len(self)):
        if is_lt(self[i].val(), self[min_val_idx].val()):
            min_val_idx = i

    min_val_rng = self[min_val_idx]

    return min_val_idx, min_val_rng, min_val_rng.val()

normalize()

Reduce number of elements by merging adjacent ranges those have the same value into one continuous range. :return: Nothing.

Source code in k3rangeset/rangeset.py
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def normalize(self):
    """
    Reduce number of elements by merging adjacent ranges those have the same value into one continuous range.
    :return: Nothing.
    """
    i = 0
    while i < len(self) - 1:
        curr = self[i]
        nxt = self[i + 1]

        if not curr.is_adjacent(nxt):
            i += 1
            continue

        # compare value if there is
        if curr[2:] == nxt[2:]:
            o = [curr[0], nxt[1]] + curr[2:]
            self[i] = self.range_clz(*o)
            self.pop(i + 1)
        else:
            i += 1
            continue

RangeSet

Bases: RangeDict

A series of int Range. All ranges in it are ascending ordered, non-overlapping and non-adjacent.

Source code in k3rangeset/rangeset.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
class RangeSet(RangeDict):
    """
    A series of int `Range`.
    All ranges in it are ascending ordered, non-overlapping and non-adjacent.
    """

    default_range_clz = Range

    def add(self, rng):
        """
        It adds a new range into this range set and if possible, collapse it with
        overlapping or adjacent range.
        :param rng:  is a `Range` or a 2 element tuple or list.
        :return: nothing, but modify the range-set in place.
        """
        rng = _to_range(self.range_clz, list(rng))

        i = bisect_left(self, rng)

        while i < len(self):
            if rng.cmp(self[i]) == 0:
                rng = union_range(rng, self[i])
                self.pop(i)
            else:
                break

        self.insert(i, rng)

add(rng)

It adds a new range into this range set and if possible, collapse it with overlapping or adjacent range. :param rng: is a Range or a 2 element tuple or list. :return: nothing, but modify the range-set in place.

Source code in k3rangeset/rangeset.py
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def add(self, rng):
    """
    It adds a new range into this range set and if possible, collapse it with
    overlapping or adjacent range.
    :param rng:  is a `Range` or a 2 element tuple or list.
    :return: nothing, but modify the range-set in place.
    """
    rng = _to_range(self.range_clz, list(rng))

    i = bisect_left(self, rng)

    while i < len(self):
        if rng.cmp(self[i]) == 0:
            rng = union_range(rng, self[i])
            self.pop(i)
        else:
            break

    self.insert(i, rng)

ValueRange

Bases: list

Source code in k3rangeset/rangeset.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
class ValueRange(list):
    def __init__(self, left, right, val=None):
        assert_compatible(left, right)

        if cmp_boundary(left, right) > 0:
            raise ValueError(
                "left not smaller or equal right: {left}, {right}".format(left=repr(left), right=repr(right))
            )

        super(ValueRange, self).__init__([left, right, val])

    def cmp(self, b):
        # if a and b can be merged into one range, we say a == b
        #
        # Different from Range: adjacent ValueRange-s are not equal,
        # because they have their own value bound and can not be merged.

        if cmp_boundary(self[0], b[1]) >= 0:
            return 1

        if cmp_boundary(b[0], self[1]) >= 0:
            return -1

        # incomparable: overlapping or adjacent ranges
        return 0

    def intersect(self, b):
        if self.cmp(b) != 0:
            return None

        rst = [None, None] + self[2:]

        if self.cmp_left(b[0]) < 0:
            rst[0] = b[0]
        else:
            rst[0] = self[0]

        if self.cmp_right(b[1]) > 0:
            rst[1] = b[1]
        else:
            rst[1] = self[1]

        if rst[0] is not None and rst[0] == rst[1]:
            return None

        return rst

    def substract(self, b):
        if self.cmp(b) > 0:
            # keep value for ValueRange
            return [None, self.dup() + self[2:]]

        if self.cmp(b) < 0:
            # keep value for ValueRange
            return [self.dup() + self[2:], None]

        # keep value for ValueRange
        rst = [None, None]

        if self.cmp_left(b[0]) < 0:
            o = [self[0], b.prev_right()] + self[2:]
            rst[0] = self.__class__(*o)

        if b.cmp_right(self[1]) < 0:
            o = [b.next_left(), self[1]] + self[2:]
            rst[1] = self.__class__(*o)

        return rst

    def cmp_left(self, pos):
        # left is None means it is negative infinite
        return cmp_val(self[0], pos, none_cmp_finite=-1)

    def cmp_right(self, pos):
        # right is None means it is positive infinite
        return cmp_val(self[1], pos, none_cmp_finite=1)

    def is_adjacent(self, b):
        """
        Check if this range is at left of `other` and they are adjacent and can be
        merged into one range.
        :param b: is another `Range` instance.
        :return:`True` for `[1, 2] and [2, 3]`
        `False` for `[1, 2] and [3, 4]` or `[1, 2] and [1, 3]`
        """
        return cmp_boundary(b[0], self[1]) == 0

    def has(self, pos):
        """
        Return True if `val` is in this range. Otherwise `False`.
        :param pos: is the value to check.
        :return: bool
        """
        return self.cmp_left(pos) <= 0 and self.cmp_right(pos) > 0

    def length(self):
        if self[0] is None or self[1] is None:
            return float("inf")

        if isinstance(self[0], str):
            a, b = self[:2]
            max_len = max([len(a), len(b)])

            rst = 0.0
            ratio = 1.0
            for i in range(max_len):
                if i >= len(a):
                    va = 0.0
                else:
                    va = ord(a[i]) + 1.0

                if i >= len(b):
                    vb = 0.0
                else:
                    vb = ord(b[i]) + 1.0
                rst += (vb - va) / 257.0 * ratio
                ratio /= 257.0

            return rst
        else:
            return self[1] - self[0]

    def dup(self):
        return self.__class__(*self)

    def next_left(self):
        return self[1]

    def prev_right(self):
        return self[0]

    def val(self):
        return self[2]

    def set(self, v):
        self[2] = v

    def __and__(self, b):
        return self.intersect(b)

    def __rand__(self, b):
        return self.intersect(b)

has(pos)

Return True if val is in this range. Otherwise False. :param pos: is the value to check. :return: bool

Source code in k3rangeset/rangeset.py
126
127
128
129
130
131
132
def has(self, pos):
    """
    Return True if `val` is in this range. Otherwise `False`.
    :param pos: is the value to check.
    :return: bool
    """
    return self.cmp_left(pos) <= 0 and self.cmp_right(pos) > 0

is_adjacent(b)

Check if this range is at left of other and they are adjacent and can be merged into one range. :param b: is another Range instance. :return:True for [1, 2] and [2, 3] False for [1, 2] and [3, 4] or [1, 2] and [1, 3]

Source code in k3rangeset/rangeset.py
116
117
118
119
120
121
122
123
124
def is_adjacent(self, b):
    """
    Check if this range is at left of `other` and they are adjacent and can be
    merged into one range.
    :param b: is another `Range` instance.
    :return:`True` for `[1, 2] and [2, 3]`
    `False` for `[1, 2] and [3, 4]` or `[1, 2] and [1, 3]`
    """
    return cmp_boundary(b[0], self[1]) == 0

intersect(a, b)

Return a new intersection set RangeSet of all a and others. :param a: a RangeSet instance. :param b: RangeSet instances. :return: a new RangeSet instance.

Source code in k3rangeset/rangeset.py
650
651
652
653
654
655
656
657
def intersect(a, b):
    """
    Return a new intersection set `RangeSet` of all `a` and `others`.
    :param a: a `RangeSet` instance.
    :param b: `RangeSet` instances.
    :return: a new `RangeSet` instance.
    """
    return substract(a, substract(a, b))

substract(a, *bs)

Return a new RangeSet with all ranges in others removed from a. :param a: a RangeSet instance. :param bs: RangeSet instances :return: a new RangeSet instance.

Source code in k3rangeset/rangeset.py
604
605
606
607
608
609
610
611
612
613
614
def substract(a, *bs):
    """
    Return a new `RangeSet` with all ranges in `others` removed from `a`.
    :param a: a `RangeSet` instance.
    :param bs: `RangeSet` instances
    :return: a new `RangeSet` instance.
    """
    s = a
    for b in bs:
        s = _substract(s, b)
    return s

union(a, *bs)

Return a new union set RangeSet of all a and others. :param a: a RangeSet instance. :param bs: RangeSet instances :return: a new RangeSet instance.

Source code in k3rangeset/rangeset.py
550
551
552
553
554
555
556
557
558
559
560
def union(a, *bs):
    """
    Return a new union set `RangeSet` of all `a` and `others`.
    :param a: a `RangeSet` instance.
    :param bs: `RangeSet` instances
    :return: a new `RangeSet` instance.
    """
    u = a
    for b in bs:
        u = _union(u, b)
    return u

License

The MIT License (MIT) - Copyright (c) 2015 Zhang Yanpo (张炎泼)