Pythonic way to convert a list of integers into a string of comma-separated ranges

后端 未结 7 765
感情败类
感情败类 2020-12-01 05:06

I have a list of integers which I need to parse into a string of ranges.

For example:

 [0, 1, 2, 3] -> \"0-3\"
 [0, 1, 2, 4, 8] -> \"0-2,4,8\"
         


        
相关标签:
7条回答
  • 2020-12-01 05:15

    To concatenate strings you should use ','.join. This removes the 2nd loop.

    def createRangeString(zones):
            rangeIdx = 0
            ranges   = [[zones[0], zones[0]]]
            for zone in list(zones):
                if ranges[rangeIdx][1] in (zone, zone-1):
                    ranges[rangeIdx][1] = zone
                else:
                    ranges.append([zone, zone])
                    rangeIdx += 1
    
           return ','.join(
                    map(
                      lambda p: '%s-%s'%tuple(p) if p[0] != p[1] else str(p[0]),
                      ranges
                    )
                  )
    

    Although I prefer a more generic approach:

    from itertools import groupby
    
    # auxiliary functor to allow groupby to compare by adjacent elements.
    class cmp_to_groupby_key(object):
      def __init__(self, f):
        self.f = f
        self.uninitialized = True
      def __call__(self, newv):
        if self.uninitialized or not self.f(self.oldv, newv):
          self.curkey = newv
          self.uninitialized = False
        self.oldv = newv
        return self.curkey
    
    # returns the first and last element of an iterable with O(1) memory.
    def first_and_last(iterable):
      first = next(iterable)
      last = first
      for i in iterable:
        last = i
      return (first, last)
    
    # convert groups into list of range strings
    def create_range_string_from_groups(groups):
      for _, g in groups:
        first, last = first_and_last(g)
        if first != last:
          yield "{0}-{1}".format(first, last)
        else:
          yield str(first)
    
    def create_range_string(zones):
      groups = groupby(zones, cmp_to_groupby_key(lambda a,b: b-a<=1))
      return ','.join(create_range_string_from_groups(groups))
    
    assert create_range_string([0,1,2,3]) == '0-3'
    assert create_range_string([0, 1, 2, 4, 8]) == '0-2,4,8'
    assert create_range_string([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44]) == '1-4,6-9,12-13,19-20,22-23,40,44'
    
    0 讨论(0)
  • 2020-12-01 05:24
    def createRangeString(zones):
        """Create a string with integer ranges in the format of '%d-%d'
        >>> createRangeString([0, 1, 2, 4, 8])
        "0-2,4,8"
        >>> createRangeString([1,2,3,4,6,7,8,9,12,13,19,20,22,22,22,23,40,44])
        "1-4,6-9,12-13,19-20,22-23,40,44"
        """
        buffer = []
    
        try:
            st = ed = zones[0]
            for i in zones[1:]:
                delta = i - ed
                if delta == 1: ed = i
                elif not (delta == 0):
                    buffer.append((st, ed))
                    st = ed = i
            else: buffer.append((st, ed))
        except IndexError:
            pass
    
        return ','.join(
                "%d" % st if st==ed else "%d-%d" % (st, ed)
                for st, ed in buffer)
    
    0 讨论(0)
  • 2020-12-01 05:29

    how about this mess...

    def rangefy(mylist):
        mylist, mystr, start = mylist + [None], "", 0
        for i, v in enumerate(mylist[:-1]):
                if mylist[i+1] != v + 1:
                        mystr += ["%d,"%v,"%d-%d,"%(start,v)][start!=v]
                        start = mylist[i+1]
        return mystr[:-1]
    
    0 讨论(0)
  • 2020-12-01 05:31
    >>> from itertools import count, groupby
    >>> L=[1, 2, 3, 4, 6, 7, 8, 9, 12, 13, 19, 20, 22, 23, 40, 44]
    >>> G=(list(x) for _,x in groupby(L, lambda x,c=count(): next(c)-x))
    >>> print ",".join("-".join(map(str,(g[0],g[-1])[:len(g)])) for g in G)
    1-4,6-9,12-13,19-20,22-23,40,44
    

    The idea here is to pair each element with count(). Then the difference between the value and count() is constant for consecutive values. groupby() does the rest of the work

    As Jeff suggests, an alternative to count() is to use enumerate(). This adds some extra cruft that needs to be stripped out in the print statement

    G=(list(x) for _,x in groupby(enumerate(L), lambda (i,x):i-x))
    print ",".join("-".join(map(str,(g[0][1],g[-1][1])[:len(g)])) for g in G)
    

    Update: for the sample list given here, the version with enumerate runs about 5% slower than the version using count() on my computer

    0 讨论(0)
  • 2020-12-01 05:31

    This is more verbose, mainly because I have used generic functions that I have and that are minor variations of itertools functions and recipes:

    from itertools import tee, izip_longest
    def pairwise_longest(iterable):
        "variation of pairwise in http://docs.python.org/library/itertools.html#recipes"
        a, b = tee(iterable)
        next(b, None)
        return izip_longest(a, b)
    
    def takeuntil(predicate, iterable):
        """returns all elements before and including the one for which the predicate is true
        variation of http://docs.python.org/library/itertools.html#itertools.takewhile"""
        for x in iterable:
            yield x
            if predicate(x):
                break
    
    def get_range(it):
        "gets a range from a pairwise iterator"
        rng = list(takeuntil(lambda (a,b): (b is None) or (b-a>1), it))
        if rng:
            b, e = rng[0][0], rng[-1][0]
            return "%d-%d" % (b,e) if b != e else "%d" % b
    
    def create_ranges(zones):
        it = pairwise_longest(zones)
        return ",".join(iter(lambda:get_range(it),None))
    
    k=[0,1,2,4,5,7,9,12,13,14,15]
    print create_ranges(k) #0-2,4-5,7,9,12-15
    
    0 讨论(0)
  • 2020-12-01 05:32

    Here is my solution. You need to keep track of various pieces of information while you iterate through the list and create the result - this screams generator to me. So here goes:

    def rangeStr(start, end):
        '''convert two integers into a range start-end, or a single value if they are the same''' 
        return str(start) if start == end else "%s-%s" %(start, end)
    
    def makeRange(seq):
        '''take a sequence of ints and return a sequence
        of strings with the ranges
        '''
        # make sure that seq is an iterator
        seq = iter(seq)
        start = seq.next()
        current = start
        for val in seq:
            current += 1
            if val != current:
                yield rangeStr(start, current-1)
                start = current = val
        # make sure the last range is included in the output
        yield rangeStr(start, current)
    
    def stringifyRanges(seq):
        return ','.join(makeRange(seq))
    
    >>> l = [1,2,3, 7,8,9, 11, 20,21,22,23]
    >>> l2 = [1,2,3, 7,8,9, 11, 20,21,22,23, 30]
    >>> stringifyRanges(l)
    '1-3,7-9,11,20-23'
    >>> stringifyRanges(l2)
    '1-3,7-9,11,20-23,30'
    

    My version will work correctly if given an empty list, which I think some of the others will not.

    >>> stringifyRanges( [] )
    ''
    

    makeRanges will work on any iterator that returns integers and lazily returns a sequence of strings so can be used on infinite sequences.

    edit: I have updated the code to handle single numbers that are not part of a range.

    edit2: refactored out rangeStr to remove duplication.

    0 讨论(0)
提交回复
热议问题