tags:

views:

127

answers:

6

Ranges in ruby are pretty cool. I end up with arrays such as this:

geneRanges = [(234..25), (500..510), (1640..1653)]

And subsequently have to remove bits of them. For that I:

genePositions = geneRanges.collect {|range| range.entries }.flatten
=> [500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 1640, 1641, 1642, 1643, 1644, 1645, 1646, 1647, 1648, 1649, 1650, 1651, 1652, 1653]

They get manipulated, so some numbers get excluded, and others may be added. I may end up with this:

[505, 506, 507, 600, 601, 602, 603, 1643, 1644, 1645, 1646, 1647, 1648, 1649, 1650, 1651, 1652, 1653, 1654]

How can I convert this back into a compact array of ranges? It seems that the inverse function should exist? I would expect it to return something like this:

[(505..507), (600..603), (1643..1654)]

Thanks!

+1  A: 

I've never seen anything in the Ruby language that does that, but here is some code that might help you do it yourself:

http://snippets.dzone.com/posts/show/4677

Tim
A: 

Untested, clumsy procedural algorithm:

ranges = []
last_range_start = indices[0]
last_index = indices[0]
indices[1..].each do |index|
  if index != last_index + 1
    ranges << (last_range_start..last_index)
    last_range_start = index;
  end
  last_index = index
end
David M.
There is a typo/issue with line 4 `indices[1..].each do |index|`. This throws a syntax error in ruby 1.9.2
Steve Weet
+5  A: 

(New and improved. Stays fresh in your refrigerator for up to two weeks!):

a = [1, 2, 3, 10, 11, 20, 20, 4]

ranges = a.sort.uniq.inject([]) do |spans, n|
  if spans.empty? || spans.last.last != n - 1
    spans + [n..n]
  else
    spans[0..-2] + [spans.last.first..n]
  end
end

p ranges    # [1..4, 10..11, 20..20]
Wayne Conrad
+1 Nice solution, however the value within the else should be `spans[0..-2] + [spans.last.first..n]` (Edited to fix)
Steve Weet
@Steve Weet, Thank you. Good catch.
Wayne Conrad
Excellent - thanks! Check also Steve Weet's generalization and Mladen Jablanović's response further down!
Yannick Wurm
Everyone in this place is smarter than I am, and @Mladen Jablanović consistently knocks them out of the park.
Wayne Conrad
+1  A: 
ar=[505, 506, 507, 600, 1647, 1648, 1649, 1650, 1651, 1654]
def to_range(a)
  s=a[0]
  a.each_cons(2) do |a|
    if a[1]-a[0]!=1
        p s .. a[0]
        s=a[1]
    end
  end
  left=a.index(s)
  p a[left..-1][0]..a[left..-1][-1]
end
to_range(ar)
+5  A: 

Functional, not-very-readable solution:

(a[0,1]+a.each_cons(2).reject{|i,j| j-i==1}.flatten+a[-1,1]).
  each_slice(2).map{|i,j| i..j}

And a nice one:

class Array
  # splits array to sub-arrays wherever two adjacent elements satisfy a condition
  def split_by
    each_cons(2).inject([[first]]){|a, (i, j)|
      a.push([])if yield(i, j)
      a.last.push j
      a
    }
  end

  # uses split_by to split array to subarrays with consecutive elements, then convert to range
  def to_range
    split_by{|i,j| j-i!=1}.map{|a| a.first..a.last}
  end
end

[505, 506, 507, 600, 1647, 1648, 1649, 1650, 1651, 1654].split_by{|i,j| j-i!=1}
#=> [[505, 506, 507], [600], [1647, 1648, 1649, 1650, 1651], [1654]]
[505, 506, 507, 600, 1647, 1648, 1649, 1650, 1651, 1654].to_range
#=> [505..507, 600..600, 1647..1651, 1654..1654]
Mladen Jablanović
that's pretty elegant :)
Yannick Wurm
+3  A: 

This is a straight crib of Wayne Conrads algorithm with a small tweak to make it work for other kinds of ranges, e.g. alphabetic

def array_to_ranges(a)
ranges = a.sort.uniq.inject([]) do |spans, n|
  if spans.empty? || spans.last.last.succ != n
    spans + [n..n]
  else
    spans[0..-2] + [spans.last.first..n]
  end
end
ranges
end

[
  [1..3, 10..11, 20..20, 4..4],
  [ "a".."c", "f".."h", "x".."z"],
  ["aa".."af"]
].each do |arange|
  p arange
  p array = arange.collect {|range| range.to_a}.flatten
  p array_to_ranges(array)
end

And the results of executing this are

[1..3, 10..11, 20..20, 4..4]
[1, 2, 3, 10, 11, 20, 4]
[1..4, 10..11, 20..20]
["a".."c", "f".."h", "x".."z"]
["a", "b", "c", "f", "g", "h", "x", "y", "z"]
["a".."c", "f".."h", "x".."z"]
["aa".."af"]
["aa", "ab", "ac", "ad", "ae", "af"]
["aa".."af"]

Steve Weet
@Steve, this is sweet.
Wayne Conrad
This is very cool. Imagine trying to do it in a language that didn't do duck-typing.
Greg