Как ActiveSupport делает месячные суммы? - PullRequest
3 голосов
/ 27 марта 2012

Я был рад и удивлен, обнаружив, что 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

метод 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

... ведет нас к since в Fixnum ...

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

... что ведет нас в никуда.

Как / где ActiveSupport (или что-то еще) делает умную месячную математику вместо простого добавления 30 дней?

Ответы [ 2 ]

5 голосов
/ 02 августа 2013

Это действительно хороший вопрос.Короткий ответ: 1.month является ActiveSupport::Duration объектом (как вы уже видели), и его идентичность определяется двумя различными способами:

  • как 30.days (на случай, если вам нужно / попробоватьчтобы преобразовать его в число секунд), * ​​1007 * и
  • как 1 месяц (в случае, если вы попытаетесь добавить эту продолжительность к дате).

Youможно увидеть, что он все еще знает, что он эквивалентен 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 объект, но очень трудно получить его признать , что это так!

Когда вы проверяете его в консоли (Я использую 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

Вы также можете отличить его от обычных Fixnum, проверив, отвечает ли он на :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 в его «эквивалентное» количество секунд или дней:

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?(Подробности реализации Gory)

ActiveSupport::Duration фактически определено (в 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

Ответ : поскольку 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 голосов
/ 27 марта 2012

Похоже, что волшебство происходит в ActiveSupport core_ext / date / computing.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.

...