3

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!

George Tucker
  • 33
  • 1
  • 7
  • 1
    As well as [`include?`](http://ruby-doc.org/core-2.4.2/Range.html#method-i-include-3F) which has already been mentioned, [`cover?`](http://ruby-doc.org/core-2.4.2/Range.html#method-i-cover-3F) and [`between?`](http://ruby-doc.org/core-2.4.2/Comparable.html#method-i-between-3F) may also be of interest. – Sagar Pandya Nov 28 '17 at 19:41

5 Answers5

4

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 ...

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • Would it be a bad idea to monkey patch `===` to `Array` (so that elements are compared with `===`) and to then use `when [1..3, 2]`? – Stefan Pochmann Nov 28 '17 at 20:01
  • @StefanPochmann I'd tend to say that monkey patching to change standard behavior is a bad idea, monkey patching to add methods tends to be less of a bad idea. Debugging a NoMethodError is usually easy, debugging an operator that isn't behaving normally is a little harder. I didn't mention that option because the OP is new at Ruby and monkey is dangerous. – mu is too short Nov 28 '17 at 20:04
  • Ok, thanks. I'm also new at Ruby, that's why I was wondering. I had already done it just to see whether I can, but I lack the judgement of whether it's good. And now I agree, changing existing behavior like this is probably bad. – Stefan Pochmann Nov 28 '17 at 20:11
  • 1
    Hmm... what do you think about adding that `===` in a *subclass* and then using `when Case[1..3, 1]`? Demo: https://ideone.com/Xq6qd7 – Stefan Pochmann Nov 28 '17 at 20:25
  • @StefanPochmann Something like that is clever, reads well, is narrowly targeted, and doesn't have a lot of sharp edges so yeah, I'd support that sort of thing. – mu is too short Nov 28 '17 at 20:58
  • Thanks, turned it into an answer now. And thanks for your answer pretty much pointing out exactly what I had to do :-) – Stefan Pochmann Nov 29 '17 at 00:01
  • This is really helpful -- made clear I was using [] as *punctuation* rather than to deliberately create an array. Total rookie mistake. – George Tucker Nov 29 '17 at 19:48
4

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

Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107
2

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.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
0

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

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
Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107
  • Just for clarification - this was an artificial MCVE. Original problem was to offer results of a rand(1..100) number with a second dimension, a, b, or c. I didn't want to write 3 different case statements duplicating results. Then I figured out I could write a single case statement by converting the second dimension to an integer, combine with original random number, and use when 1..10 || 101..105 || 201..203 do x. Just for clarification. – George Tucker Dec 01 '17 at 20:36
  • @GeorgeTucker Ok, thanks. That's what I suspected and why I wrote my other answer first. Still sounds similar, though, so maybe some of these solutions could be adapted to it. – Stefan Pochmann Dec 01 '17 at 20:41