converting a list of integers into range in python
Question:
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
Answers:
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):
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])
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.
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)]
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.
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)))
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'
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)]
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
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)]
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.
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
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):
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])
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.
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)]
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.
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)))
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'
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)]
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
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)]
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.