【问题标题】:How does ActiveSupport do month sums?ActiveSupport 如何计算月总和?
【发布时间】:2012-03-27 03:37:36
【问题描述】:

我很高兴也很惊讶地发现 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提供的Fixnum中的months方法没有给出线索:

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 in Fixnum...

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

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

ActiveSupport(或其他东西)如何/在哪里做聪明的月份数学而不是仅仅增加 30 天?

【问题讨论】:

    标签: ruby-on-rails ruby activesupport


    【解决方案1】:

    这是一个非常好的问题。简短的回答是 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 的对象层次结构的对象层次结构,代理对象如Delegator类,或其他必须避免来自 Ruby 方法和类的命名空间污染的用途。”

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

    额外问题:有谁知道为什么ActiveSupport::Duration 对象声称不响应:parts,即使它实际上确实,为什么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 方法,如下所示:

    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
    

    【讨论】:

      【解决方案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 版本一起使用时使用。

      【讨论】:

      • 不错!你知道在哪里/何时调用advance 吗?如果您能解释为什么我在遵循我的问题中的代码时没有找到这个会有所帮助。
      • 另外,您在哪里看到根据 ruby​​ 版本选择性使用的代码?
      • 查看链接文件顶部(第 10 行)的 Ruby 版本代码。
      • 好东西——我仍然没有看到Advance用于求和的地方,我只看到它在months_agoweeks_ago等中使用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-20
      • 2020-12-28
      • 1970-01-01
      相关资源
      最近更新 更多