How To Do An Inverse `range`, I.e. Create A Compact Range Based On A Set Of Numbers?
Solution 1:
A nice trick to simplify the code is to look at the difference of each element of the sorted list and its index:
a = [4, 2, 1, 5]
a.sort()
print [x - i for i, x in enumerate(a)]
prints
[1, 1, 2, 2]
Each run of the same number corresponds to a run of consecutive numbers in a
. We can now use itertools.groupby()
to extract these runs. Here's the complete code:
from itertools import groupby
def sub(x):
return x[1] - x[0]
a = [5, 3, 7, 4, 1, 2, 9, 10]
ranges = []
for k, iterable in groupby(enumerate(sorted(a)), sub):
rng = list(iterable)
if len(rng) == 1:
s = str(rng[0][1])
else:
s = "%s-%s" % (rng[0][1], rng[-1][1])
ranges.append(s)
print ranges
printing
['1-5', '7', '9-10']
Solution 2:
Sort numbers, find consecutive ranges (remember RLE compression?).
Something like this:
input = [5,7,9,8,6, 21,20, 3,2,1, 22,23, 50]
output = []
first = last = None # first and last number of current consecutive range
for item in sorted(input):
if first is None:
first = last = item # bootstrap
elif item == last + 1: # consecutive
last = item # extend the range
else: # not consecutive
output.append((first, last)) # pack up the range
first = last = item
# the last range ended by iteration end
output.append((first, last))
print output
Result: [(1, 3), (5, 9), (20, 23), (50, 50)]
. You figure out the rest :)
Solution 3:
I thought you might like my generalised clojure solution.
(def r [1 2 3 9 10])
(defn successive? [a b]
(= a (dec b)))
(defn break-on [pred s]
(reduce (fn [memo n]
(if (empty? memo)
[[n]]
(if (pred (last (last memo)) n)
(conj (vec (butlast memo))
(conj (last memo) n))
(conj memo [n]))))
[]
s))
(break-on successive? r)
Solution 4:
Since 9000 beat me to it, I'll just post the second part of the code, that prints pcre-like ranges from the previously computed output
plus the added type check:
for i in output:
if not isinstance(i, int) or i < 0:
raise Exception("Only positive ints accepted in pcre_ranges")
result = [ str(x[0]) if x[0] == x[1] else '%s-%s' % (x[0], x[1]) for x in output ]
print result
Output: ['1-3', '5-9', '20-23', '50']
Solution 5:
Let's try generators!
# ignore duplicate values
l = sorted( set( [5,7,9,8,6, 21,20, 3,2,1, 22,23, 50] ) )
# get the value differences
d = (i2-i1 for i1,i2 in zip(l,l[1:]))
# get the gap indices
gaps = (i for i,e in enumerate(d) if e != 1)
# get the range boundaries
def get_ranges(gaps, l):
last_idx = -1
for i in gaps:
yield (last_idx+1, i)
last_idx = i
yield (last_idx+1,len(l)-1)
# make a list of strings in the requested format (thanks Frg!)
ranges = [ "%s-%s" % (l[i1],l[i2]) if i1!=i2 else str(l[i1]) \
for i1,i2 in get_ranges(gaps, l) ]
This has become rather scary, I think :)
Post a Comment for "How To Do An Inverse `range`, I.e. Create A Compact Range Based On A Set Of Numbers?"