Bug #17537
closed=== on ranges of strings is not consistant with include?
Description
Hi,
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 case
s. Reading the documentation it is unclear to me whether this change was intentional.
$ cat /tmp/foo.rb
puts(("1".."12").include?("6"))
puts(("1".."12") === "6")
p(("1".."12").to_a)
$ ruby2.6 /tmp/foo.rb
true
true
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
$ ruby2.7 /tmp/foo.rb
true
false
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
$ ruby3.0 /tmp/foo.rb
true
false
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
Cheers!
Updated by zverok (Victor Shepelev) almost 4 years 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"
else
puts "It is not"
end
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"
# ...
...is 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) almost 4 years 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
1
=> 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".
Cheers!
Updated by zverok (Victor Shepelev) almost 4 years 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) over 3 years ago
- Status changed from Open to Closed