0
votes

In my Rails application, Strings have gotten smart enough to compare themselves to dates:

$ rails console
Loading development environment (Rails 5.2.4.2)
2.6.6 :001 > '2020-01-01' < Time.now
 => true 

whereas in regular irb, this fails:

2.6.6 :001 > '2020-01-01' < Time.now
Traceback (most recent call last):
        8: from /usr/share/rvm/gems/ruby-2.6.6/bin/ruby_executable_hooks:22:in `<main>'
        7: from /usr/share/rvm/gems/ruby-2.6.6/bin/ruby_executable_hooks:22:in `eval'
        6: from /usr/share/rvm/rubies/ruby-2.6.6/bin/irb:23:in `<main>'
        5: from /usr/share/rvm/rubies/ruby-2.6.6/bin/irb:23:in `load'
        4: from /usr/share/rvm/rubies/ruby-2.6.6/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        3: from (irb):6
        2: from (irb):6:in `rescue in irb_binding'
        1: from (irb):6:in `<'
ArgumentError (comparison of String with Time failed)

I have some concerns about how this is implemented, but I have no idea where to look. I have tried the following:

  • looking in config/initializers
  • searching the entire repo for (class|module) str (regex, case-insensitive)
  • searching the entire repo for comparable (presuming it was something to do with the Comparable module.

I suspect it's being brought in by some gem, but I don't know how to narrow it down. Any advice is welcome.

1

1 Answers

1
votes

This is an interesting question, here's what I think is happening... I believe that calling < on a string, actually calls the <=> method. The c-code for the String#<=> method includes this snippet

static VALUE
rb_str_cmp_m(VALUE str1, VALUE str2)
{
    int result;
    VALUE s = rb_check_string_type(str2);
    if (NIL_P(s)) {
        return rb_invcmp(str1, str2);
    }
    result = rb_str_cmp(str1, s);
    return INT2FIX(result);
}

now I'm no c-programmer, but I think it's inverting the comparison when the argument is not a string. So that some_string < Time.now becomes Time.now > some_string.

Then the Time#compare_with_coercion method is called (in active_support/core_ext/time/calculations.rb) and as a result, the method to_datetime is called on the string argument.

If you put a debugger line into the to_datetime method in activesupport/core_ext/string/conversions, you can trace the call stack and see the trace that leads to my explanation.