Grouping consecutive numbers in an array - ruby

I need to add consecutive numbers to a new array and, if it is not a consecutive number, add only that value to a new array:
old_array = [1, 2, 3, 5, 7, 8, 9, 20, 21, 23, 29]
I want to get this result:
new_array = [
[1,2,3],
[5],
[7,8,9]
[20,21]
[23],
[29]
]
Is there an easier way to do this?

A little late to this party but:
old_array.slice_when { |prev, curr| curr != prev.next }.to_a
# => [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]

This is the official answer given in RDoc (slightly modified):
actual = old_array.first
old_array.slice_before do
|e|
expected, actual = actual.next, e
expected != actual
end.to_a

A couple other ways:
old_array = [1, 2, 3, 5, 7, 8, 9, 20, 21, 23, 29]
#1
a, b = [], []
enum = old_array.each
loop do
b << enum.next
unless enum.peek.eql?(b.last.succ)
a << b
b = []
end
end
a << b if b.any?
a #=> [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]
#2
def pull_range(arr)
b = arr.take_while.with_index { |e,i| e-i == arr.first }
[b, arr[b.size..-1]]
end
b, l = [], a
while l.any?
f, l = pull_range(l)
b << f
end
b #=> [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]

Using chunk you could do:
old_array.chunk([old_array[0],old_array[0]]) do |item, block_data|
if item > block_data[1]+1
block_data[0] = item
end
block_data[1] = item
block_data[0]
end.map { |_, i| i }
# => [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]

Some answers seem unnecessarily long, it is possible to do this in a very compact way:
arr = [1, 2, 3, 5, 7, 8, 9, 20, 21, 23, 29]
arr.inject([]) { |a,e| (a[-1] && e == a[-1][-1] + 1) ? a[-1] << e : a << [e]; a }
# [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]
Alternatively, starting with the first element to get rid of the a[-1] condition (needed for the case when a[-1] would be nil because a is empty):
arr[1..-1].inject([[arr[0]]]) { |a,e| e == a[-1][-1] + 1 ? a[-1] << e : a << [e]; a }
# [[1, 2, 3], [5], [7, 8, 9], [20, 21], [23], [29]]
Enumerable#inject iterates all elements of the enumerable, building up a result value which starts with the given object. I give it an empty Array or an Array with the first value wrapped in an Array respectively in my solutions. Then I simply check if the next element of the input Array we are iterating is equal to the last value of the last Array in the resulting Array plus 1 (i.e, if it is the next consecutive element). If it is, I append it to the last list. Otherwise, I start a new list with that element in it and append it to the resulting Array.

You could also do it like this:
old_array=[1, 2, 3, 5, 7, 8, 9, 20, 21, 23, 29]
new_array=[]
tmp=[]
prev=nil
for i in old_array.each
if i != old_array[0]
if i - prev == 1
tmp << i
else
new_array << tmp
tmp=[i]
end
if i == old_array[-1]
new_array << tmp
break
end
prev=i
else
prev=i
tmp << i
end
end

Using a Hash you can do:
counter = 0
groups = {}
old_array.each_with_index do |e, i|
groups[counter] ||= []
groups[counter].push old_array[i]
counter += 1 unless old_array.include? e.next
end
new_array = groups.keys.map { |i| groups[i] }

Related

Repeating a loop when it reaches the end

I am trying to conceptualize the iteration of two loops
numbers_array = [1,2,3,4,5,6,7,8,9,10]
add_to_array = [1,2,3,4]
While the numbers_array iterates, add_to_array iterates simultaneously adding both elements together at the same time. The caveat is once add_to_array reaches the end, it starts over adding its element to the next index in numbers_array. So at numbers_array[4] we would be adding add_to_array[0] then adding numbers_array[5] to add_to_array[1] and so on. This process would repeat until we reach the end of the numbers_array.
Any input would be greatly appreciated!
You are looking for Enumerable#zip and Enumerable#cycle:
numbers_array = [1,2,3,4,5,6,7,8,9,10]
#⇒ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
add_to_array = [1,2,3,4]
#⇒ [1, 2, 3, 4]
numbers_array.zip(add_to_array.cycle)
#⇒ [[1, 1], [2, 2], [3, 3], [4, 4], [5, 1],
# [6, 2], [7, 3], [8, 4], [9, 1], [10, 2]]
Now do whatever you want with the array returned. E.g. to reduce the zipped result summing elements, map ro Enumerable#sum:
numbers_array.zip(add_to_array.cycle).map(&:sum)
#⇒ [2, 4, 6, 8, 6, 8, 10, 12, 10, 12]
It works by using the % operator to cycle through the indexes.
numbers_array = [1,2,3,4,5,6,7,8,9,10]
add_to_array = [1,2,3,4]
numbers_array.map.with_index do |n, i|
n + add_to_array[i % add_to_array.length]
end
A cool method that's similar, if you didn't want to start over at the next array, would be .zip
https://apidock.com/ruby/Array/zip
add_to_array.zip(*numbers_array.each_slice(add_to_array.size)).
map { |a| a.sum { |e| e.to_i } }
#=> [16, 20, 13, 16]
e.to_i is needed to convert nil values to zeros. See NilClass#to_i.
Another option:
numbers_array.map { |e| e + add_to_array.rotate!.last }
# => [2, 4, 6, 8, 6, 8, 10, 12, 10, 12]
Drawback: add_to_array is mutated by rotate!

How can i avoid using the same element in my two sum solution

So I am trying to get a solution to my two sum problem and I am stuck, I need to print the indices for the elements which add up to the target and my solution will return an element twice if it is one half of the target
def two_sum(nums, target)
num_hash = Hash.new(0)
nums.each_with_index do |num,idx|
num_hash[num] = idx
if num_hash.key?(target - num) && target % num != 0
return [num_hash[num], idx]
end
end
end
So I don't think the problem is related to the number being 1/2 of the target, it just seems to be "if a solution is found, it returns the same index twice". For instance, using the sample set [2, 7, 11, 15]
two_sum([2, 7, 11, 15], 14) # => [2, 7, 11, 15]
So, 7 is half of 14, which is the target, and instead of returning the index 1 twice, as you suggest it would, it returns the original input array (the result of nums.each_with_index. However, if we try passing a target of 9, it behaves as you describe:
two_sum([2, 7, 11, 15], 9) # => [1, 1]
The reason for this, is because of the line:
return [num_hash[num], idx]
you have already set num into the num_hash (num_hash[num] = idx) and then you are returning both the idx and num_hash[num], which is also idx. So what you want to do is:
return [num_hash[target - num], idx]
and then to 'fix' all the elements being returned when no result is found, just return [] at the end of the method:
def two_sum(nums, target)
num_hash = Hash.new(0)
nums.each_with_index do |num,idx|
num_hash[num] = idx
if num_hash.key?(target - num) && target % num != 0
return [num_hash[target - num], idx]
end
end
[]
end
and now:
two_sum([2, 7, 11, 15], 14) # => []
two_sum([2, 7, 11, 15], 9) # => [0, 1]
Note: you also have a problem with the code where, if you have the same number twice, it doesn't find the answer:
two_sum([2, 7, 11, 7, 15], 14) # => []
left for you to figure out, just wanted to point this out to you.
You can use the method Array#combination to advantage here.
def two_sum(nums, target)
nums.each_index.to_a.combination(2).select { |i,j| nums[i] + nums[j] == target }
end
two_sum([2, 7, 11, 15], 14)
#=> []
two_sum([2, 7, 11, 15], 9)
#=> [[0, 1]]
two_sum([2, 4, 7, 5], 9)
#=> [[0, 2], [1, 3]]
two_sum([2, 2, 2, 2], 4)
#=> [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
two_sum([2, 4, 7, 5], 8)
#=> []
For
nums = [2, 4, 7, 5]
target = 9
the steps are as follows.
a = nums.each_index
#=> #<Enumerator: [2, 4, 7, 5]:each_index>
We can see the elements that will be generated by this enumerator by converting it to an array.
b = a.to_a
#=> [0, 1, 2, 3]
Next,
c = b.combination(2)
#=> #<Enumerator: [0, 1, 2, 3]:combination(2)>
c.to_a
#=> [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
The rest is straightforward as select merely selects those pairs of indices passed to it (i,j) whose corresponding values, num[i] and num[j], sum to target.
I think what you want is ...
return [num_hash[target-num], idx]

Ruby reducing a number array into start end range array

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?
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]
}
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)>
# ]]
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
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
[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.

Merging inner arrays of indices if they contain the same content

I'm generating an array of grouped indexes. The indexes are points within an array that meet my grouping requirements. For example I'm grouping indexes from a grid where things are horizontally "close" to each other. This is kind of what I'll be working with.
[[0,1,2],[3],[4,5],[5,6],[7,8],[8,9]]
I would like to merge by common indexes. So the result should look like.
[[0,1,2],[3],[4,5,6],[7,8,9]]
It feels like it should be an inject :+ on pairs if any inner items match. But I don't see the Ruby way to do this.
x.sort.inject([]) do |y, new|
(((y.last || []) & new).length > 0) ? y[0..-2].push(y.last | new) : y.push(new)
end.map(&:sort)
Knowing Ruby, there's probably a more concise way to do this, but this should give you what you want:
foo = [[0,1,2],[3],[4,5],[5,6],[7,8],[8,9]]
foo.inject([]) {|result,element|
if (result and existing = result.find_index{|a| !(element & [*a]).empty?})
tmp = result[existing]
result.delete_at(existing)
result << (tmp | element).sort
else
result << element
end
}.sort
Output:
=> [[0, 1, 2], [3], [4, 5, 6], [7, 8, 9]]
Logic:
For each element in the original array, check the newly-built array-so-far (result) for any entry which contains any of the same numbers as the next element using array intersection -- !(element & [*a]).empty? ...
if found, remove said entry from the result, union it with the new element from the original array -- (tmp | element) -- then add it back to the result
if not found, simply concatenate the element from the original array to the result
Someone might find a more compact method, but this works...
array = [[0,1,2],[3],[4,5],[5,6],[7,8],[8,9]]
(0...array.length).each do |a|
(a+1...array.length).each do |b|
unless array[a].to_a & array[b].to_a == []
array[a].push(array[b]).flatten!.uniq!.sort!
array.delete_at(b)
b -= 1
end
end
end
p array
=> [[0, 1, 2], [3], [4, 5, 6], [7, 8, 9]]
The solutions so far seem overly-complicated to me. I suggest this (assuming each element of arr is non-empty and contains only integers):
arr = [[0, 1, 2], [3],
[4, 5], [5, 6],
[7, 8, 9], [9, 10], [10, 11],
[12, 13], [13], [13, 14]]
arr.each_with_object([]) do |a,b|
if b.any? && b.last.last == a.first
b[-1] += a[1..-1]
else
b << a
end
end
#=> [[0, 1, 2], [3], [4, 5, 6], [7, 8, 9, 10, 11], [12, 13, 14]]
You could alternatively do it by stepping through arr with an enumerator:
enum = arr.each
b = [enum.next]
loop do
a = enum.next
if b.last.last == a.first
b[-1] += a[1..-1]
else
b << a
end
end
b

How do I filter and defilter an array?

I have an array:
arr = [1,1,2,3,5,8,13,21,34]
I'd like to filter the array in the same way as select but also separately gather all the elements that fail the condition:
[evens, odds] = arr.split_filter {|p| p % 2 == 0}
# evens = [2, 8, 34]
# odds = [1, 1, 3, 5, 13, 21]
I could do
evens = arr.select {|p| p % 2 == 0}
odds = arr.select {|p| p % 2 != 0}
But that seems inefficient. Does anyone know of a function that works like split_filter?
You're looking for Enumerable#partition:
arr = [1,1,2,3,5,8,13,21,34]
evens, odds = arr.partition{|a| a % 2 == 0}
evens # => [2, 8, 34]
odds # => [1, 1, 3, 5, 13, 21]
Or, shorter version:
evens, odds = arr.partition(&:even?)
We could always use Enum#group_by for the same.
arr = [20,1,1,2,3,5,8,13,21,34]
even,odd = arr.group_by(&:even?).values_at(true,false)
even #=> [20, 2, 8, 34]
odd #=> [1, 1, 3, 5, 13, 21]

Resources