Rails is full of "magic", but if you dig a little, that magic is just some ruby code. For instance there is 1.day.ago
, which returns a DateTime
object for 1 day ago.
Did you ever wonder how does this work? Let's get to the bottom of it.
First of all, let's break down this call. We call the day
method on 1, which is an Integer
and whatever that call returns, we call the ago
method on that object.
If you open a Rails console, you can find out where the day
method is defined:
Loading development environment (Rails 7.0.2.2)
irb(main):001:0> 1.method(:day).source_location
=>
["/Users/gregmolnar/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-7.0.2.2/lib/active_support/core_ext/numeric/time.rb",
37]
active_support/core_ext/numeric/time.rb
, we can see the implementation:
# frozen_string_literal: true
require "active_support/duration"
require "active_support/core_ext/time/calculations"
require "active_support/core_ext/time/acts_like"
require "active_support/core_ext/date/calculations"
require "active_support/core_ext/date/acts_like"
class Numeric
...
# Returns a Duration instance matching the number of days provided.
#
# 2.days # => 2 days
def days
ActiveSupport::Duration.days(self)
end
alias :day :days
...
end
day
is an alias of days
, which calls ActiveSupport::Duration.days
with the integer
we call the method on. Let's look at what this days method does, but first we need to find where it's located:
irb(main):003:0> ActiveSupport::Duration.method(:days).source_location
=> ["/Users/gregmolnar/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-7.0.2.2/lib/active_support/duration.rb", 166]
# frozen_string_literal: true
require "active_support/core_ext/array/conversions"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/object/acts_like"
require "active_support/core_ext/string/filters"
module ActiveSupport
# Provides accurate date and time measurements using Date#advance and
# Time#advance, respectively. It mainly supports the methods on Numeric.
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
class Duration
...
class << self
def days(value) # :nodoc:
new(value * SECONDS_PER_DAY, { days: value }, true)
end
end
...
end
end
day
returns an instance of ActiveSupport::Duration
and we call the ago
method on that instance.
As the comment at the top of this class mentions, this is just a wrapper on top of Time.now.advance(days: -1)
, but if we got this far, let's see the method:
# Calculates a new Time or Date that is as far in the past
# as this Duration represents.
def ago(time = ::Time.current)
sum(-1, time)
end
...
def sum(sign, time = ::Time.current)
unless time.acts_like?(:time) || time.acts_like?(:date)
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
if @parts.empty?
time.since(sign * value)
else
@parts.inject(time) do |t, (type, number)|
if type == :seconds
t.since(sign * number)
elsif type == :minutes
t.since(sign * number * 60)
elsif type == :hours
t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
end
end
end
...
ago
calls sum
with -1 and by default with the current time and the definition of sum
is where the magic is happening. But we don't what the @parts
instance variable holds, so let's look at the initialize method to see if it is set there:
def initialize(value, parts, variable = nil) # :nodoc:
@value, @parts = value, parts
@parts.reject! { |k, v| v.zero? } unless value == 0
@parts.freeze
@variable = variable
if @variable.nil?
@variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) }
end
end
@parts
in our case is set to { days: 1 }
, so we hit the final else
condition which calls advance
on the time object with days
and -1 * 1
as parameters. As the comment at the top of the class said, we indeed ended up just calling Time.now.advance(days: -1)
afterall.
I hope you managed to follow along and now Rails shouldn't feel so magical anymore. It is just ruby code at the end of the day, with a lot of handy abstractions.