3

I have an array of numbers as below:

[11, 12, 13, 14, 19, 20, 21, 29, 30, 33]

I would like to reduce this array to:

[[11,14], [19,21], [29,30], [33,33]]

Identify consequent numbers in an array and push only the start and end of its ranges.

How to achieve this?

1
  • 2
    Please post any attempt you've made. Commented Aug 5, 2014 at 20:49

5 Answers 5

4

Exactly some problem is solved to give an example for slice_before method in ruby docs:

a = [0, 2, 3, 4, 6, 7, 9]
prev = a[0]
p a.slice_before { |e|
  prev, prev2 = e, prev
  prev2 + 1 != e
}.map { |es|
  es.length <= 2 ? es.join(",") : "#{es.first}-#{es.last}"
}.join(",")

In your case you need to tweak it a little:

a = [11, 12, 13, 14, 19, 20, 21, 29, 30, 33]
prev = a[0]
p a.slice_before { |e|
  prev, prev2 = e, prev
  prev2 + 1 != e
}.map { |es|
  [es.first, es.last]
}
Sign up to request clarification or add additional context in comments.

1 Comment

If you replace prev2+1 with prev2.succ, it will work for arrays that contain any objects that respond to succ. For example, if a = ['a','b','d','e'], it will return [["a", "b"], ["d", "e"]].
3

Here's another way, using an enumerator with Enumerator#next and Enumerator#peek. It works for any collection that implements succ (aka next).

Code

def group_consecs(a)
  enum = a.each
  pairs = [[enum.next]]
  loop do
    if pairs.last.last.succ == enum.peek
      pairs.last << enum.next 
    else
      pairs << [enum.next]
    end
  end
  pairs.map { |g| (g.size > 1) ? g : g*2 }
end

Note that Enumerator#peek raises a StopInteration exception if the enumerator enum is already at the end when enum.peek is invoked. That exception is handled by Kernel#loop, which breaks the loop.

Examples

a = [11, 12, 13, 14, 19, 20, 21, 29, 30, 33]
group_consecs(a)
  #=> [[11, 12, 13, 14], [19, 20, 21], [29, 30], [33, 33]]

a = ['a','b','c','f','g','i','l','m']
group_consecs(a)
  #=> [["a", "b", "c"], ["f", "g"], ["i", "i"], ["l", "m"]]

a = ['aa','ab','ac','af','ag','ai','al','am']
group_consecs(a)
  #=> [["aa", "ab", "ac"], ["af", "ag"], ["ai, ai"], ["al", "am"]]

a = [:a,:b,:c,:f,:g,:i,:l,:m]
group_consecs(a)
  #=> [[:a, :b, :c], [:f, :g], [:i, :i], [:l, :m]]

Generate an array of seven date objects for an example, then group consecutive dates:

require 'date'
today = Date.today
a = 10.times.map { today = today.succ }.values_at(0,1,2,5,6,8,9)
  #=> [#<Date: 2014-08-07 ((2456877j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-08 ((2456878j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-09 ((2456879j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-12 ((2456882j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-13 ((2456883j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-15 ((2456885j,0s,0n),+0s,2299161j)>,
  #    #<Date: 2014-08-16 ((2456886j,0s,0n),+0s,2299161j)>]
group_consecs(a)
  #=> [[#<Date: 2014-08-07 ((2456877j,0s,0n),+0s,2299161j)>,
  #     #<Date: 2014-08-08 ((2456878j,0s,0n),+0s,2299161j)>,
  #     #<Date: 2014-08-09 ((2456879j,0s,0n),+0s,2299161j)>
  #    ],
  #     [#<Date: 2014-08-12 ((2456882j,0s,0n),+0s,2299161j)>,
  #      #<Date: 2014-08-13 ((2456883j,0s,0n),+0s,2299161j)>
  #    ],
  #    [#<Date: 2014-08-15 ((2456885j,0s,0n),+0s,2299161j)>,
  #     #<Date: 2014-08-16 ((2456886j,0s,0n),+0s,2299161j)>
  #    ]]

5 Comments

For completeness: group_consecs(a).map{|g| g.size==1 ? [g] : [g.first, g.last] }
Although now that I see @BroiSatse's answer I believe group_consecs could be written more concisely using slice_before.
Yes, @Mark, but slice_before was taken, so I decided to show how one could step through with an enumerator, in part because you rarely see that technique being used here. Also, I like the way my answer reads.
@MarkThomas, thanks for the heads-up in your first comment. I fixed the code.
Agreed, it reads nicely and is a good example of an enumerator, which is why I voted up this as well as @BroiSatse's answer. Actually, theTinMan's too. Upvotes all around!!
1

This is some code I wrote for a project a while ago:

class Array
  # [1,2,4,5,6,7,9,13].to_ranges       # => [1..2, 4..7, 9..9, 13..13]
  # [1,2,4,5,6,7,9,13].to_ranges(true) # => [1..2, 4..7, 9, 13]
  def to_ranges(non_ranges_ok=false)
    self.sort.each_with_index.chunk { |x, i| x - i }.map { |diff, pairs|
      if (non_ranges_ok)
        pairs.first[0] == pairs.last[0] ? pairs.first[0] : pairs.first[0] .. pairs.last[0]
      else
        pairs.first[0] .. pairs.last[0]
      end
    }
  end
end

if ($0 == __FILE__)
  require 'awesome_print'

  ary = [1, 2, 4, 5, 6, 7, 9, 13, 12]
  ary.to_ranges(false) # => [1..2, 4..7, 9..9, 12..13]
  ary.to_ranges(true) # => [1..2, 4..7, 9, 12..13]

  ary = [1, 2, 4, 8, 5, 6, 7, 3, 9, 11, 12, 10]
  ary.to_ranges(false) # => [1..12]
  ary.to_ranges(true) # => [1..12]

end

It's easy to change that to only return the start/end pairs:

class Array
  def to_range_pairs(non_ranges_ok=false)
    self.sort.each_with_index.chunk { |x, i| x - i }.map { |diff, pairs|
      if (non_ranges_ok)
        pairs.first[0] == pairs.last[0] ? [pairs.first[0]] : [pairs.first[0], pairs.last[0]]
      else
        [pairs.first[0], pairs.last[0]]
      end
    }
  end
end

if ($0 == __FILE__)
  require 'awesome_print'

  ary = [1, 2, 4, 5, 6, 7, 9, 13, 12]
  ary.to_range_pairs(false) # => [[1, 2], [4, 7], [9, 9], [12, 13]]
  ary.to_range_pairs(true) # => [[1, 2], [4, 7], [9], [12, 13]]

  ary = [1, 2, 4, 8, 5, 6, 7, 3, 9, 11, 12, 10]
  ary.to_range_pairs(false) # => [[1, 12]]
  ary.to_range_pairs(true) # => [[1, 12]]

end

Comments

0

Here's an elegant solution:

arr = [11, 12, 13, 14, 19, 20, 21, 29, 30, 33]
output = []

# Sort array
arr.sort!

# Loop through each element in the list
arr.each do |element|

    # Set defaults - for if there are no consecutive numbers in the list
    start = element
    endd = element

    # Loop through consecutive numbers and check if they are inside the list
    i = 1
    while arr.include?(element+i) do

        # Set element as endd
        endd = element+i

        # Remove element from list
        arr.delete(element+i)

        # Increment i
        i += 1

    end

    # Push [start, endd] pair to output
    output.push([start, endd])

end

4 Comments

Won't work if elements are not sorted, e.g [11,12,13,14,5,6,7,15,16] => [11,5]
Fixed! I added a line to sort the array before the loop
@user2346953 - Sorting array will change the results as well [11,12,13,8,9,10] should result in [[11,13], [8,10]], you'll get [[8,13]]. using sort is out of the table when we consider array's order.
You are right, it is not clearly stated. However look at the order of the resulting array in the question. Obviously if order doesn't matter then your answer is good
0

[Edit: Ha! I misunderstood the question. In your example, for the array

a = [11, 12, 13, 14, 19, 20, 21, 29, 30, 33]

you showed the desired array of pairs to be:

[[11,14], [19,21], [29,30], [33,33]]

which correspond to the following offsets in a:

[[0,3], [4,6], [7,8], [9,9]]

These pairs respective span the first 4 elements, the next 3 elements, then next 2 elements and the next element (by coincidence, evidently). I thought you wanted such pairs, each with a span one less than the previous, and the span of the first being as large as possible. If you have a quick look at my examples below, my assumption may be clearer. Looking back I don't know why I didn't understand the question correctly (I should have looked at the answers), but there you have it.

Despite my mistake, I'll leave this up as I found it an interesting problem, and had the opportunity to use the quadratic formula in the solution.

tidE]

This is how I would do it.

Code

def pull_pairs(a)
  n = ((-1 + Math.sqrt(1.0 + 8*a.size))/2).to_i
  cum = 0
  n.downto(1).map do |i|
    first = cum
    cum += i
    [a[first], a[cum-1]]
  end
end

Examples

a = %w{a b c d e f g h i j k l}
  #=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]
pull_pairs(a)
  #=> [["a", "d"], ["e", "g"], ["h", "i"], ["j", "j"]]

a = [*(1..25)]
  #=> [ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,
  #    14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
pull_pairs(a)
  #=> [[1, 6], [7, 11], [12, 15], [16, 18], [19, 20], [21, 21]]

a = [*(1..990)]
  #=> [1, 2,..., 990]
pull_pairs(a)
  #=> [[1, 44], [45, 87],..., [988, 989], [990, 990]]

Explanation

First, we'll compute the the number of pairs of values in the array we will produce. We are given an array (expressed algebraically):

a = [a0,a1,...a(m-1)]

where m = a.size.

Given n > 0, the array to be produced is:

[[a0,a(n-1)], [a(n),a(2n-2)],...,[a(t),a(t)]]

These elements span the first n+(n-1)+...+1 elements of a. As this is an arithmetic progession, the sum equals n(n+1)/2. Ergo,

t = n(n+1)/2 - 1

Now t <= m-1, so we maximize the number of pairs in the output array by choosing the largest n such that

n(n+1)/2 <= m

which is the float solution for n in the quadratic:

n^2+n-2m = 0

rounded down to an integer, which is

int((-1+sqrt(1^1+4(1)(2m))/2)

or

int((-1+sqrt(1+8m))/2)

Suppose

a = %w{a b c d e f g h i j k l}

Then m (=a.size) = 12, so:

n = int((-1+sqrt(97))/2) = 4

and the desired array would be:

[['a','d'],['e','g'],['h','i'],['j','j']]

Once n has been computed, constructing the array of pairs is straightforward.

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.