Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

(Time.now.utc.to_date + 1.month + 15.days) != (Time.now.utc.to_date + 15.days + 1.month)

How's this possible?

Time.now.utc.to_date + 1.month + 15.days #=> Mon, 01 Dec 2014
Time.now.utc.to_date + 15.days + 1.month #=> Sun, 30 Nov 2014

Has anyone seen it?

/edit

I guess I asked the question a wrong way. How do you guys explain this then?

Time.now.utc.to_date + (15.days + 1.month) #=> Mon, 08 Dec 2014
Time.now.utc.to_date + (1.month + 15.days) #=> Tue, 09 Dec 2014

(15.days + 1.month) #=> 3888000
(1.month + 15.days) #=> 3888000
like image 285
Marcin Urbanski Avatar asked Dec 02 '22 18:12

Marcin Urbanski


1 Answers

First let see Integer#month, it returns an instance of ActiveSupport::Duration. At the rails console:

~/rails/rfinan (1296000):1 > elapsed = 1.month
=> 2592000
~/rails/rfinan (1296000):1 > elapsed.value
=> 2592000
~/rails/rfinan (1296000):1 > elapsed.parts
=> [[:months,1]]
~/rails/rfinan (1296000):1 > elapsed.is_a? ActiveSupport::Duration
=> true

It's time for the method: ActiveSupport::Duration#+

~/rails/rfinan (1296000):1 > sum1 = 1.month + 15.days
=> 3888000
~/rails/rfinan (1296000):1 > sum2 = 15.days + 1.month
=> 3888000
~/rails/rfinan (1296000):1 > sum1.value
=> 3888000
~/rails/rfinan (1296000):1 > sum1.parts
=> [[:months,1],[:days,15]]
~/rails/rfinan (1296000):1 > sum2.value
=> 3888000
~/rails/rfinan (1296000):1 > sum2.parts
=> [[:days,15],[:months,1]]
~/rails/rfinan (1296000):1 > sum1 == sum2
=> true
~/rails/rfinan (1296000):1 > sum1.value == sum2.value
=> true
~/rails/rfinan (1296000):1 > sum1.parts == sum2.parts
=> false

Now Date#+, the ActiveSupport version.

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

That means: if I send :+ to a Date instance with a ActiveSupport::Duration instance as parameter, it calls ActiveSupport::Duration#since, and the last one calls ActiveSupport::Duration#sum, that injects over the date instance, and calls Date#advance on each of the parts of duration instance:

  def sum(sign, time = ::Time.current) #:nodoc:
    parts.inject(time) do |t,(type,number)|
      if t.acts_like?(:time) || t.acts_like?(:date)
        if type == :seconds
          t.since(sign * number)
        else
          t.advance(type => sign * number)
        end
      else
        raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
      end
    end
  end

Remmember sum1.parts != sum2.parts?, sum send advance to the date instance orderly. Let see what means Date#advance

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

When advance recive month: 1 it calls Date#>> from stdlib, that work diferently of ActiveSupport::Duration#+. At irb:

~ (main) > Date.new(2014,10,31) >> 1
=> #<Date: 2014-11-30 ((2456992j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 2
=> #<Date: 2014-12-31 ((2457023j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 3
=> #<Date: 2015-01-31 ((2457054j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 4
=> #<Date: 2015-02-28 ((2457082j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 5
=> #<Date: 2015-03-31 ((2457113j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 12
=> #<Date: 2015-10-31 ((2457327j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 1200
=> #<Date: 2114-10-31 ((2493486j,0s,0n),+0s,2299161j)>
~ (main) > Date.new(2014,10,31) >> 12000
=> #<Date: 3014-10-31 ((2822204j,0s,0n),+0s,2299161j)>

It's clear that Date#>> don't add days, add months and keep the day number. if the day isn't valid for the target month, it fixes it. Adding a fix number of months doesn't fixes the number of days added, because depend on the start date.

Now we can say that Date#+ is not the same of ActiveSupport::Duration#+, and we know why.

The anwer is Date#+ called with an ActiveSupport::Duration instance (say duration) doesn't care about duration.value, it uses duration.parts, which are different in each case.

like image 114
Alejandro Babio Avatar answered Dec 05 '22 09:12

Alejandro Babio