9

I'd like to take input such as:

[1,2,4,5,6,7,9,13]

and turn it into something like the following:

[[1,2],[4,7],[9,9],[13,13]]

Each sub-array represents a range of integers.

5
  • 2
    Are you asking if there is code to do this already? Are you asking because you're trying to roll your own and having trouble implementing it? Commented Dec 24, 2011 at 0:11
  • I'm rolling my own. Seems there are always interesting ways to implement this kind of thing in Ruby. Commented Dec 24, 2011 at 0:14
  • By which conditions are the ranges supposed to be built? Commented Dec 24, 2011 at 0:18
  • A continuous integer sequence in the array should compose a "range" which is really just an array with the start and end. Commented Dec 24, 2011 at 0:19
  • 1
    possible duplicate of Array of indexes to array of ranges Commented Dec 24, 2011 at 5:16

5 Answers 5

21

Functional approach using Enumerable#chunk:

ranges = [1, 2, 4, 5, 6, 7, 9, 13]
  .enum_for(:chunk) # .chunk for Ruby >= 2.4
  .with_index { |x, idx| x - idx }
  .map { |_diff, group| [group.first, group.last] }

#=> [[1, 2], [4, 7], [9, 9], [13, 13]]

How it works: once indexed, consecutive elements in the array have the same x - idx, so we use that value to chunk (grouping of consecutive items) the input array. Finally we just need to take the first and last elements of each group to build the pairs.

Sign up to request clarification or add additional context in comments.

5 Comments

This looks really nice. Totally forgot about the new chunk method.
And to take it one more step, .map{ |min,max| min == max ? min : min .. max } will result in: [1..2, 4..7, 9, 13].
Or, change [pairs.first[0], pairs.last[0]] to pairs.first[0] .. pairs.last[0] to get ranges in all positions: [1..2, 4..7, 9..9, 13..13].
Image
In Ruby 2.4, the enum_for will no longer be necessary.
Image
I also posted an even easier solution with chunk_while
5

This is almost straight from the enumerable#slice_before method documentation:

ar = [1,2,4,5,6,7,9,13]
prev = ar[0]
ar.slice_before{|e|prev,prev2 = e,prev; prev2.succ != e}.map{|a|a.first..a.last}
#=> [1..2, 4..7, 9..9, 13..13]

This should work with characters, dates, anything with a .succ method.

Comments

5

An even easier solution than @tokland's very nice one is using chunk_while:

xs.chunk_while { |a, b| a + 1 == b }.map do |seq|
  [seq.first, seq.last]
end

Note: chunk_while was introduced in Ruby 2.3

Comments

3

Hmm, well, it's not tokland's masterpiece, but I think it may be a good straightforward solution...

[1,2,4,5,6,7,9,13].inject([]) do |m, v|
  if m.last.to_a.last == v.pred
    m[-1][-1] = v
  else
    m << [v, v]
  end
  m
end

Comments

0

Another approach

def summarize(x)
  x.inject([]) do |acc, value|
    if acc.last && acc.last[1] + 1 == value
      acc.last[1] = value
      acc
    else
      acc << [value,value]
    end
  end
end

Similar to Larsenal's method but using inject to manage the boring stuff.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.