| | 606 | |
| | 607 | |
| | 608 | class Ranges(object): |
| | 609 | """ |
| | 610 | Holds information about ranges parsed from a string |
| | 611 | |
| | 612 | >>> x = Ranges("1,2,9-15") |
| | 613 | >>> 1 in x |
| | 614 | True |
| | 615 | >>> 5 in x |
| | 616 | False |
| | 617 | >>> 10 in x |
| | 618 | True |
| | 619 | >>> 16 in x |
| | 620 | False |
| | 621 | >>> [i for i in range(20) if i in x] |
| | 622 | [1, 2, 9, 10, 11, 12, 13, 14, 15] |
| | 623 | |
| | 624 | Also supports iteration, which makes that last example a bit simpler: |
| | 625 | |
| | 626 | >>> list(x) |
| | 627 | [1, 2, 9, 10, 11, 12, 13, 14, 15] |
| | 628 | |
| | 629 | Note that it automatically reduces the list and short-circuits when the |
| | 630 | desired ranges are a relatively small portion of the entire set: |
| | 631 | |
| | 632 | >>> x = Ranges("99") |
| | 633 | >>> 1 in x #really fast |
| | 634 | False |
| | 635 | >>> x = Ranges("1, 2, 1-2, 2") #reduces this to 1-2 |
| | 636 | >>> x.pairs |
| | 637 | [(1, 2)] |
| | 638 | >>> x = Ranges("1-9,2-4") #handle ranges that completely overlap |
| | 639 | >>> list(x) |
| | 640 | [1, 2, 3, 4, 5, 6, 7, 8, 9] |
| | 641 | |
| | 642 | Empty ranges are ok, and ranges can be constructed in pieces, if you |
| | 643 | so choose: |
| | 644 | |
| | 645 | >>> x = Ranges() |
| | 646 | >>> x.appendrange("1, 2, 3") |
| | 647 | >>> x.appendrange("5-9") |
| | 648 | >>> x.appendrange("2-3") #reduce'd away |
| | 649 | >>> list(x) |
| | 650 | [1, 2, 3, 5, 6, 7, 8, 9] |
| | 651 | |
| | 652 | """ |
| | 653 | def __init__(self, r=None): |
| | 654 | self.pairs = [] |
| | 655 | self.appendrange(r) |
| | 656 | |
| | 657 | def appendrange(self, r): |
| | 658 | """ |
| | 659 | Add a range (from a string or None) to the current one |
| | 660 | """ |
| | 661 | if not r: return |
| | 662 | p = self.pairs |
| | 663 | for x in r.split(","): |
| | 664 | try: |
| | 665 | a, b = map(int, x.split("-", 1)) |
| | 666 | except ValueError: |
| | 667 | a, b = int(x), int(x) |
| | 668 | p.append((a, b)) |
| | 669 | self._reduce() |
| | 670 | |
| | 671 | def _reduce(self): |
| | 672 | """ |
| | 673 | Come up with the minimal representation of the ranges |
| | 674 | """ |
| | 675 | d = [] #list of indices to delete |
| | 676 | p = self.pairs |
| | 677 | p.sort() |
| | 678 | for i in range(len(p) - 1): |
| | 679 | if p[i+1][0] <= p[i][1]: #this item overlaps with the next |
| | 680 | #make the first one include the second |
| | 681 | p[i] = (p[i][0], max(p[i][1], p[i+1][1])) |
| | 682 | d.append(i+1) #delete the second on a later pass |
| | 683 | d.reverse() |
| | 684 | for i in d: |
| | 685 | del p[i] |
| | 686 | self.a = p[0][0] #min value |
| | 687 | self.b = p[-1][1] #max value |
| | 688 | |
| | 689 | def __iter__(self): |
| | 690 | """ |
| | 691 | This is another way I came up with to do it. Is it faster? |
| | 692 | |
| | 693 | from itertools import chain |
| | 694 | return chain(*[xrange(a, b+1) for a, b in self.pairs]) |
| | 695 | """ |
| | 696 | for a, b in self.pairs: |
| | 697 | for i in range(a, b+1): |
| | 698 | yield i |
| | 699 | |
| | 700 | def __contains__(self, x): |
| | 701 | if self.a <= x <= self.b: #short-circuit if outside the possible range |
| | 702 | for a, b in self.pairs: |
| | 703 | if a <= x <= b: |
| | 704 | return True |
| | 705 | if b > x: #short-circuit if we've gone too far |
| | 706 | break |
| | 707 | return False |
| | 708 | |
| | 709 | if __name__ == "__main__": |
| | 710 | from doctest import testmod |
| | 711 | testmod() |
| | 712 | |