lolcat
lolcat

Reputation: 119

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!

Upvotes: 4

Views: 1077

Answers (4)

Stefan
Stefan

Reputation: 114248

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

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110745

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.

Upvotes: 1

Wand Maker
Wand Maker

Reputation: 18762

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

Upvotes: 1

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121010

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.

Upvotes: 2

Related Questions