Search code examples
rubycastingcomparisonrangecoerce

Why doesn't `Range#cover?` raise an exception when comparison fails?


Given that Time objects cannot be compared with Fixnum without explicit casting:

0 <= Time.now # => ArgumentError: comparison of Fixnum with Time failed
Time.now <= 10000000000 # => ArgumentError: comparison of Time with 10000000000 failed

and what the documentation for Range#cover? says,

cover?(obj)true or false

Returns true if obj is between the begin and end of the range.

This tests begin <= obj <= end when exclude_end? is false and begin <= obj < end when exclude_end? is true.

I expect:

(0...10000000000).cover?(Time.now) # => false

to raise an exception rather than silently return false. Why doesn't it raise an exception?

It is understandable that, with explicit casting, the comparison works:

(0...10000000000).cover?(Time.now.to_i) # => true

Solution

  • The doc doesn't mention an implementation detail. range_cover is implemented in terms of r_less (via r_cover_p). And r_less comment says:

    /* compares _a_ and _b_ and returns:
     * < 0: a < b
     * = 0: a = b
     * > 0: a > b or non-comparable
     */
    

    Here is the source of r_cover_p:

    static VALUE
    r_cover_p(VALUE range, VALUE beg, VALUE end, VALUE val)
    {
      if (r_less(beg, val) <= 0) {
        int excl = EXCL(range);
        if (r_less(val, end) <= -excl)
          return Qtrue;
      }
      return Qfalse;
    }
    

    As we can see, a positive number returned from either of r_less invocations will result in a Qfalse.

    Now, the reason why the doc doesn't mention it, I think, is to keep it light. Normally (99.9999% of cases), you're supposed to compare comparable things, right? And in the odd case you don't, you still get a correct answer ("this Time does not belong to this range of integers").