Nicolas Poletti
Nicolas Poletti

Reputation: 45

Can I use comparison between two variables inside a Ruby case statement?

I'm trying out this code but getting nil everytime (It's a simplyfied version of a Black Jack game): Is it because I'm comparing two variables in a case statement?

def end_game_message(player_score, bank_score)
  message = ""
  case player_score
  when player_score == 21
    message = "Black Jack!"
  when player_score > bank_score
    message = "You win!"
  when player_score > 21
    message = "You lose!"
  when player_score < bank_score
    message = "You lose"
  when player_score == bank_score
    message = "Push"
  end
  message
end

puts end_game_message(21, 15)

Thanks in advance for any help!

Upvotes: 0

Views: 874

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110755

def end_game_message(player_score, bank_score)
  case player_score
  when 21
    bank_score == 21 ? "Push" : "Black Jack!"
  when (22..)
    "You lose!"
  when (..bank_score-1)
    bank_score <= 21 ? "You lose!" : "You win"
  when (bank_score+1..) 
    "You win!"
  else
    "Push"
  end
end

end_game_message(21,18)  #=> "Black Jack!" 
end_game_message(21,21)  #=> "Push" 
end_game_message(22,22)  #=> "You lose!" 
end_game_message(12,22)  #=> "You win!" 
end_game_message(18,19)  #=> "You lose!" 
end_game_message(19,18)  #=> "You win!" 
end_game_message(18,18)  #=> "Push" 

(..bank_score-1) and (bank_score+1..) are beginless and endless ranges, introduced in Ruby v2.7. They are not essential, of course, as they could be replaced with (0..bank_score-1) and (bank_score+1..30). (...bank_score) could be used in place of (..bank_score-1) but my own style guide states that three-dot ranges are to be avoided except where the end of an infinite range is to be excluded.

Upvotes: 2

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369624

There are actually several different questions in your question.

The first is in the title:

Can I use comparison between two variables inside a Ruby case statement?

And the answer to that is "Yes, of course, why wouldn't you?" You can use any Ruby expression.

Okay, well, if we want to be really pedantic, the answer is "No", for two reasons:

  • You cannot compare variables, you can only compare objects, and variables aren't objects.
  • Ruby doesn't have a case statement, only a case expression. (In fact, Ruby doesn't have statements at all.)

(This may sound overly pedantic, but understanding the difference between variables and objects is fundamental not only in Ruby, but in programming in general, as is the difference between expressions and statements.)

Your next question is, why you get nil. The answer to that is that the last expression in your code is

puts end_game_message(21, 15)

So, your code evaluates to the return value of Kernel#puts, and Kernel#puts is defined to always return nil.

The question that I think you are really asking but isn't actually in your question, is "Why does the case expression not work how I think it does?"

You simply need to remember how the case expression is evaluated. There are two different forms of the case expression. (See section 11.5.2.2.4 The case expression) of the ISO Ruby Language Specification, for example.

The simpler form is what the ISO Ruby Language Specification calls the case-expression-without-expression, which looks like this:

case               # Look ma, no expression
when bar then :bar
when baz then :baz
else          :qux
end

And is evaluated like this:

if    bar then :bar
elsif baz then :baz
else           :qux
end

The other form is the case-expression-with-expression

case foo           # In this case, there is an expression here
when bar then :bar
when baz then :baz
else          :qux
end

Which is evaluated like this:

if    bar === foo then :bar
elsif baz === foo then :baz
else                   :qux
end

In your case, you are using the case-expression-with-expression, so your case expression is evaluated like this:

if    (player_score == 21)         === player_score
  message = "Black Jack!"
elsif (player_score > bank_score)  === player_score
  message = "You win!"
elsif (player_score > 21)          === player_score
  message = "You lose!"
elsif (player_score < bank_score)  === player_score
  message = "You lose"
elsif (player_score == bank_score) === player_score
  message = "Push"
end

Since player_score == 21, player_score > bank_score, player_score > 21, player_score < bank_score, and player_score == bank_score all evaluate to either true or false and player_score is a number, all the branches in your case expression are actually checking something like this:

some_boolean === some_number

Which is never true!

So, how do we fix this?

The simplest fix would be to use the case-expression-without-expression instead, and to do that, the only thing we need to do, is delete the expression after the case:

case
when player_score == 21
  message = "Black Jack!"
when player_score > bank_score
  message = "You win!"
when player_score > 21
  message = "You lose!"
when player_score < bank_score
  message = "You lose"
when player_score == bank_score
  message = "Push"
end

This will be evaluated like this:

if    player_score == 21
  message = "Black Jack!"
elsif player_score > bank_score
  message = "You win!"
elsif player_score > 21
  message = "You lose!"
elsif player_score < bank_score
  message = "You lose"
elsif player_score == bank_score
  message = "Push"
end

Which will do what you want.

If you want to keep the case-expression-with-expression, you need to make sure that the when-argument of each when-clause is something that responds to === in a way that makes sense for your comparison.

We could, for example, use Ranges and Integers. The Range#=== method checks whether the argument is inside the Range and the Integer#=== method checks whether the argument is numerically equal, so we could re-write the case expression like this:

case player_score
when 21
  message = "Black Jack!"
when ((bank_score + 1)..)
  message = "You win!"
when (22..)
  message = "You lose!"
when ...bank_score
  message = "You lose"
when bank_score
  message = "Push"
end

Since the cases are evaluated top to bottom and you can evaluate multiple conditions in the same case, we can re-shuffle these a bit to make them nicer to read:

case player_score
when 21
  message = "Black Jack!"
when bank_score
  message = "Push"
when 21.., ...bank_score
  message = "You lose!"
when (bank_score..)
  message = "You win!"
end

Also, remember that the case expression is an expression, meaning that it evaluates to a value, namely it evaluates to the value of the branch that was evaluated. Therefore, we can "pull out" the assignment to message:

message = case player_score
when 21
  "Black Jack!"
when bank_score
  "Push"
when 21.., ...bank_score
  "You lose!"
when (bank_score..)
  "You win!"
end

Note that this has slightly different semantics than what we had before: Before, the assignment was only evaluated when one of the branches was evaluated, otherwise, the value of message stayed what it was before. Whereas in this form, the assignment is always evaluated, and if none of the branches gets evaluated, the case expression evaluates to nil.

However, the way the case expression is written, all cases are covered, so there will always be a non-nil value assigned.

Once we have made this transformation, we notice that at the beginning of the method, message is assigned the empty string "" and then immediately gets assigned a different value. So, we can get rid of the first assignment, since its effects are immediately overwritten anyway.

And once we have done that, we see that we assign to the message variable and then immediately return it. So, we might just as well return the value of the case expression instead, like this:

def end_game_message(player_score, bank_score)
  case player_score
  when 21
    "Black Jack!"
  when bank_score
    "Push"
  when 21.., ...bank_score
    "You lose!"
  when (bank_score..)
    "You win!"
  end
end

Upvotes: 1

Masafumi Okura
Masafumi Okura

Reputation: 724

IMO, you should use if instead of case in this case. If you'd like to use case, however, the code might look like this:

def end_game_message(player_score, bank_score)
  case player_score
  when 21
    "Black Jack!"
  when -> s { s > bank_score }
    "You win!"
  when -> s { s > 21 }
    "You lose!"
  when -> s { s < bank_score }
    "You lose"
  when -> s { s == bank_score }
    "Push"
  end
end

puts end_game_message(21, 15)

The key point is to give Proc object to when clause. In this case, s is player_score (value given to case clause`).

(And minor improvement: case statement returns value so you don't have to assign a message to local variable)

Upvotes: 6

Related Questions