George Tucker
George Tucker

Reputation: 33

Using 'case' with multiple conditions

I'm trying to write a case statement which looks at two conditions, like this:

roll1 = rand(1..6)
roll2 = rand(1..2)

result = case[roll1, roll2]
  when [1..3 && 1]
    "Low / Low"
  when [4..6 && 1]
    "High / Low"
  when [1..3 && 2]
    "Low / High"
  when [4..6 && 2]
    "JACKPOT!!"
end

puts result

I'd love to get this working. I'd prefer to understand why my example fails.

Edited to add:

Thanks for all the feedback! Inspired, I realized that combining the two case variables allows me to collapse them into a single value for a simple switch statement...

roll1 = rand(1..6)
roll2 = rand(1..2)

if roll2 == 1
  roll2 = 10
elsif roll2 == 2
  roll2 = 20
end

result = case(roll1 + roll2)
  when 11..13
    "Low / Low"
  when 14..16
    "High / Low"
  when 21..23
    "Low / High"
  when 24..26
    "JACKPOT!!"
end

puts result

While this solves my immediate problem, it doesn't advance my underlying knowledge -- it's a trifling insight compared to all the awesome feedback I've received. Sincere thanks!

Upvotes: 3

Views: 3839

Answers (5)

mu is too short
mu is too short

Reputation: 434685

You have two problems with your code. First of all, this:

[1..3 && 1]

is an array with one element. Since .. has lower precedence than &&, you're really writing 1..(3 && 1) which is just a complicated way of saying 1..1. That means that your case is really:

case[roll1, roll2]
  when [1..1]
    "Low / Low"
  when [4..1]
    "High / Low"
  when [1..2]
    "Low / High"
  when [4..2]
    "JACKPOT!!"
end

The second problem is that Array doesn't override the === operator that case uses so you'll be using Object#=== which is just an alias for Object#==. This means that your case is equivalent to:

if([roll1, roll2] == [1..1])
  "Low / Low"
elsif([roll1, roll2] == [4..1])
  "High / Low"
elsif([roll1, roll2] == [1..2])
  "Low / High"
elsif([roll1, roll2] == [4..2])
  "JACKPOT!!"
end

[roll1, roll2] will never equal [some_range] because Array#== compares element by element and roll1 will never == a range; furthermore, you're also comparing arrays with different sizes.

All that means that you have a complicated way of saying:

result = nil

I'd probably just use an if for this:

result = if (1..3).include?(roll1) && roll2 == 1
           'Low / Low'
         elsif (4..6).include?(roll1) && roll2 == 1
           'High / Low'
         ...

or you could use === explicitly:

result = if (1..3) === roll1 && roll2 == 1
           'Low / Low'
         elsif (4..6) === roll1 && roll2 == 1
           'High / Low'
         ...

but again, watch out for the low precedence of ...

Upvotes: 4

Stefan Pochmann
Stefan Pochmann

Reputation: 28606

If your example is not just an artificial MCVE for the general case but your actual problem, or if your actual problem is really similar, here are a few more ideas:

Treat as two independent problems, combine, handle the special case:

result = "#{roll1 < 4 ? 'Low' : 'High'} / #{roll2 < 2 ? 'Low' : 'High'}".
           sub('High / High', 'JACKPOT!!')

Same idea written differently:

result = [roll1 < 4, roll2 < 2].
           map { |low| low ? 'Low' : 'High' }.
           join(' / ').
           sub('High / High', 'JACKPOT!!')

Same again but a bit silly:

result = [roll1 < 4, roll2 < 2].
           join(' / ').
           gsub('true', 'Low').gsub('false', 'True').
           sub('High / High', 'JACKPOT!!')

Using booleans because they just need == comparison:

result = case [roll1 > 3, roll2 > 1]
         when [false, false]
           "Low / Low"
         when [true, false]
           "High / Low"
         when [false, true]
           "Low / High"
         when [true, true]
           "JACKPOT!!"
         end

(case [(4..6) === roll1, roll2 == 2] would work as well. And note I indented differently.)

Combine the rolls into a single number:

result = case roll1 * (-1)**roll2
         when -3..-1
           "Low / Low"
         when -6..-4
           "High / Low"
         when 1..3
           "Low / High"
         when 4..6
           "JACKPOT!!"
         end

Same idea written differently:

result = case roll2 * 10 + roll1
         when 11..13
           "Low / Low"
         when 14..16
           "High / Low"
         when 21..23
           "Low / High"
         when 24..26
           "JACKPOT!!"
         end

Same again, just different code style:

result = case roll1 * (-1)**roll2
         when -3..-1 then "Low / Low"
         when -6..-4 then "High / Low"
         when  1..3  then "Low / High"
         when  4..6  then "JACKPOT!!"
         end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110685

Except in the case of “JACKPOT!!” you have two separate problems, which are easier to deal with by treating them separately.

def result_of_rolls(roll1, roll2)
  if (4..6).cover?(roll1) && roll2 == 2
    "JACKPOT!!"
  else
    "%s / %s" % [(1..3).cover?(roll1) ? "Low" : "High",
                 roll2 == 1 ? "Low" : "High"]
  end
end

result_of_rolls(2,1) #=> "Low / Low"
result_of_rolls(4,1) #=> "High / Low"
result_of_rolls(3,2) #=> "Low / High"
result_of_rolls(5,2) #=> "JACKPOT!!"

If there were three or more rolls, rather than just two, it can be seen that this approach would be much more efficient than one that examined all the combinations of values of roll1, roll2, roll3, and so on.

Upvotes: 0

Stefan Pochmann
Stefan Pochmann

Reputation: 28606

As the other answers explain in more detail, your when [1..3 && 2] doesn't work because that's actually when [1..2] and because arrays don't compare their elements with === (which when does and which the range would need to do).

Here's another way to make it work, by fixing exactly those two issues.

First, use [1..3, 2] instead of [1..3 && 2], so the two conditions don't get combined but stay separated in the array. Then, to get === used, create a subclass of Array that compares elements with === instead of ==. And use it in the when condition instead of a normal array. Full code:

roll1 = rand(1..6)
roll2 = rand(1..2)

class Case < Array
  def ===(other)
    zip(other).all? { |x, y| x === y }
  end
end

result = case[roll1, roll2]
  when Case[1..3, 1]
    "Low / Low"
  when Case[4..6, 1]
    "High / Low"
  when Case[1..3, 2]
    "Low / High"
  when Case[4..6, 2]
    "JACKPOT!!"
end

puts roll1, roll2, result

That for example prints:

6
2
JACKPOT!!

I guess whether this is good / worth it for you depends on your actual use case. But I like it. And as a Ruby beginner myself, this little exercise helped me understand better how when and === work.

Also see the discussion under @muistooshort's answer for some thoughts about this.

And this answer about what === does was also very illuminating:
https://stackoverflow.com/a/3422349/1672429

Upvotes: 4

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84373

Case Statements, Expressions, and Case Equality

The when-statement doesn't really work the way you seem to think, and neither does an array literal. In its basic form, case compares a top-level expression to the expressions in a when statement using the threequals operator (===).

In Ruby, almost everything is an expression. In your example, you're trying to match an array of values to an array that resolves to a single-element array. Consider:

[1..3 && 2]
#=> [1..2]

[1..3 && 2].count
#=> 1

[1..3 && 2].map &:class
#=> [Range]

Basically, your top-level array never matches any of the array expressions you're providing as conditions. What you want is probably something like this:

result = case
when (1..3).include?(roll1) && roll2.eql?(1)
  "Low / Low"
else
  raise "invalid comparison"
end

In this type of construction, you're not using a threequals expression with a top-level value to compare against. Instead, you're constructing a truthy or falsey Boolean from a pair of expressions. Flesh this out with additional when statements, and then debug your expressions if you're still getting invalid comparisons.

Upvotes: 2

Related Questions