How to group array elements by index? - ruby

I have the array arr which I want to group by indexes given in array idx. I mean,
sub array 1 will end at index 1
sub array 2 will end at index 5
sub array 3 will end at index 7
sub array N will be formed from element at index 8 to last element
of arr
With my current code I can group the first sub array with first index of idx idx[0] = 1.
Then, How to replicate for all indexes within array idx? Thanks in advance.
My current code and output is this:
idx = [1,5,7]
arr = ['a','b','c','d','e','f','g','h','i','j','k']
arr.group_by.with_index { |z, i| i <= idx[0] }.values
=> [["a", "b"], ["c", "d", "e", "f", "g", "h", "i", "j", "k"]]
and my desired output is like this:
output --> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"], ["i", "j", "k"]]
#Indexes --> 0 1 2 3 4 5 6 7 8 9 10

You can use slice_after to slice the array after each item whose index is in idx:
idx = [1, 5, 7]
arr = %w[a b c d e f g h i j k]
arr.enum_for(:slice_after).with_index { |_, i| idx.include?(i) }.to_a
#=> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"], ["i", "j", "k"]]
That enum_for is (unfortunately) needed to chain slice_after and with_index.

Another solution
idx = [1, 5, 7]
arr = ['a','b','c','d','e','f','g','h','i','j','k']
from = 0
arr.map.with_index { |a, i|
if idx.include?(i)
result = arr[from..i]
from = i + 1
end
result
}.compact
=> [["a", "b"], ["c", "d", "e", "f"], ["g", "h"]]

arr = [9, 3, 1, 6, 2, 4, 0, 1, 5, 8]
end_idx = [1, 5, 7]
[-1, *end_idx, arr.size-1].uniq.each_cons(2).
map { |s,e| arr.values_at(s+1..e) }
#=> [[9, 3], [1, 6, 2, 4], [0, 1], [5, 8]]
The steps are as follow:
a = [-1, *end_idx, arr.size-1]
#=> [-1, 1, 5, 7, 9]
b = a.uniq
#=> [-1, 1, 5, 9]
c = b.each_cons(2)
#=> #<Enumerator: [-1, 1, 5, 7, 9]:each_cons(2)>
c.map { |s,e| arr.values_at(s+1..e) }
#=> [[9, 3], [1, 6, 2, 4], [0, 1], [5, 8]]
One can see the elements generated and passed to map by the enumerator c by converting c to an array.
c.to_a
#=> [[-1, 1], [1, 5], [5, 7], [7, 9]]
See Array#values_at.

Related

How to replace this nil value in Ruby style

I have an array like
A=[[a,x,y,z],[nil,e,f,d],[nil,k,j,u],[nil,w,p,k],[b,x,y,z],[nil,e,f,d],[nil,k,j,u],[nil,w,p,k]]
Result
A=[[a,x,y,z],[a,e,f,d],[a,k,j,u],[a,w,p,k],[b,x,y,z],[b,e,f,d],[b,k,j,u],[b,w,p,k]]
You might be noticing that first character of the first array is replacing the upcoming nil value until first character of the first array is b and then nil values are replaced with b.
This one I have achieved through iterating the array and writing the if condition and storing the first character somewhere and then by comparing the first character while I am iterating. But I don't still don't find a ruby way of doing this. Can someone help me here
A = [['a',1,2],[nil,3,4],[nil,5,6],[nil,7,8],['b',9,10],[nil,11,12],[nil,13,14]]
A.each_cons(2){|first, last| last[0] ||= first[0] }
last[0] ||= first[0] means something like "Give last[0] the value of first[0], unless it already has a value (not nil or false)".
arr = [['a',1,2],[nil,3,4],[nil,5,6],[nil,7,8],['b',9,10],[nil,11,12],[nil,13,14]]
last = nil
arr.map do |f,*rest|
if f.nil?
[last, *rest]
else
last = f
[f, *rest]
end
end
#=> [["a", 1, 2], ["a", 3, 4], ["a", 5, 6], ["a", 7, 8],
# ["b", 9, 10], ["b", 11, 12], ["b", 13, 14]]
The steps are as follows:
last = nil
enum = arr.map
#=> #<Enumerator: [["a", 1, 2], [nil, 3, 4], [nil, 5, 6], [nil, 7, 8],
# ["b", 9, 10], [nil, 11, 12], [nil, 13, 14]]:map>
f,*rest = enum.next
#=> ["a", 1, 2]
f #=> "a"
rest
#=> [1, 2]
f.nil?
#=> false
last = f
#=> "a"
[f, *rest]
#=> ["a", 1, 2]
f,*rest = enum.next
#=> [nil, 3, 4]
f.nil?
#=> true
[last, *rest]
#=> ["a", 3, 4]
f,*rest = enum.next
#=> [nil, 5, 6]
f.nil?
#=> true
[last, *rest]
#=> ["a", 5, 6]
f,*rest = enum.next
#=> [nil, 7, 8]
f.nil?
#=> true
[last, *rest]
#=> ["a", 7, 8]
and so on.
Alternatively,
arr.drop(1).each_with_object([arr.first]) { |a,ar|
ar << [a.first || ar.last.first, *a.drop(1)] }
#=> [["a", 1, 2], ["a", 3, 4], ["a", 5, 6], ["a", 7, 8],
# ["b", 9, 10], ["b", 11, 12], ["b", 13, 14]]

How does one create a loop with indefinite nested loops?

say you have a list [ 1 , 2 ,3 ...... n]
if you needed to compare two elements so you would write something like
list = (0..9999).to_a
idx = 0
while idx < list.length
idx2 = idx
while idx2 < list.length
puts list[idx] + list[idx2] if (list[idx] + list[idx2]).odd?
idx2 += 1
end
idx += 1
end
But what if the number of comparisons is not constant and increases?
This code hard codes the comparison by having one loop inside another, but if you needed to compare 4 or more elements how does one write a loop or something that achieves this if you don't know the maximum number of comparisons?
We have a helpful method in ruby to do this, and that is Array#combination:
def find_odd_sums(list, num_per_group)
list.combination(num_per_group).to_a.map(&:sum).select(&:odd?)
end
You can re-implement combination, if you choose to. There are many versions of this function available at Algorithm to return all combinations of k elements from n
This question is not clear. Firstly, the title, which is vague, asks how a particular approach to an unstated problem can be implemented. What you need, at the beginning, is a statement in words of the problem.
I will make a guess as to what that statement might be and then propose a solution.
Given
an array arr;
a positive integer n, 1 <= n <= arr.size; and
a method m having n arguments that are distinct elements of arr that returns true or false,
what combinations of n elements of arr cause m to return true?
We can use the following method combined with a definition of the method m.
def combos(arr, n, m)
arr.combination(n).select { |x| public_send(m, *x) }
end
The key, of course, is the method Array#combination. See also the docs for the methods Enumerable#select and Object#public_send.
Here is its use with the example given in the question.
def m(*x)
x.sum.odd?
end
arr = [1,2,3,4,5,6]
combos(arr, 2, :m)
#=> [[1, 2], [1, 4], [1, 6], [2, 3], [2, 5], [3, 4], [3, 6], [4, 5], [5, 6]]
combos(arr, 3, :m)
#=> [[1, 2, 4], [1, 2, 6], [1, 3, 5], [1, 4, 6], [2, 3, 4], [2, 3, 6],
# [2, 4, 5], [2, 5, 6], [3, 4, 6], [4, 5, 6]]
combos(arr, 4, :m)
#=> [[1, 2, 3, 5], [1, 2, 4, 6], [1, 3, 4, 5], [1, 3, 5, 6], [2, 3, 4, 6], [2, 4, 5, 6]]
See the doc for Array#sum (which made it's debut in Ruby v2.4.
Here's a second example: given an array of letters, which combinations of five letters have two vowels?
VOWEL_COUNTER = %w| a e i o u |.product([1]).to_h.tap { |h| h.default=0 }
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
VOWEL_COUNTER['a']
#=> 1
By setting the hash's default value to zero, VOWEL_COUNTER[k] will return zero if it does not have a key k. For example,
VOWEL_COUNTER['r']
#=> 0
def m(*x)
x.sum { |c| VOWEL_COUNTER[c] } == 2
end
arr = %w| a r t u e v s |
combos(arr, 5, :m)
#=> [["a", "r", "t", "u", "v"], ["a", "r", "t", "u", "s"],
# ["a", "r", "t", "e", "v"], ["a", "r", "t", "e", "s"],
# ["a", "r", "u", "v", "s"], ["a", "r", "e", "v", "s"],
# ["a", "t", "u", "v", "s"], ["a", "t", "e", "v", "s"],
# ["r", "t", "u", "e", "v"], ["r", "t", "u", "e", "s"],
# ["r", "u", "e", "v", "s"], ["t", "u", "e", "v", "s"]]
Note that VOWEL_COUNTER is constructed as follows.
a = %w| a e i o u |
#=> ["a", "e", "i", "o", "u"]
b = a.product([1])
#=> [["a", 1], ["e", 1], ["i", 1], ["o", 1], ["u", 1]]
c = b.to_h
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
With this hash,
c['r']
#=> nil
so we need to set the default value to zero.
VOWEL_COUNTER = c.tap { |h| h.default=0 }
#=> {"a"=>1, "e"=>1, "i"=>1, "o"=>1, "u"=>1}
c['r']
#=> 0
Alternatively, we could have omitted the last step (setting the hash's default to zero), and written
x.sum { |c| VOWEL_COUNTER[c].to_i } == 2
because NilClass#to_i converts nil to zero.
See also the docs for the methods #select, #public_send
I feel like everyone is making this more complicated than it is. You sure got pointed to the right direction (Array#combination, Array#repeated_combination, Array#permutation, Array#repeated_permutation). To accomplish the exact thing you are doing, you can simply do:
list.repeated_combination(2) { |c| puts c.sum if c.sum.odd? }
Check the links above to see the difference between them.
If you want to create a helper method you can, but in my opinion it's not really needed in this case. Replace 2 with the number you are looking for and you got your answer.

How to find the coordinates of a specific matrix-entry in ruby

Say I have a matrix, 5×6, filled with single letters and a few special signs, like this one:
upper = [['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'J'],
['K', 'L', 'M', 'N', 'O'],
['P', 'Q', 'R', 'S', 'T'],
['U', 'V', 'W', 'X', 'Y'],
['Z', ',', '.', ' ', '?']]
How can I determine the coordinates of single letters of a string I loop through?
I found no clear description on the #index method, and all ways I've tried to call it (i.e. upper.index("A") ), have failed.
In the end I'm trying to code a simple version of the two-square encryption method and this is the one step I am currently stumped on.
Thanks for your help!
You could build an array of coordinates:
coordinates = upper.first.each_index.to_a.product(upper.each_index.to_a)
#=> [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [0, 5],
# [1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [1, 5],
# [2, 0], [2, 1], [2, 2], [2, 3], [2, 4], [2, 5],
# [3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5],
# [4, 0], [4, 1], [4, 2], [4, 3], [4, 4], [4, 5]]
And find the item's coordinates by traversing this array:
coordinates.find { |x, y| upper[y][x] == 'A' }
#=> [0, 0]
coordinates.find { |x, y| upper[y][x] == '?' }
#=> [4, 5]
coordinates.find { |x, y| upper[y][x] == '-' }
#=> nil
Alternative approach
Instead of a (two-dimenstional) array of rows, you could use a (one-dimensional) hash of coordinate => value pairs. Example:
str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ,. ?'
upper = {}
str.each_char.with_index { |c, i| upper[i.divmod(5)] = c }
upper
#=> {[0, 0]=>"A", [0, 1]=>"B", [0, 2]=>"C", [0, 3]=>"D", [0, 4]=>"E",
# [1, 0]=>"F", [1, 1]=>"G", [1, 2]=>"H", [1, 3]=>"I", [1, 4]=>"J",
# [2, 0]=>"K", [2, 1]=>"L", [2, 2]=>"M", [2, 3]=>"N", [2, 4]=>"O",
# [3, 0]=>"P", [3, 1]=>"Q", [3, 2]=>"R", [3, 3]=>"S", [3, 4]=>"T",
# [4, 0]=>"U", [4, 1]=>"V", [4, 2]=>"W", [4, 3]=>"X", [4, 4]=>"Y",
# [5, 0]=>"Z", [5, 1]=>",", [5, 2]=>".", [5, 3]=>" ", [5, 4]=>"?"}
The values can be accessed by [x, y] coordinates:
upper[[0, 2]] #=> "C"
and looking up a key is trivial:
upper.key('C') #=> [0, 2]
You can also build an inverse hash that maps values to coordinates:
upper.invert
#=> {"A"=>[0, 0], "B"=>[0, 1], "C"=>[0, 2], "D"=>[0, 3], "E"=>[0, 4],
# "F"=>[1, 0], "G"=>[1, 1], "H"=>[1, 2], "I"=>[1, 3], "J"=>[1, 4],
# "K"=>[2, 0], "L"=>[2, 1], "M"=>[2, 2], "N"=>[2, 3], "O"=>[2, 4],
# "P"=>[3, 0], "Q"=>[3, 1], "R"=>[3, 2], "S"=>[3, 3], "T"=>[3, 4],
# "U"=>[4, 0], "V"=>[4, 1], "W"=>[4, 2], "X"=>[4, 3], "Y"=>[4, 4],
# "Z"=>[5, 0], ","=>[5, 1], "."=>[5, 2], " "=>[5, 3], "?"=>[5, 4]}
I would start with building hash letter ⇒ index:
hash = upper.each_with_index.inject({}) do |memo, (inner, x)|
inner.each_with_index.inject(memo) do |memo, (letter, y)|
memo[letter] = [x,y]
memo
end
end
Now we have a desired hash, so to determine an index:
▶ hash['C']
#⇒ [
# [0] 0,
# [1] 2
# ]
Since according to your question, you are going to iterate through string and find an index for each letter, it is way more efficient not to lookup for an index on every loop iteration. Once this hash is built, the lookup for indices will be as fast as hash lookup.
Will this work for you?
Assuming c represents a character whose index you are interested in, we loop through the 2-D array - and if a inner array contains c, then, we take its index in outer array, and index of c in that inner array, and assign them as array to value pos
upper=[['A','B','C','D','E'],['F','G','H','I','J'],
['K','L','M','N','O'],['P','Q','R','S','T'],
['U','V','W','X','Y'],['Z',',','.',' ','?']]
def find_index(array, c)
# initialize the return value to be empty
pos = []
# each_index is a method that will execute the given block (code inside {..}
# for each element of the array, by passing index as parameter to block.
# In this case 'i' receives index value.
# if array[i].include?(c) checks whether array[i] has an element with
# value contained in variable c. If yes, then, we lookup index of c in
# it by using array[i].index(c).
# We then use array literal notation, such as pos = [x,y], to assign the
# the result
array.each_index { |i| pos = [i, array[i].index(c)] if array[i].include?(c) }
# Return pos - you need not use 'return' keyword, last statement's value is treated as return value in Ruby
return pos
end
# p prints the variable value - in below cases prints the return value of function calls
p find_index(upper, 'C')
p find_index(upper, 't'.upcase)
p find_index(upper, 'S')
x, y = *find_index(upper, '?') # Here we splat the array into two variables
p x
p y
Output
[0, 2]
[3, 4]
[3, 3]
5
4
Here is the another variant of above function which purposefully stays away with advanced Ruby Idioms and tries to keep code easier to understand
def find_index(array, c)
x = nil, y = nil
# Find the index of array that has element c
for i in 0..array.length-1 do
if array[i].include?(c)
x = i
end
end
# Find the index of c within that array
y = array[x].index(c)
return [x, y]
end
The following is one way to do it:
upper = [['A','B','C','D','E','C'],
['F','C','C','C','G','H','I','J'],
['K','L','M','N','O'],
['P','Q','R','S','T'],
['U','V','W','X','Y','C'],
['Z',',','.',' ','?']
]
target = 'C'
upper.each_with_index.with_object([]) { |(a,i),arr|
a.each_with_index { |c,j| arr << [i,j] if c == target } }
#=> [[0, 2], [0, 5], [1, 1], [1, 2], [1, 3], [4, 5]]
Note that Array#index doesn't work when the target appears more than once in an inner array.
If the target can appear at most once in each inner array, you could write:
upper = [['A','B','C','D','E'],
['F','C','G','H','I','J'],
['K','L','M','N','O'],
['P','Q','R','S','T'],
['U','V','W','X','Y','C'],
['Z',',','.',' ','?']
]
target = 'C'
upper.each_with_index.with_object([]) do |(a,i),arr|
j = a.index(target)
arr << [i,j] if j
end
#=> [[0, 2], [1, 1], [4, 5]]
As you are a self-confessed Ruby newbie, this probably looks pretty formidable. It's not so bad, however, if we break it down step-by-step.
We first send the method Enumerable#each_with_index to the "receiver" upper, without a block. If you examine the docs for that method, you'll see that an enumerator is returned:
enum0 = upper.each_with_index
#=> #<Enumerator: [["A", "B", "C", "D", "E"], ["F", "C", "G", "H", "I", "J"],
# ["K", "L", "M", "N", "O"], ["P", "Q", "R", "S", "T"],
# ["U", "V", "W", "X", "Y", "C"],
# ["Z", ",", ".", " ", "?"]]:each_with_index>
Next, the method Enumerator#with_object is sent to enum0, with an argument equal to an empty array (the "object"):
enum1 = enum0.with_object([])
#=> #<Enumerator: #<Enumerator: [["A", "B", "C", "D", "E"],
# ["F", "C", "G", "H", "I", "J"], ["K", "L", "M", "N", "O"],
# ["P", "Q", "R", "S", "T"], ["U", "V", "W", "X", "Y", "C"],
# ["Z", ",", ".", " ", "?"]]:each_with_index>:with_object([])>
(Since enum0 is an instance of the class Enumerator, each_object must be a method of that class.)
As you see, enum1 is another enumerator, which you might think of as a "compound" enumerator. (Inspect the return value above carefully.) We can view the elements of this enumerator by converting it to an array:
enum1.to_a
#=> [[[["A", "B", "C", "D", "E"], 0], []],
# [[["F", "C", "G", "H", "I", "J"], 1], []],
# [[["K", "L", "M", "N", "O"], 2], []],
# [[["P", "Q", "R", "S", "T"], 3], []],
# [[["U", "V", "W", "X", "Y", "C"], 4], []],
# [[["Z", ",", ".", " ", "?"], 5], []]]
enum1 contains six elements, the first being:
[[["A", "B", "C", "D", "E"], 0], []]
The elements of enum1 are passed into the block by Enumerator#each (which calls Array#each). We can use Enumerator#next to sequentially obtain those elements and set the block variables equal to them:
(a,i),arr = enum1.next
#=> [[["A", "B", "C", "D", "E"], 0], []]
a #=> ["A", "B", "C", "D", "E"]
i #=> 0
arr #=> []
The array passed into the block is broken down by Ruby's use of "parallel assignment" and "disambiguation".
We can now perform the block calculation:
j = a.index(target)
#=> j = ["A", "B", "C", "D", "E"].index("C")
#=> 2
arr << [i,j] if j
#=> [] << [0,2] if 2
#=> [[0, 2]]
So now arr (which will be returned when the calculations are complete) equals [[0, 2]].
The next element of enum1 is now passed into the block and assigned to the block variables:
(a,i),arr = enum1.next
#=> [[["F", "C", "G", "H", "I", "J"], 1], [[0, 2]]]
a #=> ["F", "C", "G", "H", "I", "J"]
i #=> 1
arr #=> [[0, 2]]
Notice that arr has been updated. We now perform the block calculation:
j = a.index(target)
#=> ["F", "C", "G", "H", "I", "J"].index("C")
#=> 1
arr << [i,j] if j
#=> [[0, 2]] << [1,1] if 1
#=> [[0, 2], [1, 1]]
The third element of enum1 is passed to the block:
(a,i),arr = enum1.next
#=> [[["K", "L", "M", "N", "O"], 2], [[0, 2], [1, 1]]]
a #=> ["K", "L", "M", "N", "O"]
i #=> 2
arr #=> [[0, 2], [1, 1]]
j = a.index(target)
#=> ["K", "L", "M", "N", "O"].index("C")
#=> nil
arr << [i,j] if j
#=> [[0, 2], [1, 1]] << [2,nil] if nil
so arr is not altered. The remainIng calculations are similar.

Ruby: Invert a hash to also preserve non unique values

I have a hash that looks like this:
{"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}
What I want to do is to make a hash of all existing values with an array of all keys that included it, e.g. turn the above into this:
{1 => ["a"], 2 => ["a", "d"], 3 => ["a", "c", "d"], 4 => ["b", "c"]}
Try this:
module HashReverser
def invert_map
each_with_object({}) do |(key, value), result|
value.each { |v| (result[v] ||= []) << key }
end
end
end
original = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5]}
original.extend(HashReverser).invert_map # => {1=>["a"], 2=>["a"], 3=>["a", "c"], 4=>["b", "c"], 5=>["b", "c"], 6=>["b"]}
I do prefer #Jikku's solution, but there's always another way. Here's one.
(I see this is very close to #Chris's solution. I will leave it for the last line, which is a little different.)
Code
def inside_out(h)
g = h.flat_map { |s,a| a.product([s]) }
.group_by(&:first)
g.merge(g) { |_,a| a.map(&:last) }
end
Example
h = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}
inside_out(h)
#=> {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"],
# 5=>["b", "c"], 6=>["b"], 7=>["d"]}
Explanation
For h above:
a = h.flat_map { |s,a| a.product([s]) }
#=> [[1, "a"], [2, "a"], [3, "a"], [4, "b"], [5, "b"], [6, "b"],
# [3, "c"], [4, "c"], [5, "c"], [7, "d"], [2, "d"], [3, "d"]]
g = a.group_by(&:first)
#=> {1=>[[1, "a"]], 2=>[[2, "a"], [2, "d"]],
# 3=>[[3, "a"], [3, "c"], [3, "d"]],
# 4=>[[4, "b"], [4, "c"]],
# 5=>[[5, "b"], [5, "c"]],
# 6=>[[6, "b"]],
# 7=>[[7, "d"]]}
g.merge(g) { |_,a| a.map(&:last) }
#=> {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"],
# 5=>["b", "c"], 6=>["b"], 7=>["d"]}
An alternate solution:
# Given
h = {"a" => [1, 2, 3], "b" => [4, 5, 6], "c" => [3, 4, 5], "d" => [7, 2, 3]}
h.flat_map {|k, v| v.product [k]}.group_by(&:first).each_value {|v| v.map! &:last }
Or:
h.flat_map {|k, v| v.product [k]}.reduce({}) {|o, (k, v)| (o[k] ||= []) << v; o}
The idea here is that we use Array#product to create a list of inverted single key-value pairs:
product = h.flat_map {|k, v| v.product([k]) }
# => [[1, "a"], [2, "a"], [3, "a"], [4, "b"], [5, "b"], [6, "b"], [3, "c"], [4, "c"], [5, "c"], [7, "d"], [2, "d"], [3, "d"]]
Group them by the value of the first item in each pair:
groups = product.group_by(&:first)
# => {1=>[[1, "a"]], 2=>[[2, "a"], [2, "d"]], 3=>[[3, "a"], [3, "c"], [3, "d"]], 4=>[[4, "b"], [4, "c"]], 5=>[[5, "b"], [5, "c"]], 6=>[[6, "b"]], 7=>[[7, "d"]]}
And then convert the values to a list of the last values in each pair:
result = groups.each_value {|v| v.map! &:last }
# => {1=>["a"], 2=>["a", "d"], 3=>["a", "c", "d"], 4=>["b", "c"], 5=>["b", "c"], 6=>["b"], 7=>["d"]}

Is there any way to create 2 x 2 array/matrix from a larger array/matrix

New to Ruby and learning at the moment.
I am not sure I should use arrays or matrix for this.
I have arrays
[['J','O','I','J','O'],
['I','J','O','J','O'],
['I','I','J','I','J']]
I want to find out the following as you can see in the image.
['J', 'O']
['I', 'J']
What I thought was using Ruby Matrix, but I am not sure if I can divide the original array/matrix to chunk of small 2 by 2 matrix and if it matches with [J, O], [I, J].
Or should I use array and loop through.
I appreciate any inputs.
I suggest you use the method Matrix#minor to do this.
Code
require 'matrix'
def find_in_matrix(arr,sub)
sub_nrows = sub.size
sub_ncols = sub.first.size
rows = Array[*0..arr.size-sub_nrows]
cols = Array[*0..arr.first.size-sub_ncols]
arr_m = Matrix[*arr]
sub_m = Matrix[*sub]
rows.product(cols).select {|i,j| arr_m.minor(i,sub_nrows,j,sub_ncols)==sub_m}
end
Example
arr = [['J','O','I','J','O'],
['I','J','O','J','O'],
['I','I','J','I','J']]
sub = [['J', 'O'],
['I', 'J']]
find_in_matrix(arr,sub) #=> [[0, 0], [1, 1], [1, 3]]
find_in_matrix(arr, [['O'], ['J']]) #=> [[0, 1], [1, 2], [1, 4]]
find_in_matrix(arr, [['O']]) #=> [[0, 1], [0, 4], [1, 2], [1, 4]]
find_in_matrix(arr, [['I','J','O']]) #=> [[0, 2], [1, 0]]
find_in_matrix(arr, [['I','J'],['J','O']]) #=> []
find_in_matrix(arr, [[]]) #=> [[0, 0], [0, 1],...,[0, 5]]
# [1, 0], [1, 1],...,[1, 5]]
# [2, 0], [2, 1],...,[2, 5]]
Explanation
For the example above:
sub_nrows = sub.size #=> 2
sub_ncols = sub.first.size #=> 2
rows = Array[*0..(arr.size-sub_nrows)] #=> [0, 1]
cols = Array[*0..(arr.first.size-sub_ncols)] #=> [0, 1, 2, 3]
arr_m = Matrix[*arr]
#=> Matrix[["J", "O", "I", "J", "O"], ["I", "J", "O", "J", "O"],
# ["I", "I", "J", "I", "J"]]
sub_m = Matrix[*sub]
#=> Matrix[["J", "O"], ["I", "J"]]
a = rows.product(cols)
#=> [[0, 0], [0, 1], [0, 2], [0, 3], [1, 0], [1, 1], [1, 2], [1, 3]]
a.select {|i,j| arr_m.minor(i,sub_nrows,j,sub_ncols)==sub_m}
#=> [[0, 0], [1, 1], [1, 3]]
Consider the first element of a that select passes into the block: [0, 0] (i.e., the block variables i and j are both assigned the value zero). We therefore compute:
arr_m.minor(i,sub_nrows,j,sub_ncols) #=> arr_m.minor(0,2,0,2)
#=> Matrix[["J", "O"], ["I", "J"]]
As
arr_m.minor(0,2,0,2) == sub_m
[0, 0] is selected. On the other hand, for the element [1, 2] of a, i => 1, j => 2, so:
arr_m.minor(i,sub_nrows,j,sub_ncols) #=> arr_m.minor(1,2,2,2)
#=> Matrix[["O", "J"], ["J", "I"]]
which does not equal sub_m, so the element [1, 2] is not selected.
Note that Matrix#minor has two forms. I used the form that takes four parameters. The other form takes two ranges as parameters.
Not directly to answer the question, and not an efficient way to get the matched position:
matrix = [
%w(J O I J O),
%w(I J O J O),
%w(I I J I J)
]
target = [
%w(J O),
%w(I J)
]
matrix.each_cons(target.length).each_with_index do |sub, row|
sub.map{|a| a.each_cons(target[0].length).to_a}.tap do |sub|
head = sub.shift
head.zip(*sub).each_with_index do |m, col|
if m == target
puts "#{row}, #{col}"
end
end
end
end
You can define the following:
def find_in_matrix(matrix, target)
(0..matrix.length-target.length).to_a.product(
(0..matrix.first.length-target.first.length).to_a).select do |x, y|
(0...target.length).to_a.product(
(0...target.first.length).to_a).all? do |test_x, test_y|
matrix[x+test_x][y+test_y] == target[test_x][test_y]
end
end
end
matrix = [["J", "O", "I", "J", "O"],
["I", "J", "O", "J", "O"],
["I", "I", "J", "I", "J"]]
target = [["J", "O"],
["I", "J"]]
find_in_matrix(matrix, target)
=> [[0, 0], [1, 1], [1, 3]]
This solution simply goes over all the sub-matrices of matrix with target's size, and selects the ones that are equal to it.

Resources