Ruby reducing a number array into start end range array - ruby

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.

Related

How to use collect and include for multidimensional array

I have:
array1 = [[1,2,3,4,5],[7,8,9,10],[11,12,13,14]]
#student_ids = [1,2,3]
I want to replace elements in array1 that are included in #student_ids with 'X'. I want to see:
[['X','X','X',4,5],[7,8,9,10],[11,12,13,14]]
I have code that is intended to do this:
array1.collect! do |i|
if i.include?(#student_ids) #
i[i.index(#student_ids)] = 'X'; i # I want to replace all with X
else
i
end
end
If #student_ids is 1, then it works, but if #student_ids has more than one element such as 1,2,3, it raises errors. Any help?
It's faster to use a hash or a set than to repeatedly test [1,2,3].include?(n).
arr = [[1,2,3,4,5],[7,8,9,10],[11,12,13,14]]
ids = [1,2,3]
Use a hash
h = ids.product(["X"]).to_h
#=> {1=>"X", 2=>"X", 3=>"X"}
arr.map { |a| a.map { |n| h.fetch(n, n) } }
#=> [["X", "X", "X", 4, 5], [7, 8, 9, 10], [11, 12, 13, 14]]
See Hash#fetch.
Use a set
require 'set'
ids = ids.to_set
#=> #<Set: {1, 2, 3}>
arr.map { |a| a.map { |n| ids.include?(n) ? "X" : n } }
#=> [["X", "X", "X", 4, 5], [7, 8, 9, 10], [11, 12, 13, 14]]
Replace both maps with map! if the array is to be modified in place (mutated).
Try following, (taking #student_ids = [1, 2, 3])
array1.inject([]) { |m,a| m << a.map { |x| #student_ids.include?(x) ? 'X' : x } }
# => [["X", "X", "X", 4, 5], [7, 8, 9, 10], [11, 12, 13, 14]]
You can use each_with_index and replace the item you want:
array1 = [[1,2,3,4,5],[7,8,9,10],[11,12,13,14]]
#student_ids = [1,2,3]
array1.each_with_index do |sub_array, index|
sub_array.each_with_index do |item, index2|
array1[index][index2] = 'X' if #student_ids.include?(item)
end
end
You can do the following:
def remove_student_ids(arr)
arr.each_with_index do |value, index|
arr[index] = 'X' if #student_ids.include?(value) }
end
end
array1.map{ |sub_arr| remove_student_ids(sub_arr)}

Grouping consecutive numbers in an array

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] }

Splitting an array by performing an arithmetic function in ruby

I have an array in Ruby like [3,4,5] and I want to create sub-arrays by diving or multiplying. For example, I want to multiply each number in the array by 2, 3, and 4, returning [[6,9,12],[8,12,16],[10,15,20]]
After that, what's the best way to count the total number of units? In this example, it would be 9, while array.count would return 3.
Thanks
The simplest way I could think of was:
[3,4,5].map { |v|
[3,4,5].map { |w|
w * v
}
}
I'm sure there is a more elegant way.
As for the count you can use flatten to turn it into a single array containing all the elements.
[[9, 12, 15], [12, 16, 20], [15, 20, 25]].flatten
=> [9, 12, 15, 12, 16, 20, 15, 20, 25]
You might find it convenient to use matrix operations for this, particularly if it is one step among several involving matrices, vectors, and/or scalars.
Code
require 'matrix'
def doit(arr1, arr2)
(Matrix.column_vector(arr2) * Matrix.row_vector(arr1)).to_a
end
def nbr_elements(arr1, arr2) arr1.size * arr2.size end
Examples
arr1 = [3,4,5]
arr2 = [3,4,5]
doit(arr1, arr2)
#=> [[ 9, 12, 15],
# [12, 16, 20],
# [15, 20, 25]]
nbr_elements(arr1, arr2)
#=> 9
doit([1,2,3], [4,5,6,7])
#=> [[4, 8, 12],
# [5, 10, 15],
# [6, 12, 18],
# [7, 14, 21]]
nbr_elements([1,2,3], [4,5,6,7])
#=> 12
Alternative
If you don't want to use matrix operations, you could do it like this:
arr2.map { |e| [e].product(arr1).map { |e,f| e*f } }
Here's an example:
arr1 = [1,2,3]
arr2 = [4,5,6,7]
arr2.map { |e| [e].product(arr1).map { |e,f| e*f } }
#=> [[4, 8, 12],
# [5, 10, 15],
# [6, 12, 18],
# [7, 14, 21]]

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]

Every Other 2 Items in Array

I need a ruby formula to create an array of integers. The array must be every other 2 numbers as follows.
[2, 3, 6, 7, 10, 11, 14, 15, 18, 19...]
I have read a lot about how I can do every other number or multiples, but I am not sure of the best way to achieve what I need.
Here's an approach that works on any array.
def every_other_two arr
arr.select.with_index do |_, idx|
idx % 4 > 1
end
end
every_other_two((0...20).to_a) # => [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
# it works on any array
every_other_two %w{one two three four five six} # => ["three", "four"]
array = []
#Change 100000 to whatever is your upper limit
100000.times do |i|
array << i if i%4 > 1
end
This code works for any start number to any end limit
i = 3
j = 19
x =[]
(i...j).each do |y|
x << y if (y-i)%4<2
end
puts x
this should work
For fun, using lazy enumerables (requires Ruby 2.0 or gem enumerable-lazy):
(2..Float::INFINITY).step(4).lazy.map(&:to_i).flat_map { |x| [x, x+1] }.first(8)
#=> => [2, 3, 6, 7, 10, 11, 14, 15]
here's a solution that works with infinite streams:
enum = Enumerator.new do |y|
(2...1/0.0).each_slice(4) do |slice|
slice[0 .. 1].each { |n| y.yield(n) }
end
end
enum.first(10) #=> [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
enum.each do |n|
puts n
end
Single Liner:
(0..20).to_a.reduce([0,[]]){|(count,arr),ele| arr << ele if count%4 > 1;
[count+1,arr] }.last
Explanation:
Starts the reduce look with 0,[] in count,arr vars
Add current element to array if condition satisfied. Block returns increment and arr for the next iteration.
I agree though that it is not so much of a single liner though and a bit complex looking.
Here's a slightly more general version of Sergio's fine answer
module Enumerable
def every_other(slice=1)
mod = slice*2
res = select.with_index { |_, i| i % mod >= slice }
block_given? ? res.map{|x| yield(x)} : res
end
end
irb> (0...20).every_other
=> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
irb> (0...20).every_other(2)
=> [2, 3, 6, 7, 10, 11, 14, 15, 18, 19]
irb> (0...20).every_other(3)
=> [3, 4, 5, 9, 10, 11, 15, 16, 17]
irb> (0...20).every_other(5) {|v| v*10 }
=> [50, 60, 70, 80, 90, 150, 160, 170, 180, 190]

Resources