Lasonic
Lasonic

Reputation: 901

Finding similar objects located in same index position of arrays in Ruby

I have the following hash:

hash = {"1"=>[ 5, 13, "B",  4, 10],
        "2"=>[27, 19, "B", 18, 20],
        "3"=>[45, 41, "B", 44, 31],
        "4"=>[48, 51, "B", 58, 52],
        "5"=>[70, 69, "B", 74, 73]}

Here is my code:

 if hash.values.all? { |array| array[0] == "B" } ||
    hash.values.all? { |array| array[1] == "B" } ||
    hash.values.all? { |array| array[2] == "B" } ||
    hash.values.all? { |array| array[3] == "B" } ||
    hash.values.all? { |array| array[4] == "B" }
      puts "Hello World"

What my code does is iterates through an array such that if the same element appears in the same index position of each array, it will output the string "Hello World" (Since "B" is in the [2] position of each array, it will puts the string. Is there a way to condense my current code without having a bunch of or's connecting each index of the array?

Upvotes: 1

Views: 236

Answers (5)

Cary Swoveland
Cary Swoveland

Reputation: 110675

If efficiency, rather than readability, is most important, I expect this decidedly un-Ruby-like and uninteresting solution probably would do well:

arr = hash.values
arr.first.size.times.any? { |i| arr.all? { |e| e[i] == ?B } }
  #=> true

Only one intermediate array (arr) is constructed (e.g, no transposed array), and it quits if and when a match is found.

More Ruby-like is the solution I mentioned in a comment on your question:

hash.values.transpose.any? { |arr| arr.all? { |e| e == ?B } } 

As you asked for an explanation of @Phrogz's solution to the earlier question, which is similar to this one, let me explain the above line of code, by stepping through it:

a = hash.values
  #=> [[ 5, 13, "B",  4, 10],
  #    [27, 19, "B", 18, 20],
  #    [45, 41, "B", 44, 31],
  #    [48, 51, "B", 58, 52],
  #    [70, 69, "B", 74, 73]]

b = a.transpose
  #=> [[  5,  27,  45,  48,  70],
  #    [ 13,  19,  41,  51,  69],
  #    ["B", "B", "B", "B", "B"],
  #    [  4,  18,  44,  58,  74],
  #    [ 10,  20,  31,  52,  73]]

In the last step:

b.any? { |arr| arr.all? { |e| e == ?B } } 
  #=> true

(where ?B is shorthand for the one-character string "B") an enumerator is created:

c = b.to_enum(:any?)
  #=> #<Enumerator: [[  5,  27,  45,  48,  70],
  #                  [ 13,  19,  41,  51,  69],
  #                  ["B", "B", "B", "B", "B"],
  #                  [  4,  18,  44,  58,  74],
  #                  [ 10,  20,  31,  52,  73]]:any?>

When the enumerator (any enumerator) is acting on an array, the elements of the enumerator are passed into the block (and assigned to the block variable, here arr) by Array#each. The first element passed into the block is:

arr = [5, 27, 45, 48, 70]

and the following is executed:

arr.all? { |e| e == ?B }
  #=> [5, 27, 45, 48, 70].all? { |e| e == ?B }
  #=> false

Notice that false is returned to each right after:

5 == ?B
  #=> false

is evaluated. Since false is returned, we move on to the second element of the enumerator:

[13, 19, 41, 51, 69].all? { |e| e == ?B }
  #=> false

so we continue. But

["B", "B", "B", "B", "B"].all? { |e| e == ?B }
  #=> true

so when true is returned to each, the latter returns true and we are finished.

Upvotes: 0

Lasonic
Lasonic

Reputation: 901

I ended up using the following:

fivebs = ["B","B","B","B","B"]

if hash.values.transpose.any? {|array| array == fivebs}
  puts "Hello World"

Upvotes: 0

tessi
tessi

Reputation: 13574

Assuming all arrays are always of the same length, the following gives you the column indexes where all values are equal:

hash.values.transpose.each_with_index.map do |column, index|
  index if column.all? {|x| x == column[0] }
end.compact

The result is [2] for your hash. So you know that for all arrays the index 2 has the same values. You can print "Hello World" if the resulting array has at least one element.

How does it work?

hash.values.transpose gives you all the arrays, but with transposed (all rows are now columns) values:

hash.values.transpose
=> [[5, 27, 45, 48, 70],
    [13, 19, 41, 51, 69],
    ["B", "B", "B", "B", "B"],
    [4, 18, 44, 58, 74],
    [10, 20, 31, 52, 73]]

.each_with_index.map goes over every row of the transposed array while providing an inner array and its index. We look at every inner array, yielding the column index only if all elements are equal using all?.

hash.values.transpose.each_with_index.map {|column, index| index if column.all? {|x| x == column[0] }
=> [nil, nil, 2, nil, nil]

Finally, we compact the result to get rid of the nil values.


Edit: First, I used reduce to find the column with identical elements. @Nimir pointed out, that I re-implemented all?. So I edited my anwer to use all?.

Upvotes: 2

Nimir
Nimir

Reputation: 5839

From @tessi brilliant answer i though of this way:

hash.values.transpose.each_with_index do |column, index|
  puts "Index:#{index} Repeated value:#{column.first}" if column.all? {|x| x == column[0]}
end
#> Index:2 Repeated value:B

How?

Well, the transpose already solves the problem:

hash.values.transpose
=> [[5, 27, 45, 48, 70],
    [13, 19, 41, 51, 69],
    ["B", "B", "B", "B", "B"],
    [4, 18, 44, 58, 74],
    [10, 20, 31, 52, 73]
]

We can do:

column.all? {|x| x == column[0]}

To find column with identical items

Upvotes: 2

CDub
CDub

Reputation: 13354

Assuming that all the values of the hash will be arrays of the same size, how about something like:

hash
=> {"1"=>[5, 13, "B", 4, 10], "2"=>[27, 19, "B", 18, 20], "3"=>[45, 41, "B", 44, 31], "4"=>[48, 51, "B", 58, 52], "5"=>[70, 69, "B", 74, 73]}

arr_of_arrs = hash.values
=> [[5, 13, "B", 4, 10], [27, 19, "B", 18, 20], [45, 41, "B", 44, 31], [48, 51, "B", 58, 52], [70, 69, "B", 74, 73]]

first_array = arr_of_arrs.shift
=> [5, 13, "B", 4, 10]

first_array.each_with_index do |element, index|
  arr_of_arrs.map {|arr| arr[index] == element }.all?
end.any?
=> true

This is not really different from what you have now, as far as performance - in fact, it may be a bit slower. However, it allows for a dynamic number of incoming key/value pairs.

Upvotes: 1

Related Questions