47

Is there something existing in python that can convert an increasing list of integers into a range list

E.g. given the set {0, 1, 2, 3, 4, 7, 8, 9, 11} I want to get { {0,4}, {7,9}, {11,11} }.

I can write a program to do this, but want to know if there is an inbuilt function in python

Akhil
  • 2,269
  • 6
  • 32
  • 39
  • 1
    Almost the same question was asked and answered in http://stackoverflow.com/questions/3429510/pythonic-way-to-convert-a-list-of-integers-into-a-string-of-comma-separated-range/3430231#3430231 – Apalala Jan 07 '11 at 19:59
  • Well, I can say with confidence that I don't know of such a function. It is a lot harder to say with confidence that something I'm not aware of doesn't exist.... – Brett Stottlemyer Jan 07 '11 at 17:31
  • I think your proposed result should really be a list of ranges.. cf. my answer below! – Konchog Nov 06 '19 at 12:27

11 Answers11

60

Using itertools.groupby() produces a concise but tricky implementation:

import itertools

def ranges(i):
    for a, b in itertools.groupby(enumerate(i), lambda pair: pair[1] - pair[0]):
        b = list(b)
        yield b[0][1], b[-1][1]

print(list(ranges([0, 1, 2, 3, 4, 7, 8, 9, 11])))

Output:

[(0, 4), (7, 9), (11, 11)]
bossylobster
  • 9,993
  • 1
  • 42
  • 61
  • 4
    This is really useful, I'm wondering if you could explain how this method works so I can understand the functionality. this would be great if possible. – openCivilisation Aug 09 '16 at 00:38
  • 1
    To handle non-unique and non-sorted input surround 'i' with 'sorted(set(i))', see: https://stackoverflow.com/a/43091576/1201614 – luca Oct 05 '17 at 08:07
  • 1
    This recipe is also available in `more_itertools.consecutive_groups`. See demonstration [here](https://stackoverflow.com/a/47642650/4531270). – pylang Dec 04 '17 at 22:03
  • 1 letter variable names is the worst crime! – Smit Johnth Oct 11 '22 at 18:39
15

This is an improvement over the very elegant answer. This one covers non-unique and non-sorted input and is python3 compatible too:

import itertools

def to_ranges(iterable):
    iterable = sorted(set(iterable))
    for key, group in itertools.groupby(enumerate(iterable),
                                        lambda t: t[1] - t[0]):
        group = list(group)
        yield group[0][1], group[-1][1]

Example:

>>> x
[44, 45, 2, 56, 23, 11, 3, 4, 7, 9, 1, 2, 2, 11, 12, 13, 45]

>>> print( list(to_ranges(x))) 
[(1, 4), (7, 7), (9, 9), (11, 13), (23, 23), (44, 45), (56, 56)]
luca
  • 7,178
  • 7
  • 41
  • 55
14

You can use a list comprehension with a generator expression and a combination of enumerate() and itertools.groupby():

>>> import itertools
>>> l = [0, 1, 2, 3, 4, 7, 8, 9, 11]
>>> [[t[0][1], t[-1][1]] for t in
... (tuple(g[1]) for g in itertools.groupby(enumerate(l), lambda (i, x): i - x))]
[[0, 4], [7, 9], [11, 11]]

First, enumerate() will build tuples from the list items and their respective index:

>>> [t for t in enumerate(l)]
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 7), (6, 8), (7, 9), (8, 11)]

Then groupby() will group those tuples using the difference between their index and their value (which will be equal for consecutive values):

>>> [tuple(g[1]) for g in itertools.groupby(enumerate(l), lambda (i, x): i - x)]
[((0, 0), (1, 1), (2, 2), (3, 3), (4, 4)), ((5, 7), (6, 8), (7, 9)), ((8, 11),)]

From there, we only need to build lists from the values of the first and last tuples of each group (which will be the same if the group only contains one item).

You can also use [(t[0][1], t[-1][1]) ...] to build a list of range tuples instead of nested lists, or even ((t[0][1], t[-1][1]) ...) to turn the whole expression into a iterable generator that will lazily build the range tuples on the fly.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • 1
    In which Python versions does the lambda argument unpacking work? `python3.9 -c 'fn1 = lamba (a, b): a + b'` `SyntaxError: invalid syntax` --- I think it was only in the obsolete Python 2. See https://stackoverflow.com/questions/21892989/what-is-the-good-python3-equivalent-for-auto-tuple-unpacking-in-lambda --- OK, I have found the PEP for this: https://www.python.org/dev/peps/pep-3113/ --- IMHO the answer should be fixed. – pabouk - Ukraine stay strong Jan 02 '22 at 13:12
4

Generating range pairs:

def ranges(lst):
    s = e = None
    r = []
    for i in sorted(lst):
        if s is None:
            s = e = i
        elif i == e or i == e + 1:
            e = i
        else:
            r.append((s, e))
            s = e = i
    if s is not None:
        r.append((s, e))
    return r

Example:

>>> lst = [1, 5, 6, 7, 12, 15, 16, 17, 18, 30]
>>> print repr(ranges(lst))
[(1, 1), (5, 7), (12, 12), (15, 18), (30, 30)]

As a generator:

def gen_ranges(lst):
    s = e = None
    for i in sorted(lst):
        if s is None:
            s = e = i
        elif i == e or i == e + 1:
            e = i
        else:
            yield (s, e)
            s = e = i
    if s is not None:
        yield (s, e)

Example:

>>> lst = [1, 5, 6, 7, 12, 15, 16, 17, 18, 30]
>>> print repr(','.join(['%d' % s if s == e else '%d-%d' % (s, e) for (s, e) in gen_ranges(lst)]))
'1,5-7,12,15-18,30'
Curt
  • 470
  • 3
  • 7
3

This generator:

def ranges(p):
    q = sorted(p)
    i = 0
    for j in xrange(1,len(q)):
        if q[j] > 1+q[j-1]:
            yield (q[i],q[j-1])
            i = j
    yield (q[i], q[-1])

sample = [0, 1, 2, 3, 4, 7, 8, 9, 11]
print list(ranges(sample))
print list(ranges(reversed(sample)))
print list(ranges([1]))
print list(ranges([2,3,4]))
print list(ranges([0,2,3,4]))
print list(ranges(5*[1]))

Produces these results:

[(0, 4), (7, 9), (11, 11)]
[(0, 4), (7, 9), (11, 11)]
[(1, 1)]
[(2, 4)]
[(0, 0), (2, 4)]
[(1, 1)]

Note that runs of repeated numbers get compressed. I don't know if that's what you want. If not, change the > to a !=.

I understand your question. I looked into itertools and tried to think of a solution that could be done in a couple of lines of Python, which would have qualified as "almost a built in", but I couldn't come up with anything.

Apalala
  • 9,017
  • 3
  • 30
  • 48
3

As there hasn't been a new answer for 2 years or so, here's one for zombie lovers!

If you don't want to use itertools or a generator, the following uses logic(!). It uses a set (cf. the question!) for input and returns a list of proper ranges as a result; it's easy enough to adjust the code to suit though.

def ranges(l_set: set) ->list:
    rb_set = sorted(l_set - {i +1 for i in l_set})
    re_set = sorted(l_set - {i -1 for i in l_set})
    return [range(rb_set[i], re_set[i]+1) for i in range(len(rb_set))]

For example:

>>>ranges({6, 9, 10, 7, 8, 2, 3, 14})
[range(2, 4), range(6, 11), range(14, 15)]

>>>ranges({6, 7, 3, 15, 8, 5, 12, 0, 12, 7, 15, 6, 14, 8, 16})
[range(0, 1), range(3, 4), range(5, 9), range(12, 13), range(14, 17)]
Konchog
  • 1,920
  • 19
  • 23
  • 3
    As an improvement I suggest to use `rb_set = sorted(l_set.difference(i+1 for i in l_set))` as it avoids creating another temporary set in memory. Also for the final list you can use `[range(b, e+1) for b, e in zip(rb_set, re_set)]` or if you want to have tuples instead just `list(zip(rb_set, re_set))`. – a_guest Jan 21 '20 at 11:29
2

Related questions for the case when step sizes other than 1 are of interest and a near duplicate of this question here. A solution for either case that performs well is given here.

smichr
  • 16,948
  • 2
  • 27
  • 34
1

Put it shorter:

ranges=lambda l:map(lambda x:(x[0][1],x[-1][1]),map(lambda (x,y):list(y),itertools.groupby(enumerate(l),lambda (x,y):x-y)))
Luca
  • 9,259
  • 5
  • 46
  • 59
Neuer
  • 33
  • 2
1

I think the other answers are hard to understand, and probably inefficient. Hope this is easier and faster.

def ranges(ints):
    ints = sorted(set(ints))
    range_start = previous_number = ints[0]
    for number in ints[1:]:
        if number == previous_number + 1:
            previous_number = number
        else:
            yield range_start, previous_number
            range_start = previous_number = number
    yield range_start, previous_number
Mike Amy
  • 351
  • 3
  • 4
1

Nothing built-in, or in any libraries that I know of. Not very helpful, I know, but I've never come across anything like what you want.

Here are some ideas for your program atleast (in C++, but it can give you some other ideas):

Converting sets of integers into ranges

Community
  • 1
  • 1
Mark Loeser
  • 17,657
  • 2
  • 26
  • 34
1

In the case there is no such feature in python, here is an implementation

p = []
last = -2                                                            
start = -1

for item in list:
    if item != last+1:                        
        if start != -1:
            p.append([start, last])
        start = item
    last = item

p.append([start, last])
Akhil
  • 2,269
  • 6
  • 32
  • 39