ActiveSupport如何处理月份金额?

Joh*_*hir 3 ruby ruby-on-rails activesupport

我很高兴和惊讶地发现ActiveSupport以我想要的方式完成月份总和.无论在这几个月中有多少天,添加1.month到某个特定的日期Time都会让您在同一天的日子里登陆Time.

> Time.utc(2012,2,1)
=> Wed Feb 01 00:00:00 UTC 2012
> Time.utc(2012,2,1) + 1.month
=> Thu Mar 01 00:00:00 UTC 2012
Run Code Online (Sandbox Code Playgroud)

activesupport提供的months方法Fixnum没有给出线索:

def months
  ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
end
Run Code Online (Sandbox Code Playgroud)

按照...中的+方法Time

def plus_with_duration(other) #:nodoc:
  if ActiveSupport::Duration === other
    other.since(self)
  else
    plus_without_duration(other)
  end
end
Run Code Online (Sandbox Code Playgroud)

...使我们sinceFixnum...

def since(time = ::Time.current)
  time + self
end
Run Code Online (Sandbox Code Playgroud)

......这导致我们无处可去.

ActiveSupport(或其他)如何/在哪里进行聪明的月份数学而不是仅仅添加30天?

Tyl*_*ick 5

这是一个非常好的问题.简短的回答是,这1.month是一个ActiveSupport::Duration对象(如您所见),其身份以两种不同的方式定义:

  • as 30.days(如果你需要/尝试将其转换为几秒钟),
  • 为1个月(如果您尝试将此持续时间添加到日期).

你可以通过检查它的parts方法看到它仍然知道它相当于1个月:

main > 1.month.parts
=> [[:months, 1]]
Run Code Online (Sandbox Code Playgroud)

一旦你看到它仍然知道它正好是1个月的证据,那么Time.utc(2012,2,1) + 1.month即使在没有完全29天的月份中计算得出正确的结果也不那么神秘,以及为什么它给出的结果与Time.utc(2012,2,1) + 30.days给出的结果不同.

怎么ActiveSupport::Duration隐瞒自己的真实身份?

对我来说真正的神秘之处在于它如何很好地隐藏了它的真实身份.我们知道这是一个ActiveSupport::Duration对象,但很难让它承认它是!

当你在控制台中检查它时(我正在使用Pry),它看起来就像(并声称)正常的Fixnum对象:

main > one_month = 1.month
=> 2592000

main > one_month.class
=> Fixnum
Run Code Online (Sandbox Code Playgroud)

它甚至声称等同于30.days(或2592000.seconds),我们已经证明这是不正确的(至少在所有情况下都不是这样):

main > one_month = 1.month
=> 2592000

main > thirty_days = 30.days
=> 2592000

main > one_month == thirty_days
=> true

main > one_month == 2592000
=> true
Run Code Online (Sandbox Code Playgroud)

因此,要找出对象是否是一个对象ActiveSupport::Duration,您不能依赖该class方法.相反,你必须直截了当地问:"你或者你不是ActiveSupport :: Duration的实例吗?" 面对这样一个直接的问题,有问题的对象别无选择,只能承认事实:

main > one_month.is_a? ActiveSupport::Duration
=> true
Run Code Online (Sandbox Code Playgroud)

另一方面,单纯的Fixnum对象必须垂头丧气并承认它们不是:

main > 2592000.is_a? ActiveSupport::Duration
=> false
Run Code Online (Sandbox Code Playgroud)

您还可以通过检查它是否响应来区分常规Fixnums :parts:

main > one_month.parts
=> [[:months, 1]]

main > 2592000.parts
NoMethodError: undefined method `parts' for 2592000:Fixnum
from (pry):60:in `__pry__'
Run Code Online (Sandbox Code Playgroud)

拥有一系列零件非常棒

拥有一系列零件的一件很酷的事情是它允许你将持续时间定义为单位的混合,如下所示:

main > (one_month + 5.days).parts
=> [[:months, 1], [:days, 5]]
Run Code Online (Sandbox Code Playgroud)

这使它可以准确计算如下事项:

main > Time.utc(2012,2,1) + (one_month + 5.days)
=> 2012-03-06 00:00:00 UTC
Run Code Online (Sandbox Code Playgroud)

...这将能够正确计算,如果简单地存储只是许多为它的价值.如果我们首先转换1.month为"等效"秒数或天数,您可以自己查看:

main > Time.utc(2012,2,1) + (one_month + 5.days).to_i
=> 2012-03-07 00:00:00 UTC

main > Time.utc(2012,2,1) + (30.days + 5.days)
=> 2012-03-07 00:00:00 UTC
Run Code Online (Sandbox Code Playgroud)

ActiveSupport::Duration工作怎么样?(血腥实施细节)

ActiveSupport::Duration实际上,它被定义为(in gems/activesupport-3.2.13/lib/active_support/duration.rb)作为子类BasicObject,根据文档,"可用于创建独立于Ruby的对象层次结构的对象层次结构,代理对象(如Delegator类),或其他用途,其中来自Ruby的方法和类的命名空间污染必须要避免."

ActiveSupport::Duration用于method_missing将方法委托给其@value变量.

奖金问题:有没有人知道为什么一个ActiveSupport::Duration对象声称没有响应,:parts即使它确实存在,为什么部分方法没有列在方法列表中?

main > 1.month.respond_to? :parts
=> false

main > 1.month.methods.include? :parts
=> false

main > 1.month.methods.include? :since
=> true
Run Code Online (Sandbox Code Playgroud)

:因为BasicObject没有定义respond_to?方法,发送respond_to?ActiveSupport::Duration对象将最终调用其method_missing方法,如下所示:

def method_missing(method, *args, &block) #:nodoc:
  value.send(method, *args, &block)
end
Run Code Online (Sandbox Code Playgroud)

1.month.value只是Fixnum 2592000,所以它实际上最终会调用2592000.respond_to? :parts,当然这是false.

但是,通过简单地向类中添加一个respond_to?方法,这很容易解决ActiveSupport::Duration:

main > ActiveSupport::Duration.class_eval do
           def respond_to?(name, include_private = false)
               [:value, :parts].include?(name) or
               value.respond_to?(name, include_private) or
               super
             end
         end
=> nil

main > 1.month.respond_to? :parts
=> true
Run Code Online (Sandbox Code Playgroud)

为什么的解释methods不正确省略:parts方法是一样的:因为methods消息只是被委托给价值,这当然也不能有一个parts方法.我们可以像添加自己的methods方法一样轻松修复此错误:

main > ActiveSupport::Duration.class_eval do
         def methods(*args)
           [:value, :parts] | super
         end
       end
=> nil

main >  1.month.methods.include? :parts
=> true
Run Code Online (Sandbox Code Playgroud)