Commits

Jonathan Eunice committed d1a3f03

initial commit; already working

  • Participants

Comments (0)

Files changed (9)

+syntax: glob
+*.swp.{py,txt,html,css,js}
+*.swp
+*.pyc
+.tox
+.DS_Store
+build/*
+dist/*
+*.egg-info
+setup.cfg
+PKG-INFO
+include MANIFEST.in
+include README
+include README.rst
+include pytest.ini
+include tox.ini
+recursive-include test *.py
+
+See README.rst
+Subset of ``set`` designed to conveniently hold sets of integers. It 
+creates them from, and displays them as, 
+integer spans (e.g. ``1-3,14,29,92-97``). When iterating over an ``intspan``,
+iteration is ordered. 
+
+The main draw is that this provides a convenient way
+to specify ranges--for example, ranges of rows to be processed in a spreadsheet.
+
+
+Usage
+=====
+
+::
+
+    from intspan import intspan
+    
+    s = intspan('1-3,14,29,92-97')
+    s.discard('2,13,92')
+    print s
+
+yields::
+
+    1,3,14,29,93-97
+    
+While::
+
+    for n in intspan('1-3,5'):
+        print n                 # Python 2
+        
+yields::
+
+    1
+    2
+    3
+    5
+
+Performance
+===========
+
+`intspan`` piggybacks Python's ``set``, so it stores every integer individually.
+Unlike Perl's ``Set::IntSpan`` it is not
+optimized for long contiguous runs. For sets of several hundred or thousand
+members, you will probably never notice the difference.
+
+On the other hand, if you're dealing
+with large sets (e.g. with 10,000 or more elements), or doing lots of set operations
+on them (e.g. union, intersection), a data structure based on
+lists of ranges, `run length encoding
+<http://en.wikipedia.org/wiki/Run-length_encoding>`_, or `Judy arrays
+<http://en.wikipedia.org/wiki/Judy_array>`_ would probably perform / scale
+better.
+
+Alternatives
+============
+
+There are several generally available modules you might want to consider as
+alternatives or supplements. None of them provide (AFAIK) the convenient
+integer span specification that ``intspan`` does, but they have other virtues:
+
+ *  `cowboy <http://pypi.python.org/pypi/cowboy>`_ provides
+    generalized ranges and multi-ranges. Bonus points
+    for the package name:
+    "It works on ranges."
+    
+ *  `rangeset <http://pypi.python.org/pypi/rangeset>`_ is a generalized range set
+    module. It also supports infinite ranges.
+    
+ *  `judy <http://pypi.python.org/pypi/judy>`_ a Python wrapper around Judy arrays
+    that are implemented in C. No docs or tests to speak of.
+
+Notes
+=====
+
+ *  Some ``set`` operations such as ``update()``, ``add()``, ``discard()``, and
+    ``remove()`` take integer span strings as arguments. In some cases this
+    changes a method that took one item into one that takes multiple. Not all
+    ``set`` operations have been so extended.
+    
+ *  String representation 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!
+
+ *  Automated multi-version testing managed with the wonderful
+    `pytest <http://pypi.python.org/pypi/pytest>`_
+    and `tox <http://pypi.python.org/pypi/tox>`_. 
+    Successfully packaged for, and tested against, all late-model verions of
+    Python: 2.6, 2.7, 3.2, and 3.3, as well as PyPy 1.9 (based on 2.7.2).
+ 
+ *  The author, `Jonathan Eunice <mailto:jonathan.eunice@gmail.com>`_ or
+    `@jeunice on Twitter <http://twitter.com/jeunice>`_
+    welcomes your comments and suggestions.
+
+Installation
+============
+
+To install the latest version::
+
+    pip install -U intspan
+
+To ``easy_install`` under a specific Python version (3.3 in this example)::
+
+    python3.3 -m easy_install --upgrade intspan
+    
+(You may need to prefix these with "sudo " to authorize installation.)
+
+import sys
+from itertools import groupby, count
+
+
+_PY3 = sys.version_info[0] > 2
+if _PY3:
+    basestring = str
+    
+def _parse_range(datum):
+    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))
+        return result
+    else:
+        return datum if hasattr(datum, '__iter__') else [ datum ]
+
+def _as_range(iterable): 
+    l = list(iterable)
+    if len(l) > 1:
+        return '{0}-{1}'.format(l[0], l[-1])
+    else:
+        return '{0}'.format(l[0])
+    
+class intspan(set):
+    def __init__(self, initial=None):
+        set.__init__(self)
+        if initial:
+            self.update(initial)
+        
+    def update(self, items):
+        set.update(self, _parse_range(items))
+        
+    def discard(self, items):
+        for item in _parse_range(items):
+            set.discard(self, item)
+            
+    def remove(self, items):
+        for item in _parse_range(items):
+            set.remove(self, item)
+            
+    def add(self, items):
+        for item in _parse_range(items):
+            set.add(self, item)
+                
+    def __iter__(self):
+        """
+        Iterate in ascending order.
+        """
+        return iter(sorted(set.__iter__(self)))
+
+    def __repr__(self):        
+        return 'intspan({0!r})'.format(self.__str__())
+    
+    def __str__(self):
+        items = sorted(self)
+        return ','.join(_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
+
+    
+[pytest]
+python_files = test/*.py
+#! /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() ]
+    
+    # The double-mention of l.strip() is yet another fine example of why
+    # Python needs en passant aliasing.
+
+setup(
+    name='intspan',
+    version=verno("0.112"),
+    author='Jonathan Eunice',
+    author_email='jonathan.eunice@gmail.com',
+    description="Sets of integers like 1,3-7,33. Inspired by Perl's Set::IntSpan",
+    long_description=open('README.rst').read(),
+    url='https://bitbucket.org/jeunice/intspan',
+    py_modules=['intspan'],
+    install_requires=[],
+    tests_require = ['tox', 'pytest'],
+    zip_safe = True,
+    keywords='integer set span range intspan intrange',
+    classifiers=linelist("""
+        Development Status :: 3 - Alpha
+        Operating System :: OS Independent
+        License :: OSI Approved :: BSD License
+        Intended Audience :: Developers
+        Programming Language :: Python
+        Programming Language :: Python :: 2.6
+        Programming Language :: Python :: 2.7
+        Programming Language :: Python :: 3
+        Programming Language :: Python :: 3.2
+        Programming Language :: Python :: 3.3
+        Programming Language :: Python :: Implementation :: CPython
+        Programming Language :: Python :: Implementation :: PyPy
+        Topic :: Software Development :: Libraries :: Python Modules
+    """)
+)

test/test_intspan.py

+
+from intspan import intspan
+
+def test_basic():
+    tests = ['1','1-2', '1-3,9-10', '1-3,14,29,92-97']
+    for t in tests:
+        s = intspan(t)
+        assert str(s) == t
+        
+def test_merge():
+    assert str(intspan('1-4,5')) ==  '1-5'
+        
+def test_out_of_order():
+    assert str(intspan('1,0,99,4,7,9,98')) == '0-1,4,7,9,98-99'
+
+def test_discard():
+    s = intspan('1-3,14,29,92-97')
+    s.discard('2,13,92')
+    assert str(s) == '1,3,14,29,93-97'
+
+def test_add():
+    s = intspan('1-2')
+    s.add('3,29')
+    assert str(s) == '1-3,29'
+    s.add('92,97,96,95,94')
+    assert str(s) == '1-3,29,92,94-97'
+    s.add(93)
+    assert str(s) == '1-3,29,92-97'
+    s.add('14')
+    assert str(s) == '1-3,14,29,92-97'
+    
+def test_iteration():
+    
+    s = intspan('92,97,96,95,0,94')
+    l = [ item for item in s ]
+    assert l == [0, 92, 94, 95, 96, 97]
+[tox]
+envlist = py26, py27, py32, py33, pypy
+
+[testenv]
+changedir=test
+deps=
+    pytest 
+commands=py.test {posargs}