Bug #17537

=== on ranges of strings is not consistant with include?

Added by akim (Akim Demaille) about 2 months ago. Updated 25 days ago.

Target version:
ruby -v:
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin18]



In Ruby up to 2.6 both ("1".."12").include?("6") and ("1".."12") === "6" were true. In 2.7 and 3.0, include? accepts "6", but === does not. This was very handy in cases. Reading the documentation it is unclear to me whether this change was intentional.

$ cat /tmp/foo.rb
puts(("1".."12") === "6")
$ ruby2.6 /tmp/foo.rb
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
$ ruby2.7 /tmp/foo.rb
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
$ ruby3.0 /tmp/foo.rb
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]


Updated by zverok (Victor Shepelev) about 2 months ago

It was intentional.

Before 2.6, === was using include? underneath, which had some undesirable consequences:

case '2.5.2'
when '2.5'..'2.6'
  puts "It is between 2.5 and 2.6"
  puts "It is not"

This prints "It is between 2.6 and 2.6" in Ruby 2.7, but prints "It is not" on 2.5. It seems that "logically" the former is right.

So, in Ruby 2.6, === was changed to use cover? -- but, somehow, not for strings.
It was considered an oversight and was changed in 2.7.

case "6"
when "1".."12"
# ... somewhat semantically "wrong" (you are expecting to strings being compared by their numeric values), but strings are tricky, and I agree there are many edge cases which can be considered "weird"/"inconsistent", there are several tickets (I don't remember the numbers by heart) complaining about strings being between the range ends, but not in the range's items list, or vice versa.

Updated by akim (Akim Demaille) about 2 months ago

Ok. Well, my personal opinion is that just to have some fancy way to handle version strings, ranges of strings have inconsistent semantics. With to_a, they behave like their natural order is shortlex on some imaginary alphabet:

irb(main):005:0> ('a'..'aa').to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa"]

but === behaves like it's using only the lexical order:

irb(main):001:1* case 'ANA'
irb(main):002:1* when 'A'..'Z' then puts 1
irb(main):003:1* else puts 2
irb(main):004:0> end
=> nil

If the point was to support version strings, of course that does not work properly in all the cases, just the simple ones.

irb(main):001:1* case '2.6.5'
irb(main):002:1* when '2.4'..'2.10'
irb(main):003:1*   puts 'matches'
irb(main):004:1* else
irb(main):005:1*   puts 'nope :('
irb(main):006:0> end
nope :(
=> nil

Version strings should be handled, well, with their specific semantics. So you need ranges of Versions.

I'm sad that global consistency (ranges have a set-semantics, and === means ∋) was sacrificed for some nice-looking-broken-example.

Just to be clear: I agree that expecting "6" to be part of "1".."12" is weird (but I really don't think it is less weird to expect "ANA" to be part of "A".."Z".). But at least the language was consistent: "6" is there when one iterates over "1".."12".


Updated by zverok (Victor Shepelev) about 2 months ago

ranges of strings have inconsistent semantics

That's an inherent property of string ranges, and of Range in general.
Range in Ruby represents TWO things:

  • sequence (enumeration) from begin to end (based on the notion of the "next value", e.g. <Type>#succ)
  • continuous space between begin and end (based on the notion of the order, e.g. <Type>#<=>)

The discrepancy between two is always present (e.g. 1...3 covers 1.5 as a space, but doesn't include it as a sequence), and indeed, with Strings, it is most noticeable.

It is true, though, not only for ranges but for other string "math" too:

str = 'b'
str < 'я' # => true
str = str.succ while str < 'я' # infinite cycle, the string will "inrease" to "z", then turns to "aa", and so on

That's because the notion of the "next string" is quite synthetic, it works for some cases, but in general, it is a very limited "convenience" feature.

On the other hand, string order is more or less generic.
It is impossible to make them fully consistent, and introducing "special" cases for "number-alike strings", for example, is unwanted.

"Versions" example was not the main reason for a change, it was just a counter-example for your "1".."10" one, to show that "what's intuitive" depends on the case. (And of course, both of our "intuitive" examples are better solved with better types: mine with Gem::Version, yours with Numeric)


Updated by jeremyevans0 (Jeremy Evans) 25 days ago

  • Status changed from Open to Closed

Also available in: Atom PDF