Ruby grouping elements - arrays

I have array:
a = [1, 3, 1, 3, 2, 1, 2]
And I want to group by values, but save it indexes, so result must be looks like this:
[[0, 2, 5], [1, 3], [4, 6]]
or hash
{1=>[0, 2, 5], 3=>[1, 3], 2=>[4, 6]}
Now I'm using pretty ugly and big code:
struc = Struct.new(:index, :value)
array = array.map.with_index{ |v, i| struc.new(i, v) }.group_by {|s| s[1]}.map { |h| h[1].map { |e| e[0]}}
`

If you use a hash with a default value to avoid iterating twice over the elements:
a = [1, 3, 1, 3, 2, 1, 2]
Hash.new { |h, k| h[k] = [] }.tap do |result|
a.each_with_index { |i, n| result[i] << n }
end
#=> { 1 => [0, 2, 5], 3 => [1, 3], 2 => [4, 6] }

a = [1, 3, 1, 3, 2, 1, 2]
a.each_with_index.group_by(&:first).values.map { |h| h.map &:last }
First we get an Enumerator in the form [val, idx], ... (each_with_index), then group_by the value (first value in pair), then take the index (last element) of each pair.

You can use:
Enumerable#each_with_index
Enumerable#group_by and
Array#transpose:
a = [1, 3, 1, 3, 2, 1, 2]
a.each_with_index.group_by(&:first).values.map { |b| b.transpose.last }
#=> [[0, 2, 5], [1, 3], [4, 6]]

Related

How can I arrange k elements by frequency in Decreasing Order in Ruby?

Given the array (array) [1, 1, 2, 2, 2, 3] this method should return (new_array)
[2, 2, 2, 1, 1, 3]
Heres what I have tried so far
Converted array into hash
Key being the element and value being the count
How do I recreate the array again to match new_array?
Here's one way:
array = [1,1,2,2,2,3]
array.tally # This is the bit you did already. Note that this uses the new ruby 2.7 method. You get: {1=>2, 2=>3, 3=>1}
.sort_by {|k, v| -v} # Now we have: [[2, 3], [1, 2], [3, 1]]
.flat_map { |element, count| Array.new(count, element) }
# And that gives the final desired result of:
[2, 2, 2, 1, 1, 3]
Or another variant, along the same lines:
array.tally # {1=>2, 2=>3, 3=>1}
.invert # {2=>1, 3=>2, 1=>3}
.sort # [[1, 3], [2, 1], [3, 2]]
.reverse # [[3, 2], [2, 1], [1, 3]]
.flat_map { |element, count| [element] * count }
Or, here's something completely different:
array.sort_by { |x| -array.count(x) }
Here is another one:
array = [1,1,2,2,2,3]
p array.group_by(&:itself).values.sort_by(&:size).flatten
def sort_chunks_by_length(arr)
arr.slice_when(&:!=).sort_by { |a| -a.size }.flatten
end
sort_chunks_by_length [1,1,2,2,2,3]
#=> [2, 2, 2, 1, 1, 3]
sort_chunks_by_length [1,1,2,2,2,1,3,3]
#=> [2, 2, 2, 1, 1, 3, 3, 1]
I have assumed that for the second example the desired return value is as shown, as opposed to:
#=> [2, 2, 2, 1, 1, 1, 3, 3]
The steps for that example are as follows.
arr = [1,1,2,2,2,1,3,3]
enum = arr.slice_when(&:!=)
#=> #<Enumerator: #<Enumerator::Generator:0x00007ffd1a9740b8>:each>
This is shorthand for:
enum = arr.slice_when { |x,y| x!=y }
We can see the elements that will be generated by this enumerator by converting it to an array:
enum.to_a
#=> [[1, 1], [2, 2, 2], [1], [3, 3]]
Continuing,
a = enum.sort_by { |a| -a.size }
#=> [[2, 2, 2], [1, 1], [3, 3], [1]]
a.flatten
#=> [2, 2, 2, 1, 1, 3, 3, 1]
The operative line could be replaced by either of the following.
arr.chunk(&:itself).map(&:last).sort_by { |a| -a.size }.flatten
arr.chunk_while(&:==).sort_by { |a| -a.size }.flatten
See Enumerable#slice_when, Enumerable#sort_by, Enumerable#chunk and Enumerable#chunk_while.

Summing elements from arrays with matching elements

I have an array that looks like this:
original = [[1, 2, 3], [2, 2, 2], [1, 2, 3], [2, 2, 2], [2, 2, 3], [1, 2, 2], [5, 4, 2]]
I'd like to get a new array whose elements that match the second and third position would sum up its first position to get this:
expected_output = [[4, 2, 3], [5, 2, 2], [5, 4, 2]]
I got to grouping the elements from the array as follows:
new_array = original.group_by {|n| n[1] && n[2] }
# => {3=>[[1, 2, 3], [1, 2, 3], [2, 2, 3]], 2=>[[2, 2, 2], [2, 2, 2], [1, 2, 2], [5, 4, 2]]}
It is still far from my desired output.
Here's one way to return a new array of arrays where the first element of each array is the sum of the original array's first element where its second and third elements match:
arr = [[1, 2, 3], [2, 2, 2], [1, 2, 3], [2, 2, 2], [2, 2, 3], [1, 2, 2], [5, 4, 2]]
array_groups = arr.group_by { |sub_arr| sub_arr[1, 2] }
result = array_groups.map do |k, v|
k.unshift(v.map(&:first).inject(:+))
end
result
# => [[4, 2, 3], [5, 2, 2], [5, 4, 2]]
Hope this helps!
This will produce a similar result using an array grouping rather than combining the two latter numbers.
original = [[1, 2, 3], [2, 2, 2], [1, 2, 3], [2, 2, 2], [2, 2, 3], [1, 2, 2], [5, 4, 2]]
new = original.group_by {|n| [n[1], n[2]] }
added = new.map{|x| [new[x.first].map(&:first).inject(0, :+),x.first].flatten}
puts added.to_s
original.each_with_object(Hash.new(0)) { |(f,*rest),h| h[rest] += f }.
map { |(s,t),f| [f,s,t] }
# => [[4, 2, 3], [5, 2, 2], [5, 4, 2]]
Note that
original.each_with_object(Hash.new(0)) { |(f,*rest),h| h[rest] += f }
#=> {[2, 3]=>4, [2, 2]=>5, [4, 2]=>5}
Hash.new(0) is sometimes called a counting hash. To understand how that works, see Hash::new, especially the explanation of the effect of providing a default value as an argument of new. In brief, if a hash is defined h = Hash.new(0), then if h does not have a key k, h[k] returns the default value, here 0 (and the hash is not changed).

Ruby - collect same numbers from array into array of arrays

I have created a very ugly script to collect same numbers from an array. I don't think this is a very Ruby way :) Anyone could provide a more clean solution?
ar = [5, 5, 2, 2, 2, 6, 6]
collections = []
collect_same = []
while ar.length > 0
first = ar.values_at(0).join.to_i
second = ar.values_at(1).join.to_i
if ar.length == 1
collect_same << ar[0]
collections << collect_same
break
else
sum = ar.values_at(0, 1).inject {|a,b| a + b}
if second == first
p collect_same << ar[0]
ar.shift
else
collect_same << ar[0]
collections << collect_same
collect_same = []
ar.shift
end
end
end
p collections
The output:
=> [[5, 5], [2, 2, 2], [6, 6]]
Note, that in primary array same numbers always goes one after another.
So I wouldn't have primary array like this - ar = [1, 2, 1, 2]
Using chunk_while:
[5, 5, 2, 2, 2, 6, 6].chunk_while(&:==).to_a
#=> [[5, 5], [2, 2, 2], [6, 6]]
Ruby prior to 2.3:
[5, 5, 2, 2, 2, 6, 6].each_with_object([]) do |e, acc|
acc.last && acc.last.last == e ? acc.last << e : acc << [e]
end
#=> [[5, 5], [2, 2, 2], [6, 6]]
In case if you want to do it without order:
ar.group_by(&:itself).values
=> [[5, 5], [2, 2, 2], [6, 6]]
[5, 5, 2, 2, 2, 6, 6].slice_when(&:!=).to_a
#=> [[5, 5], [2, 2, 2], [6, 6]]
One could perhaps say that Enumerable#chunk_while and Enumerable#slice_when are ying and yang.
Prior to Ruby v2.3, one might write
[5, 5, 2, 2, 2, 6, 6].chunk(&:itself).map(&:last)
and prior to v2.2,
[5, 5, 2, 2, 2, 6, 6].chunk { |n| n }.map(&:last)
just another oneliner
arr = [5, 5, 2, 2, 2, 6, 6]
arr.uniq.map {|e| [e]*arr.count(e) }
# => [[5, 5], [2, 2, 2], [6, 6]]

Finding index and size of consecutive repeated elements in an array

An array consists of 1, 2, and 0s. I am trying to identify the maximum repetition and its starting index within the array.
Example:
2 2 1 0 2 2 2 0 1 1
The method should accept an integer arguement, which can be one of the numbers 1 or 2
If we demonstrate these inputs on above array, the outputs would be:
find_duplicates(2)
=> 3,4
find_duplicates(1)
=> 2,8
where the first number indicates the size of the duplication, and second is the starting index of it.
I tried looping through the array and compare with arr[i+1] or arr[-1], but this is not the correct approach. Any help will be greatly appreciated.
Edit:
I had not pasted what I had tried at the time I asked the question, this is not something I would do if I could feel some confidence on the way I followed:
def find_status(arr,participant)
status = Array.new
#arr is a two dimensional array
for i in 0...arr.length do
current_line=arr[i]
cons=0
for j in 0...current_line.length do
#I worked on lots of if/else/case statements here, this is just one of them
if current_line[j] == participant
cons+=1 #count consecutive
if current_line[j]!=participant
cons=0
end
end
status[i] = cons
end
end
return status
end
def max_run(arr, target)
_,b = arr.each_with_index.
chunk { |n,_| n==target }.
select { |tf,_| tf==true }.
max_by { |_,a| a.size }
b ? [b.size, b.first.last] : nil
end
arr = [1,1,2,2,2,3,1,1,1,1,2,2,2,2,3,3]
max_run(arr,1) #=> [4, 6]
max_run(arr,2) #=> [4, 10]
max_run(arr,3) #=> [2, 14]
max_run(arr,4) #=> nil
For target = 2, the steps are as follows:
enum0 = arr.each_with_index
#=> #<Enumerator: [1, 1, 2, 2, 2, 3, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3]
# :each_with_index>
We can see the elements that will be generated by this enumerator by converting it to an array:
enum0.to_a
#=> [[1, 0], [1, 1], [2, 2], [2, 3], [2, 4], [3, 5], [1, 6], [1, 7], [1, 8],
# [1, 9], [2, 10], [2, 11], [2, 12], [2, 13], [3, 14], [3, 15]]
Continuing,
enum1 = enum0.chunk { |n,_| n==target }
#=> #<Enumerator: #<Enumerator::Generator:0x007f9beb9b0850>:each>
Carefully examine the return value here. You can think of enum1 as a "compound enumerator". It will generate the following values:
enum1.to_a
#=> [[false, [[1, 0], [1, 1]]], [true, [[2, 2], [2, 3], [2, 4]]],
# [false, [[3, 5], [1, 6], [1, 7], [1, 8], [1, 9]]],
# [true, [[2, 10], [2, 11], [2, 12], [2, 13]]], [false, [[3, 14], [3, 15]]]]
Continuing,
c = enum1.select { |tf,_| tf==true }
#=> [[true, [[2, 2], [2, 3], [2, 4]]],
# [true, [[2, 10], [2, 11], [2, 12], [2, 13]]]]
_,b = c.max_by { |_,a| a.size }
#=> [true, [[2, 10], [2, 11], [2, 12], [2, 13]]]
b #=> [[2, 10], [2, 11], [2, 12], [2, 13]]
b ? [b.size, b.first.last] : nil
#=> [[2, 10], [2, 11], [2, 12], [2, 13]] ? [4, [2,10].last]
#=> [4, 10]
a = [2, 2, 1, 0, 2, 2, 2, 0, 1, 1]
longest_sequence =
a.each_index.select{|i| a[i] == 2}.chunk_while{|i, j| i.next == j}.max_by(&:length)
# => [4, 5, 6]
[longest_sequence.length, longest_sequence.first] # => [3, 4]
The solution below is likely most efficient since it is O(N). It walks through an array, collecting the chunks:
arr.each.with_index.reduce({idx:-1, i: -1, len: 0}) do |memo, (e, i)|
memo[:i] = i if memo[:i] == -1 && e == 2 # at the beginning of chunk
memo[:len], memo[:idx] = [i - memo[:i], memo[:i]] \
if memo[:i] >= 0 && i - memo[:i] > memo[:len] # save values if needed
memo[:i] = -1 unless e == 2 # reset index counter
memo
end.reject { |k, _| k == :i } # reject temporary index value
#⇒ {
# :idx => 4,
# :len => 3
# }
To use it as method, accepting a parameter; just wrap the code above with def find_duplicates number and substitute 2 with number in the code above. Yes, it returns hash instead of an array.

How do you replace repeating elements in an array?

I have an array:
[1, 4, 4, 4, 2, 9, 0, 4, 3, 3, 3, 3, 4]
and want to replace the repeating values with a string "repeat". The repeated 4 at indices 1, 2, 3 and 3 at indices 8, 9, 10, 11 should be replaced. I should get:
[1, "repeat", 2, 9, 0, 4, "repeat", 4]
How is this accomplished?
Here are two ways you could do that.
#1 Use Enumerable#chunk:
arr = [1,4,4,4,2,9,0,4,3,3,3,3,4]
arr.chunk(&:itself).map { |f,a| a.size==1 ? f : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
The steps:
enum = arr.chunk(&:itself)
#=> #<Enumerator: #<Enumerator::Generator:0x007febc99fb160>:each>
We can view the elements of this enumerator by converting it to an array:
enum.to_a
#=> [[1, [1]], [4, [4, 4, 4]], [2, [2]], [9, [9]], [0, [0]],
# [4, [4]], [3, [3, 3, 3, 3]], [4, [4]]]
Object#itself was added in Ruby v2.2. For earlier version you would use
enum = arr.chunk { |e| e }
It is now a simple matter to map the elements of enum as required:
enum.map { |f,a| a.size==1 ? f : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
#2 Use Enumerable#slice_when
arr.slice_when { |e,f| e !=f }.map { |a| a.size==1 ? a.first : "repeat" }
The steps:
enum = arr.slice_when { |e,f| e !=f }
#=> #<Enumerator: #<Enumerator::Generator:0x007febc99b8cc0>:each>
a = enum.to_a
#=> [[1], [4, 4, 4], [2], [9], [0], [4], [3, 3, 3, 3], [4]]
a.map { |a| a.size==1 ? a.first : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
slice_when was introduced in Ruby v.2.2.

Resources