Project

General

Profile

Actions

Bug #17537

closed

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

Added by akim (Akim Demaille) about 3 years ago. Updated about 3 years ago.

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

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 cases. 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) about 3 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) about 3 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) about 3 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)

Actions #4

Updated by jeremyevans0 (Jeremy Evans) about 3 years ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0