ActiveSupport如何处理月份金额?

时间:2012-03-27 03:37:36

标签: ruby-on-rails ruby 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

activesupport提供的months中的Fixnum方法无法提供线索:

def months
  ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
end

按照+中的Time方法...

def plus_with_duration(other) #:nodoc:
  if ActiveSupport::Duration === other
    other.since(self)
  else
    plus_without_duration(other)
  end
end

...将我们引导至since中的Fixnum ...

def since(time = ::Time.current)
  time + self
end

......这使我们无处可去。

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

2 个答案:

答案 0 :(得分:5)

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

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

通过检查parts方法,您可以看到它仍然相当于1个月:

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

一旦你看到它仍然知道它正好是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

它甚至声称等同于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

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

main > one_month.is_a? ActiveSupport::Duration
=> true
另一方面,单纯的Fixnum对象必须垂头丧气并承认它们不是:

main > 2592000.is_a? ActiveSupport::Duration
=> false

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

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

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

拥有一系列零件非常棒

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

main > (one_month + 5.days).parts
=> [[:months, 1], [:days, 5]]

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

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

...如果只是 秒,它 能够正确计算作为其价值。如果我们首先将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

ActiveSupport::Duration如何运作? (血腥实施细节)

ActiveSupport::Duration实际定义为(在gems/activesupport-3.2.13/lib/active_support/duration.rb中)作为BasicObject的子类,根据docs,“可以用于创建独立于Ruby的对象层次结构对象层次结构,委托代理类之类的代理对象,或者必须避免使用Ruby方法和类的名称空间污染的其他用途。“

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

加分问题:有人知道为什么ActiveSupport::Duration对象声明不响应:parts,即使它实际 < / em>,以及为什么parts方法没有列在方法列表中?

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

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

main > 1.month.methods.include? :since
=> true

回答:由于BasicObject未定义respond_to?方法,因此向respond_to?对象发送ActiveSupport::Duration将最终调用其method_missing方法1}}方法,如下所示:

def method_missing(method, *args, &block) #:nodoc:
  value.send(method, *args, &block)
end

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

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

答案 1 :(得分:2)

在ActiveSupport的core_ext/date/calculations.rb中看起来很神奇:

def advance(options)
  options = options.dup
  d = self
  d = d >> options.delete(:years) * 12 if options[:years]
  d = d >> options.delete(:months)     if options[:months]
  d = d +  options.delete(:weeks) * 7  if options[:weeks]
  d = d +  options.delete(:days)       if options[:days]
  d
end

def >>(n)
  y, m = (year * 12 + (mon - 1) + n).divmod(12)
  m,   = (m + 1)                    .divmod(1)
  d = mday
  until jd2 = self.class.valid_civil?(y, m, d, start)
    d -= 1
    raise ArgumentError, 'invalid date' unless d > 0
  end
  self + (jd2 - jd)
end

看起来Ruby 1.9+处理这个问题,所以只有当Rails与旧的Ruby版本一起使用时才会使用此代码。