FEATURED IN RUBY WEEKLY ISSUE #229
Once in a while working with Rails we encounter something that makes us scratch our heads. When this happens, I make a point to try to figure it out. There is a lot to be learned in the process.
In this adventure, David stumbles upon some strange Rails behavior and together we investigate.
UPDATE: David encounters some more fun with Rails dates and
Duration in this follow-up post: Rails’
1.month has a variable length
There is some strange behavior involving Rails
Date class and durations like
> Date.current + 1.day => 2015-01-15 > 1.day => 86400 > Date.current + 86400 => 2251-08-05
1.day to the current date returns tomorrow’s date, as expected.
86400 the number of seconds in a day. But adding
86400 to the current date returns the date 86,400 days from today. WTF?
They must be different classes?
> 1.day.class => Fixnum > 86400 => Fixnum
No, they’re both
Fixnum. How does
Date#+ know the difference?
# rails/activesupport/lib/active_support/core_ext/date/calculations.rb class Date ... def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other other.since(self) else plus_without_duration(other) end end alias_method :plus_without_duration, :+ alias_method :+, :plus_with_duration ... end
Date#plus_with_duration) has special handling for
ActiveSupport::Duration instances, and everything else goes to default Ruby
1.day is a
> 1.day.class => Fixnum > 1.day.is_a? ActiveSupport::Duration => true
Apparently it’s also an
Fixnum as well as an
Duration inherit from
Fixnum, or override
Neither. It inherits from
ProxyObject. Hmm, this looks like something:
# rails/activesupport/lib/active_support/duration.rb module ActiveSupport ... class Duration < ProxyObject attr_accessor :value, :parts def initialize(value, parts) #:nodoc: @value, @parts = value, parts end ... private def method_missing(method, *args, &block) #:nodoc: value.send(method, *args, &block) end end end
If a method is missing, it calls that method on
@value, which would be a
Fixnum. That makes sense.
#class is missing,
#class is called on
Fixnum. That’s why
But why would
#class be missing on a
Duration? All objects respond to
> 1.day.is_a?(Object) => true
At first glance, yes, but upon closer inspection, not really…
> ActiveSupport::Duration.superclass => ActiveSupport::ProxyObject > ActiveSupport::ProxyObject.superclass => BasicObject > BasicObject.superclass => nil
Duration does not inherit
@value does, and
Duration#is_a? delegates to
# rails/activesupport/lib/active_support/duration.rb module ActiveSupport ... class Duration < ProxyObject ... def is_a?(klass) #:nodoc: Duration == klass || value.is_a?(klass) end ... end end
ActiveSupport::Duration inherits from
ActiveSupport::ProxyObject which inherits from
BasicObject which inherits from nothing. An
ActiveSupport::Duration is technically not an
So what is a
“BasicObject is the parent class of all classes in Ruby. It’s an explicit blank class.”
It’s a blank class.
> BasicObject.new.class NoMethodError: undefined method `class' for #<BasicObject:0x007ff3927616b8> from (pry):40:in `<main>'
Indeed, it doesn’t even respond to
#class. That’s why
NOTE: In Rails 4.2,
ActiveSupport::Duration does not inherit
ActiveSupport::ProxyObject thus not a
Published January 14, 2015