Skip to content Skip to sidebar Skip to footer

How To Do An Inverse `range`, I.e. Create A Compact Range Based On A Set Of Numbers?

Python has a range method, which allows for stuff like: >>> range(1, 6) [1, 2, 3, 4, 5] What I’m looking for is kind of the opposite: take a list of numbers, and return

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

defsub(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)
     iflen(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 # firstandlast number ofcurrent consecutive rangefor item in sorted(input):
  if firstisNone:
    first=last= item # bootstrap
  elif item ==last+1: # consecutive
    last= item # extend the rangeelse: # not consecutive
    output.append((first, last)) # pack up the rangefirst=last= item
# the lastrange 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 [123910])

(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:
    ifnotisinstance(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 inzip(l,l[1:]))

# get the gap indices
gaps = (i for i,e inenumerate(d) if e != 1)

# get the range boundariesdefget_ranges(gaps, l):
  last_idx = -1for 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 elsestr(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?"