Commits

Jonathan Eunice committed 4209cb8

fixed parsing of negative integers; added ranges method

Comments (0)

Files changed (4)

     3
     5
 
+Most set operations such as intersection, union, and so on are available just
+as they are in Python's ``set``. In addition, if you wish to extract the
+contiguous ranges::
+
+    for r in intspan('1-3,5,7-9,10,21-22,23,24').ranges():
+        print r                 # Python 2
+        
+yields::
+
+    (1, 3)
+    (5, 5)
+    (7, 10)
+    (21, 24)
+
 Performance
 ===========
 
 
  *  `cowboy <http://pypi.python.org/pypi/cowboy>`_ provides
     generalized ranges and multi-ranges. Bonus points
-    for the package name:
+    for the package tagline:
     "It works on ranges."
     
  *  `rangeset <http://pypi.python.org/pypi/rangeset>`_ is a generalized range set
 Notes
 =====
 
+ *  Version 0.7 fixed parsing of spans including negative numbers, and
+    added the ``ranges()`` method.
+    
  *  Though inspired by Perl's `Set::IntSpan <http://search.cpan.org/~swmcd/Set-IntSpan-1.16/IntSpan.pm>`_,
     that's where the similarity stops.
     ``intspan`` supports only finite sets, and it
     inlcuding arguments that are technically string specifications rather than
     proper ``intspan`` objects. 
     
- *  String representation based on Jeff Mercado's concise answer to `this
+ *  String representation and ``ranges()`` method
+    based on Jeff Mercado's concise answer to `this
     StackOverflow question <http://codereview.stackexchange.com/questions/5196/grouping-consecutive-numbers-into-ranges-in-python-3-2>`_.
     Thank you, Jeff!
 
 
 import sys, copy
 from itertools import groupby, count
+import re
 
 _PY3 = sys.version_info[0] > 2
 if _PY3:
     basestring = str
+    
+SPANRE = re.compile(r'^\s*(?P<start>-?\d+)\s*(-\s*(?P<stop>-?\d+))?\s*$')
         
 class intspan(set):
     def __init__(self, initial=None):
     
     @staticmethod
     def _parse_range(datum):
+        
+        def parse_chunk(chunk):
+            """
+            Parse each comma-separated chunk. Hyphens (-) can indicate ranges,
+            or negative numbers. Returns a list of specified values. NB Designed
+            to parse correct input correctly. Results of incorrect input are
+            undefined.
+            """
+            m = SPANRE.search(chunk)
+            if m:
+                start = int(m.group('start'))
+                if m.group('stop'):
+                    stop = int(m.group('stop'))
+                    return list(range(start, stop+1))
+                return [ start ]
+            else:
+                raise ValueError("Can't parse chunk '{0}'".format(chunk))
+        
         if isinstance(datum, basestring):
             result = []
             for part in datum.split(','):
-                if '-' in part:
-                    start, stop = part.split('-')
-                    result.extend(list(range(int(start), int(stop)+1)))
-                else:
-                    result.append(int(part))
+                result.extend(parse_chunk(part))
             return result
         else:
             return datum if hasattr(datum, '__iter__') else [ datum ]
     def _as_range(iterable): 
         l = list(iterable)
         if len(l) > 1:
+            return (l[0], l[-1])
+        else:
+            return (l[0], l[0])
+
+    @staticmethod
+    def _as_range_str(iterable): 
+        l = list(iterable)
+        if len(l) > 1:
             return '{0}-{1}'.format(l[0], l[-1])
         else:
             return '{0}'.format(l[0])
 
-    def __repr__(self):        
+    def __repr__(self):
+        """
+        Return the representation.
+        """
         return 'intspan({0!r})'.format(self.__str__())
     
     def __str__(self):
+        """
+        Return the stringification.
+        """
         items = sorted(self)
-        return ','.join(self._as_range(g) for _, g in groupby(items, key=lambda n, c=count(): n-next(c)))
+        return ','.join(self._as_range_str(g) for _, g in groupby(items, key=lambda n, c=count(): n-next(c)))
+
+    def ranges(self):
+        """
+        Return a list of the set's contiguous (inclusive) ranges.
+        """
+        items = sorted(self)
+        return [ self._as_range(g) for _, g in groupby(items, key=lambda n, c=count(): n-next(c)) ]
 
     # see Jeff Mercado's answer to http://codereview.stackexchange.com/questions/5196/grouping-consecutive-numbers-into-ranges-in-python-3-2
     # see also: http://stackoverflow.com/questions/2927213/python-finding-n-consecutive-numbers-in-a-list
 #! /usr/bin/env python
 from setuptools import setup
-import sys
-
-def verno(s):
-    """
-    Update the version number passed in by extending it to the 
-    thousands place and adding 1/1000, then returning that result
-    and as a side-effect updating setup.py
-
-    Dangerous, self-modifying, and also, helps keep version numbers
-    ascending without human intervention.
-    """
-    
-    from decimal import Decimal
-    import re
-    d = Decimal(s)
-    increment = Decimal('0.001')
-    d = d.quantize(increment) + increment
-    dstr = str(d)
-    setup = open('setup.py', 'r').read()
-    setup = re.sub('verno\(\w*[\'"]([\d\.]+)[\'"]', 'verno("' + dstr + '"', setup)
-    open('setup.py', 'w').write(setup)
-    return dstr
 
 def linelist(text):
     """
     Returns each non-blank line in text enclosed in a list.
     """
-    return [ l.strip() for l in text.strip().splitlines() if l.split() ]
+    return [ l.strip() for l in text.strip().splitlines() if l.strip() ]
     
-    # The double-mention of l.strip() is yet another fine example of why
-    # Python needs en passant aliasing.
 
 setup(
     name='intspan',
-    version=verno("0.6"),
+    version='0.7',
     author='Jonathan Eunice',
     author_email='jonathan.eunice@gmail.com',
     description="Sets of integers like 1,3-7,33. Inspired by Perl's Set::IntSpan",

test/test_intspan.py

         s = intspan(t)
         assert str(s) == t
         
+def test_negatives():
+    assert list(intspan('-2')) == [-2]
+    assert list(intspan('-2-1')) == [-2, -1, 0, 1]
+    assert list(intspan('-2--1')) == [-2, -1]
+        
 def test_contains():
     s = intspan()
     assert 1 not in s
     assert s.pop() == 104
     assert s.pop() == 105
     assert s == intspan('106-110')
+    
+def test_ranges():
+    assert intspan('2').ranges()   == [ (2,2) ]
+    assert intspan('1-3').ranges() == [ (1,3) ]
+    assert intspan('1-3,5-6').ranges() == [ (1,3), (5,6) ]
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.