Heiman Chan
Heiman Chan

Reputation: 39

Ruby: How to use .each to iteration through an array?

In the card game bridge, four cards are given point values: Jack: 1, Queen: 2, King: 3, Ace: 4. Given an array of strings corresponding to a hand of cards (the cards are represented like so: ["2","3","4","5","6","7","8","9","10","J","Q","K","A"]), return the total number of high card points for that hand.

I can solve this simple problem with while loop, but I would like to learn how to use .each to iterate through an array as well, Here is my code that doesn't work

def high_card_points(hand)
  sum = 0
  hand.each do |i|
    if hand[i] == "J"
      sum += 1
    elsif hand[i] == "Q"
      sum += 2
    elsif hand[i] == "K"
      sum += 3
    elsif hand[i] == "A"
      sum += 4
    end
  end
  sum
end

Now, when I run it, the error no implicit conversion of String into Integer comes out. How should I do it in the right way?

Upvotes: 1

Views: 143

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110755

The error message states, "TypeError (no implicit conversion of String into Integer)" and that the exception was raised in the line hand[i] == "J". The first element passed to the block by each and assigned to the block variable i is i = hand.first #=> "2". We therefore have hand["2"] == "J", or in fact, hand.[]("2"), but the method Array#[] requires its argument to be an integer, and there is "no implicit conversion of String into Integer".

Let me now address a different aspect of your question.

arr = ["2","3","4","5","6","7","8","9","10","J","Q","K","A"]

You could write the following.

arr.reduce(0) do |tot, s|
  tot + 
  case s
  when "J" then 1
  when "Q" then 2
  when "K" then 3
  when "A" then 4
  else 0
  end
end
  #=> 10  

I can hear you. You are saying, "I said I wanted to use .each!". Well, you have! Let me explain.

arr is an instance of the class Array. Array has Module#include'd the module Enumerable, which is why we can invoke the instance method Enumerable#reduce on arr. (Array.included_modules #=> [Enumerable, Kernel]).

Like all other instance methods in Enumerable, Enumerable#reduce (aka inject) requires a receiver that is an instance of the class Enumerator, but arr is an instance of Array, not Enumerator. Ruby gets around this as follows. When reduce is invoked on arr, she sees that arr is not an instance of Enumerator so she checks to see if arr has a method each (that is, whether arr's class Array has an instance method each). It does, so she invokes each on arr to obtain

enum = arr.each
  #=> #<Enumerator: ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J",
  #                  "Q", "K", "A"]:each>

We now have the enumerator on which reduce can be invoked:

enum.reduce(0) do |tot, s|
  tot + 
  case s
  when "J" then 1
  when "Q" then 2
  when "K" then 3
  when "A" then 4
  else 0
  end
end
  #=> 10

You don't see Array#each being invoked, but it certainly is. We can confirm that by including Enumerable in a class that does not have a method each and see what happens.

class C
  include Enumerable
end

c = C.new
  #=> #<C:0x0000000002a118a8>
c.reduce {}
  #=> NoMethodError (undefined method `each' for #<C:0x0000000002a118a8>)

class C
  def each
  end
end

c.reduce {}
  #=> nil

This is why every class that includes Enumerable must have an instance method each that returns an enumerator and why each is invoked on instances of that class before an instance method from Enumerable is called.

Upvotes: 1

anquegi
anquegi

Reputation: 11542

The problem here, is that when you use each the variable inside the block, is the object inside the array not the index, so you can work as follow:

def high_card_points(hand)
  sum = 0
  hand.each do |card|
    if card == "J"
      sum += 1
    elsif card == "Q"
      sum += 2
    elsif card == "K"
      sum += 3
    elsif card == "A"
      sum += 4
    end
  end
  sum
end

and if you execute in pry

[5] pry(main)* => :high_card_points
[6] pry(main)> high_card_points(cards)
=> 10

You can also work whit the index like with each_index. But you can also take an other object-functional aproach:

You can create your class or monkeypatch the class string:

  class String
    def card_points
      case self
      when 'J'
        1
      when 'Q'
        2
      when 'K'
        3
      when 'A'
        4
      else
        0
      end
    end
  end

Then proceed like this:

[31] pry(main)> cards.map(&:card_points).inject(0, :+)
=> 10

Upvotes: 2

Related Questions