2012-03-27 32 views
2

我很高興和驚訝地發現,ActiveSupport以我想要的方式進行月份總計。無論有問題的月份中有多少天,將1.month添加到特定的Time將會使您處於與Time相同的日期。ActiveSupport如何進行月份總和?

> 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 

months方法Fixnum通過的ActiveSupport提供不提供線索:

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 

...使我們sinceFixnum ...

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

......這導致我們無處可去。

ActiveSupport(或別的什麼)在哪裏做聰明的月數學,而不是隻增加30天?

回答

4

這是一個非常好的問題。簡短的回答是,1.monthActiveSupport::Duration對象(因爲你已經看到的),其身份是兩種不同的方式定義:

  • 30.days(如果你需要/嘗試將其轉換成秒數) ,
  • 爲1個月(如果您嘗試將此持續時間添加到日期)。

你可以看到,它仍然知道它是相當於1個月通過檢查其parts方法:

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的對象,但它很難得到它承認它是!

當您在一個控制檯(我用撬)檢查它,它看起來酷似(並聲稱)正常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 

您也可以通過檢查告訴它除了定期對Fixnums如果響應:parts

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到秒或幾天的「等效」號:

​​

如何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即使它實際上確實,爲什麼部分方法未在方法列表中列出?

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方法一樣容易地修復這個bug:

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

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

貌似神奇的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用這個代碼僅使用。

+0

不錯!你知道'advance'被調用的地方/時間嗎?如果你能解釋爲什麼我沒有找到這個,而遵循我的問題,這將是有益的代碼。 – 2012-03-27 18:42:01

+0

也,你在哪裏看到的代碼有選擇地使用基於Ruby版本? – 2012-03-27 18:44:46

+1

查看Ruby版本代碼的鏈接文件(第10行)的頂部。 – Jon 2012-03-27 21:25:31

相關問題