Ruby
Range#include? vs. Range#cover?
Today we are going to be comparing Ruby’s Range#include?
and Range#cover?
methods.
For starters, what is the difference between the two? They both seem to answer the same question of "Is this object contained inside this range?"
First, let’s check to see how the two methods perform against each other by checking how long it takes for them to process 1 million times.
Run times will vary by machine. The output presented here was the average result of running these code blocks 100 times on my personal machine.
Range#include?
range = ("a".."z")
string = "m"
start_time = Time.now
1_000_000.times do
range.include?(string)
end
Time.now - start_time
=> 0.28333794
Range#cover?
range = ("a".."z")
string = "m"
start_time = Time.now
1_000_000.times do
range.cover?(string)
end
Time.now - start_time
=> 0.20700693
So if you were to look at the individual results of Range#cover?
compared to Range#include?
you would see for yourself that Range#cover?
is faster every time. Well then, should we just always be using Range#cover?
?
Well, not quite. Let’s take a look at what these two methods are really doing.
Say we have a string with the value of "dog"
and a range of ("a".."z")
—That is, every single letter from a-z. ["a", "b", "c"…"x", "y", "z"]
Then, if we ask the question from earlier, “Is my string contained inside this range?”, We would obviously expect that answer to be a definite no!
Well, let’s take a look at the actual results:
range = ("a".."z")
range.include?("dog")
=> false
range.cover?("dog")
=> true
range.cover?("zebra")
=> false
Wait, what? That seems strange, doesn’t it? If these are both answering the same question, why does Range#cover?
give us seemingly different answers? Why does "a".."z" cover dogs but not zebras?
Well, this is where we get our performance difference. Range#include?
is slower because it is checking every element in the range to see if it is equal to the target value.
In our example, it would look like this:
# Generic example of what Range#include? is doing under the hood.
"a" == "dog"
=> false
"b" == "dog"
=> false
"c" == "dog"
=> false
...
"z" == "dog"
=> false
Since none of those returned true
, range.include?(string)
returned false
.
Range#cover?
on the other hand, compares the string to the first element of the range, and the last element of the range. If it is greater than the start and less than the end, then it returns true. So, it saves time from having to check through every single element of the range.
In our example, it would look like this:
# Generic example of what Range#cover? is doing under the hood
"dog" >= "a" && "dog" <= "z"
=> true
So we can see, it is much quicker but we can also see that we can get some false-positives, if we’re not careful.
If you're still confused as to why some strings might be covered while others might not be covered (like dog and zebra), it might help if you see them sorted alphabetically.
["a", "z", "dog", "zebra"].sort
=> ["a", "dog", "z", "zebra"]
In Conclusion
Range#cover?
and Range#include?
answer the same question, but in different ways.
While cover?
is a faster method, it is consequently a more generic method. It works better for more natural ranges, like dates.
If you need to check the range more specifically, like in the case of strings, then #include?
is the method for you.
Photo by: Jeff Griffith on Unsplash