How to get even groups in ruby? - ruby-on-rails

Let's say I want to get a certain number of even groups based on a collection of records with varying count. How is this possible?
I'm looking for a method like objects.in_x_even_groups(4)

Group your objects by their index modulo the number of groups.
objects.group_by.with_index { |_, i| i % num_groups }.values
Example:
objects = %w{a b c d e f g h i j k}
objects.group_by.with_index { |_, i| i % 3 }.values
# [["a", "d", "g", "j"], ["b", "e", "h", "k"], ["c", "f", "i"]]
This won't pad undersized groups with nil and it also will interleave your objects. So this won't work if you need consecutive objects to be in the same group.

You are probably looking for the in_groups method. From the docs:
in_groups(number, fill_with = nil)
Splits or iterates over the array in number of groups, padding any remaining slots with fill_with unless it is false.
%w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
["1", "2", "3", "4"]
["5", "6", "7", nil]
["8", "9", "10", nil]

I assume:
the elements are to be kept in order;
l-s is to be minimized, where l is the size of the largest group and s is the size of the smallest group; and
group sizes are non-increasing.
l-s will be at most 1.
def group_em(arr, ngroups)
n_per_group, left_over = arr.size.divmod(ngroups)
cum_off = 0
ngroups.times.map do |i|
n = n_per_group + (i < left_over ? 1 : 0)
a = arr[cum_off, n]
cum_off += n
a
end
end
arr = [1, 2, 3, 4, 5, 6, 7]
(1..7).each { |m| puts "ngroups=#{m}: #{group_em(arr, m)}" }
ngroups=1: [[1, 2, 3, 4, 5, 6, 7]]
ngroups=2: [[1, 2, 3, 4], [5, 6, 7]]
ngroups=3: [[1, 2, 3], [4, 5], [6, 7]]
ngroups=4: [[1, 2], [3, 4], [5, 6], [7]]
ngroups=5: [[1, 2], [3, 4], [5], [6], [7]]
ngroups=6: [[1, 2], [3], [4], [5], [6], [7]]
ngroups=7: [[1], [2], [3], [4], [5], [6], [7]]

You're looking for in_groups_of:
https://apidock.com/rails/Array/in_groups_of
array = %w(1 2 3 4 5 6 7 8 9 10)
array.in_groups_of(3) {|group| p group}
=>
["1", "2", "3"]
["4", "5", "6"]
["7", "8", "9"]
["10", nil, nil]

Related

How to count consecutive numbers in an array?

If I have an array:
array = [1,2,2,2,2,5,5,1,1,1,3,3,3,3,2,2,2,2,2,2,2]
I want to be able to identify consecutive matching numbers that have a length of greater than 3. And map the starting index of the consecutive numbers. An example output for the above array would be:
consecutive_numbers = [
{starting_index: 1, value: 2, length: 4},
{starting_index: 10, value: 3, length: 4},
{starting_index: 14, value: 2, length: 7}
]
The values can be the same, but the consecutive series' must be mutually exclusive. See that there are 2 hashes with a value of 2, but their starting indexes are different.
My attempt so far... looks like this:
array.each_cons(3).with_index.select{|(a,b,c), i|
[a,b,c].uniq.length == 1
}
but that will returns:
[[[2, 2, 2], 1], [[2, 2, 2], 2], [[1, 1, 1], 7], [[3, 3, 3], 10], [[3, 3, 3], 11], [[2, 2, 2], 14], [[2, 2, 2], 15], [[2, 2, 2], 16], [[2, 2, 2], 17], [[2, 2, 2], 18]]
But that returns overlapping results.
array.each_with_index.
chunk(&:first).
select { |_,a| a.size > 3 }.
map { |n,a| { starting_index: a.first.last, value: n, length: a.size } }
#=> [{:starting_index=> 1, :value=>2, :length=>4},
# {:starting_index=>10, :value=>3, :length=>4},
# {:starting_index=>14, :value=>2, :length=>7}]
The steps are as follows.
e = array.each_with_index.chunk(&:first)
#=> #<Enumerator: #<Enumerator::Generator:0x00005b1944253c18>:each>
We can convert this enumerator to an array to view the elements it will generate and pass to its block.
e.to_a
#=> [[1, [[1, 0]]],
# [2, [[2, 1], [2, 2], [2, 3], [2, 4]]],
# [5, [[5, 5], [5, 6]]],
# [1, [[1, 7], [1, 8], [1, 9]]],
# [3, [[3, 10], [3, 11], [3, 12], [3, 13]]],
# [2, [[2, 14], [2, 15], [2, 16], [2, 17], [2, 18], [2, 19], [2, 20]]]]
Continuing,
c = e.select { |_,a| a.size > 3 }
#=> [[2, [[2, 1], [2, 2], [2, 3], [2, 4]]],
# [3, [[3, 10], [3, 11], [3, 12], [3, 13]]],
# [2, [[2, 14], [2, 15], [2, 16], [2, 17], [2, 18], [2, 19], [2, 20]]]]
c.map { |n,a| { starting_index: a.first.last, value: n, length: a.size } }
#=> [{:starting_index=> 1, :value=>2, :length=>4},
# {:starting_index=>10, :value=>3, :length=>4},
# {:starting_index=>14, :value=>2, :length=>7}]
This is another way.
array.each_with_index.with_object([]) do |(n,i),arr|
if arr.any? && arr.last[:value] == n
arr.last[:length] += 1
else
arr << { starting_index: i, value: n, length: 1 }
end
end.select { |h| h[:length] > 3 }
#=> [{:starting_index=> 1, :value=>2, :length=>4},
# {:starting_index=>10, :value=>3, :length=>4},
# {:starting_index=>14, :value=>2, :length=>7}]
You can chunk_while each pair of elements are equal:
p array.chunk_while { |a, b| a == b }.to_a
# [[1], [2, 2, 2, 2], [5, 5], [1, 1, 1], [3, 3, 3, 3], [2, 2, 2, 2, 2, 2, 2]]
You select the arrays with 3 or more elements.
After that, with then, you can yield self, so you have access to the array of arrays, which you can use to get the starting_index:
[1,2,2,2,2,5,5,1,1,1,3,3,3,3,2,2,2,2,2,2,2].chunk_while(&:==).then do |this|
this.each_with_object([]).with_index do |(e, memo), index|
memo << { starting_index: this.to_a[0...index].flatten.size, value: e.first, length: e.size }
end
end.select { |e| e[:length] > 3 }
# [{:starting_index=>1, :value=>2, :length=>4},
# {:starting_index=>10, :value=>3, :length=>4},
# {:starting_index=>14, :value=>2, :length=>7}]
For the starting_index, you get the elements to the current index (non inclusive), flatten them, and get the total of elements.
The value, as each array in the array has the same elements, can be anything, the length, is the length of the current array in the "main" array.
This is another option..
Zipping the array (Enumerable#zip) with its indexes by Endles Ranges
Calling Enumerable#slice_when (another flavour of chunk_while)
Mapping (Enumerable#map) to the required Hash
Finally rejecting (Enumerable#reject) hashes if length is greater than 3
array
.zip(0..)
.slice_when { |a, b| a.first != b.first }
.map { |a| { starting_index: a.first.last, value: a.first.first, length: a.size } }
.reject { |h| h[:length] < 3 }
#=> [{:starting_index=>1, :value=>2, :length=>4}, {:starting_index=>7, :value=>1, :length=>3}, {:starting_index=>10, :value=>3, :length=>4}, {:starting_index=>14, :value=>2, :length=>7}]
Well, the most obvious (and probably the fastest) way is iterate over an array and count everything manually:
array = [1,2,2,2,2,5,5,1,1,1,3,3,3,3,2,2,2,2,2,2,2]
array_length_pred = array.length.pred
consecutive_numbers = []
starting_index = 0
value = array.first
length = 1
array.each_with_index do |v, i|
if v != value || i == array_length_pred
length += 1 if i == array_length_pred && value == v
if length >= 3
consecutive_numbers << {
starting_index: starting_index,
value: value,
length: length
}
end
starting_index = i
value = v
length = 1
next
end
length += 1
end
p consecutive_numbers
# [{:starting_index=>1, :value=>2, :length=>4},
# {:starting_index=>7, :value=>1, :length=>3},
# {:starting_index=>10, :value=>3, :length=>4},
# {:starting_index=>14, :value=>2, :length=>7}]
You could work with strings instead.
Here, I coerce the array into a string:
input_sequence = [1,2,2,2,2,5,5,1,1,1,3,3,3,3,2,2,2,2,2,2,2].join
I use a regex to group consecutive characters:
groups = input_sequence.gsub(/(.)\1*/).to_a
#=> ["1", "2222", "55", "111", "3333", "2222222"]
Now I can search for the groups as substrings within the input string:
groups.map do |group|
{
starting_index: input_sequence.index(group),
value: group[0].to_i,
length: group.length
}
end.reject { |group| group[:length] <= 3 }
#=> [{:starting_index=>1, :value=>2, :length=>4},
{:starting_index=>7, :value=>1, :length=>3},
{:starting_index=>10, :value=>3, :length=>4},
{:starting_index=>14, :value=>2, :length=>7}]
There's room for improvement here -- I'm creating lots of intermediate objects for one -- but I thought I would offer a different approach.

Simplest way to sum 2D array elements based on duplicates?

So say I have a 2D array. Maybe something like:
data = [['oct 1', 4], ['oct 2', 5], ['oct 3', 9], ['oct 1', 2]]
And I want to get a new array out of this, that removes the duplicate values (eg. oct 1), but sums the corresponding values.
So I would end up with:
data = (['oct 1', 6], ['oct 2', 5], ['oct 3', 9])
I can think of a few ways to do this but they seem pretty inefficent and usually there's always some crazy ruby code that can do anything in a few lines, any suggestions?
Here are a couple of ways of doing that, my preference being the first.
Use a counting hash
Code
def combine(data)
data.each_with_object(Hash.new(0)) { |(date, val), h| h[date] += val }.to_a
end
See Hash::new, the discussion of the default value in particular.
Example
data = [['oct 1', 4], ['oct 2', 5], ['oct 3', 9], ['oct 1', 2]]
combine data
#=> [["oct 1", 6], ["oct 2", 5], ["oct 3", 9]]
Explanation
enum = data.each_with_object(Hash.new(0))
#=> #<Enumerator: [["oct 1", 4], ["oct 2", 5], ["oct 3", 9],
# ["oct 1", 2]]:each_with_object({})>
We can see the values generated by this enumerator by converting it to an array.
enum.to_a
#=> [[["oct 1", 4], {}], [["oct 2", 5], {}], [["oct 3", 9], {}],
# [["oct 1", 2], {}]]
The first value of enum is passed to the block and the values of the block variables are computed, using disambiguation and parallel assignment.
(date, val), h = enum.next
#=> [["oct 1", 4], {}]
date
#=> "oct 1"
val
#=> 4
h[date] += val
#=> h[date] = h[date] + val
#=> h["oct 1"] = h["oct 1"] + 4
#=> h["oct 1"] = 0 + 4 (no key "oct 1" so default value of `0` used)
#=> h["oct 1"] = 4
Now,
h #=> {"oct 1"=>4}
The remaining three values of enum are passed to the block and the block calculations are performed.
(date, val), h = enum.next
#=> [["oct 2", 5], {"oct 1"=>4}]
h[date] += val
#=> 5 (the default value of `0` is again used)
h #=> {"oct 1"=>4, "oct 2"=>5}
(date, val), h = enum.next
#=> [["oct 3", 9], {"oct 1"=>4, "oct 2"=>5}]
h[date] += val
#=> 9 (the default value of `0` is again used)
h #=> {"oct 1"=>4, "oct 2"=>5, "oct 3"=>9}
(date, val), h = enum.next
#=> [["oct 1", 2], {"oct 1"=>4, "oct 2"=>5, "oct 3"=>9}]
h[date] += val
#=> 6
h #=> {"oct 1"=>6, "oct 2"=>5, "oct 3"=>9}
In the last calculation, the default value was not used because the hash h already had a key "oct 1":
h[date] += val
#=> h[date] = h[date] + val
#=> h["oct 1"] = h["oct 1"] + 2
#=> h["oct 1"] = 4 + 2
Lastly,
h.to_a
#=> [["oct 1", 6], ["oct 2", 5], ["oct 3", 9]]
Use Enumerable#group_by
Code
def combine(data)
data.group_by(&:first).map { |date, vals| [date, vals.map(&:last).reduce(:+)] }
end
Example
combine data
#=> [["oct 1", 6], ["oct 2", 5], ["oct 3", 9]]
Explanation
The steps:
h = data.group_by(&:first)
#=> {"oct 1"=>[["oct 1", 4], ["oct 1", 2]],
# "oct 2"=>[["oct 2", 5]], "oct 3"=>[["oct 3", 9]]}
The first key-value pair of h is passed to the block:
date, vals = h.first
#=> ["oct 1", [["oct 1", 4], ["oct 1", 2]]]
date
#=> "oct 1"
vals
#=> [["oct 1", 4], ["oct 1", 2]]
and the block calculation is performed.
a = vals.map(&:last)
#=> [4, 2]
t = a.reduce(:+)
#=> 6
So the first key-value pair of h is mapped to
[date, t]
#=> ["oct 1", 6]
The remaining calculations are similar.
Try following with inject method:
data = [["oct 1", 4], ["oct 2", 5], ["oct 3", 9], ["oct 1", 2]]
data.inject({}) { |sum, (n, t)| sum[n] ||= 0; sum[n] += t; sum }.to_a
=> [["oct 1", 6], ["oct 2", 5], ["oct 3", 9]]

How to compare arrays inside an array with each other in ruby?

[ 1, 1, 3, 5 ] & [ 1, 2, 3 ] #=> [ 1, 3 ]
[ 'a', 'b', 'b', 'z' ] & [ 'a', 'b', 'c' ] #=> [ 'a', 'b' ]
I need the intersection of each array with all other arrays within an array.
So the array could look like ->
a = [[1, 2, 3], [3, 4, 5], [4, 5, 6]]
The result should look like ->
a = [[3],[3,4,5][4,5]]
Any suggestions?
Look into the combination method.
a = [[1, 2, 3], [3, 4, 5], [4, 5, 6],[1,"a","b"]]
p a.combination(2).map{|x,y| x & y } #=> [[3], [], [1], [4, 5], [], []]
And if you do not want the empty arrays in there:
p a.combination(2).map{|x,y| x & y }.reject(&:empty?) #=> [[3], [1], [4, 5]]
Edit: After seeing some examples what OP actually want here is how I would achieve the desired result:
original = [[1, 2, 3], [3, 4, 5], [4, 5, 6]]
def intersect_with_rest(array)
array.size.times.map do
first, *rest = array
array.rotate!
first & rest.flatten
end
end
p intersect_with_rest(original) #=> [[3], [3, 4, 5], [4, 5]]
p original #=> [[1, 2, 3], [3, 4, 5], [4, 5, 6]]
Or:
original = [[1, 2, 3], [3, 4, 5], [4, 5, 6]]
result = original.map.with_index do |x,i|
x & (original[0...i]+original[1+i..-1]).flatten
end
p result #=> [[3], [3, 4, 5], [4, 5]]
Yeah, finally I found a solution. Maybe there is a simpler way, but that works for me now..
c = [[1,2,3],[3,4,5],[4,5,6]]
results = [];c.length.times.each {|e| results.push c.rotate(e).combination(2).map {|x, y| x & y}}
results.map{|x, y| y + x}
=> [[3], [3, 4, 5], [4, 5]]
Thanks to #hirolau for the hint. Best regards

Cycle through an array in Ruby

I have an array of the form [1,2,3,4,5].
Is it possible to loop through this array and during each iteration get an array where the starting point is the current element and the end point is the element before that?
Like
[1,2,3,4,5]
[2,3,4,5,1]
[3,4,5,1,2]
[4,5,1,2,3]
[5,1,2,3,4]
I am trying with .cycle method of array but it is not giving the expected result.
Check out rotate.
a = [ "a", "b", "c", "d" ]
a.rotate #=> ["b", "c", "d", "a"]
irb(main):005:0> array = [1,2,3,4,5]
=> [1, 2, 3, 4, 5]
irb(main):006:0> array.size.times.map{|i| array.rotate(i)}
=> [[1, 2, 3, 4, 5], [2, 3, 4, 5, 1], [3, 4, 5, 1, 2], [4, 5, 1, 2, 3], [5, 1, 2 , 3, 4]]

How can I create an array of duplicate case-insensitive strings from another array in Ruby?

I have an array of strings. I'm wanting to change the name of these duplicate strings to append a numerical value to make them unique like so...
Original Array
a, a, A, b, c, D, d
Corrected Array
a, a1, A2, b, c, D, d1
I've gotten close to this with the following code; however, if the strings are a different case structure then they aren't currently considered duplicates with this code snippet. I would like them to be considered duplicates, but yet not change their case in the results array.
duplicate_counter = 1
duplicates = Array.new
duplicates = file_columns.select{ |e| file_columns.count(e) > 1 } # get duplicate column names
duplicates.each{ |x| file_columns.delete(x) }
duplicates.sort!
duplicates.each_with_index do |d, i|
if i > 0
if d == duplicates[i-1]
d = d.strip + duplicate_count.to_s
duplicate_count += 1
else
duplicate_count = 1
end
end
# Add back the column names, but with the appended numerical counts to make them unique
file_columns.push(d)
end
You are over thinking it considerably. I'm sure there are better ways to do this as well, but it gets the job done.
a = ['a', 'a', 'A', 'b', 'c', 'D', 'd']
letters = Hash.new(-1)
a.map do |letter|
l = letter.downcase
letters[l] += 1
if (letters[l] > 0)
"#{letter}#{letters[l]}"
else
"#{letter}"
end
end
Here's a way to do it if letters independent of case are not necessarily grouped. For example, it will convert this array:
arr = %w{ a D a A b c D a d }
#=> ["a", "D", "a", "A", "b", "c", "D", "a", "d"]
to:
["a", "D", "a1", "A2", "b", "c", "D1", "a3", "d2"]
Code
def convert(arr)
arr.each_with_index
.group_by { |c,_| c.downcase }
.values
.flat_map { |c|
c.map
.with_index { |(f,l),i| [i > 0 ? f<<i.to_s : f, l] } }
.sort_by(&:last)
.map(&:first)
end
Example
For arr above:
convert(arr)
#=> ["a", "D", "a1", "A2", "b", "c", "D1", "a3", "d2"]
Explanation
Dear reader, if you are new to Ruby, this may look impossibly complex. If you break it down into steps, however, it's not that bad. After you gain experience and become familiar with commonly-used methods, it will come quite naturally. Here I've used the following methods, chained together so that the return value of each becomes the receiver of the next:
Enumerable#each_with_index
Enumerable#group_by
Hash#values
Enumerable#flat_map
Enumerable#sort_by
Enumerable#first
Here's what's happening.
enum = arr.each_with_index
#=> #<Enumerator: ["a", "D", "a", "A", "b", "c",
# "D", "a", "d"]:each_with_index>
h = enum.group_by { |c,_| c.downcase }
#=> {"a"=>[["a", 0], ["a", 2], ["A", 3], ["a", 7]],
# "d"=>[["D", 1], ["D", 6], ["d", 8]],
# "b"=>[["b", 4]],
# "c"=>[["c", 5]]}
a = h.values
#=> [[["a", 0], ["a", 2], ["A", 3], ["a", 7]],
# [["D", 1], ["D", 6], ["d", 8]],
# [["b", 4]],
# [["c", 5]]]
b = a.flat_map { |c| c.map.with_index { |(f,l),i| [i > 0 ? f<<i.to_s : f, l] } }
#=> [["a", 0], ["a1", 2], ["A2", 3], ["a3", 7], ["D", 1],
# ["D1", 6], ["d2", 8], ["b", 4], ["c", 5]]
c = b.sort_by(&:last)
#=> [["a", 0], ["D", 1], ["a1", 2], ["A2", 3], ["b", 4],
# ["c", 5], ["D1", 6], ["a3", 7], ["d2", 8]]
c.map(&:first)
#=> ["a", "D", "a1", "A2", "b", "c", "D1", "a3", "d2"]

Resources