Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the string '3' not matched in a case statement with the range ('0'...'10')?

Tags:

ruby

When I try to match for the string '3' in a case statement, it matches if the range goes up to '9', but not '10'.

I'm guessing it has something to do with the triple equals operator, but I don't know the exact reason why it can be in the range, but not matched.

Here is an IRB run documenting both cases that work (with '9'), and don't work (with '10'):

 case '3'
 when ('0'...'9')
     puts "number is valid"
 else
   puts "number is not valid"
 end

Output: number is valid

 case '3'
 when ('0'...'10')
     puts "number is valid"
 else
   puts "number is not valid"
 end

Output: number is not valid

The methods that I used as a reference for the expected results are
Enumerable#include?
Enumerable#member?
and seeing what is output when converted to an array is (Enumerable#to_a).

The result of the "case equality" (===) operator surprised me.

 puts ('0'...'10').include?('3')
 # => true
 puts ('0'...'10').member?('3')
 # => true
 puts ('0'...'10') === '3'
 # => false
 puts ('0'...'10').to_a
 # => ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
like image 593
jockofcode Avatar asked Apr 23 '21 17:04

jockofcode


2 Answers

Ranges use cover? for case equality. So it is comparing '3' >= '0' && '3' < '10' which results in false because '3' < '10' #=> false. Strings are compared based on character values.

For a better understanding you might want to see a string as an array of characters:

['3'] <=> ['1', '0'] #=> 1 (first operand is larger than the second)

To solve the issue convert your case input to an integer and use integer ranges:

case 3 # or variable.to_i
when 0...10
  puts 'number is valid'
else
  puts 'number is invalid'
end

This works because integers are not compared based on character code, but on actual value. 3 >= 0 && 3 < 10 results in true.

Alternatively you could explicitly tell when to use the member? (or include?) method, by not passing a range, but a method instead.

case '3'
when ('0'...'10').method(:member?)
  puts 'number is valid'
else
  puts 'number is invalid'
end
like image 151
3limin4t0r Avatar answered Nov 20 '22 19:11

3limin4t0r


=== says it's equivalent to cover?, and the documentation for the latter states that it's equivalent to

begin <= obj < end

So, in your case, we're getting

'0' <= '3' < '10'

And <= and < on strings compare using dictionary order, so the comparison is false.

On the other hand, we have to do a bit more digging to figure out what member? / include? actually do (the two are equivalent). If we look in the source code, we see that both invoke a function called range_include_internal which has a special case for string arguments that behaves differently than cover?. The latter calls rb_str_include_range_p which has even more special cases, including your digit case.

like image 34
Silvio Mayolo Avatar answered Nov 20 '22 20:11

Silvio Mayolo