我很高兴并且惊讶地发现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天?
答案 0 :(得分:5)
这是一个非常好的问题。简短的回答是1.month
是一个ActiveSupport::Duration
对象(如您所见),其身份以两种不同的方式定义:
30.days
(如果您需要/尝试将其转换为秒数),和 通过检查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版本一起使用时才会使用此代码。