mad.ly rails, jquery, flash, etc    about »

We're looking for senior developer. More Info

Posted by
Geoff Buesing

Posted on
15 July 2008 @ 8am

Tagged
rails, time zones

Two fixes to ActiveSupport::TimeWithZone

Last night, I pulled in two fixes to the ActiveSupport::TimeWithZone class to the Rails 2-1-stable branch:

1. TimeWithZone unmarshals correctly now

Prior to this fix, if you marshaled and unmarshaled a TWZ, it would be off:

>> t = Time.zone.local(2000)
=> Sat, 01 Jan 2000 00:00:00 CST -06:00
>> mt = Marshal.dump(t)
=> "\004\bU: ActiveSupport::TimeWithZone…"
>> t2 = Marshal.load(mt)
=> Fri, 31 Dec 1999 18:00:00 CST -06:00

…this bug is pretty obvious on the command line, but the test suite wasn’t catching this, because the underlying wrapped Time instances were just being checked for equality, but not for zone.

In Ruby, two times are equal if they represent the same number of seconds since the epoch, even if they have different zones:

>> local, utc = Time.local(1999, 12, 31, 18), Time.utc(2000)
=> [Fri Dec 31 18:00:00 -0600 1999, Sat Jan 01 00:00:00 UTC 2000]
>> local.to_f
=> 946684800.0
>> local.zone
=> "CST"
>> utc.to_f
=> 946684800.0
>> utc.zone
=> "UTC"
>> local == utc
=> true

Ruby’s marshaling of Time instances unfortunately doesn’t respect the zone — it always returns the equivalent time in the local zone, even if the marshaled instance was in UTC:

>> t = Time.utc(2000)
=> Sat Jan 01 00:00:00 UTC 2000
>> mt = Marshal.dump(t)
=> "\004\bu:\tTime\r \000\031\200\000\000\000\000"
>> t2 = Marshal.load(mt)
=> Fri Dec 31 18:00:00 -0600 1999
>> t == t2
=> true
>> t.zone == t2.zone
=> false

…hence our problem with TimeWithZone: the wrapped UTC Time was unmarshaled as its localtime equivalent. This was easily fixed by calling #utc on the wrapped Times after unmarshaling, and we’re now checking for the correct representation in the test suite.

The now correct behavior:

>> t = Time.zone.local(2000)
=> Sat, 01 Jan 2000 00:00:00 CST -06:00
>> mt = Marshal.dump(t)
=> "\004\bU: ActiveSupport::TimeWithZone…"
>> t2 = Marshal.load(mt)
=> Sat, 01 Jan 2000 00:00:00 CST -06:00

View the changeset.

Thanks to the Lighthouse user “2 College Bums” for reporting this bug.

2. Advancing across DST boundary respects variable-length durations

Prior to this fix, when you advanced from a TWZ across a DST boundary with a variable-length duration (i.e., days, months or years), the time would be off by an hour. Example:

>> t = Time.zone.local(2008, 3, 9, 1) # 1 hour before DST
=> Sun, 09 Mar 2008 01:00:00 CST -06:00
>> t + 24.hours
=> Mon, 10 Mar 2008 02:00:00 CDT -05:00
>> t + 1.day
=> Mon, 10 Mar 2008 02:00:00 CDT -05:00

…the result of advancing 24.hours is correct: given that May 9 2008 is only 23 hours long, we end up at 2AM instead of 1AM on May 10. But when we advance 1.day, we don’t want this behavior — we always want the same time on the next day, regardless of the length of the day.

The now correct behavior:

>> t = Time.zone.local(2008, 3, 9, 1) # 1 hour before DST
=> Sun, 09 Mar 2008 01:00:00 CST -06:00
>> t + 24.hours
=> Mon, 10 Mar 2008 02:00:00 CDT -05:00
>> t + 1.day
=> Mon, 10 Mar 2008 01:00:00 CDT -05:00

Extensive tests were added to ensure that all advancing methods (e.g., +, -, advance, since, ago, months_since, months_ago, years_since, years_ago, etc.) behave correctly when crossing a DST boundary.

View the changeset.

No other outstanding bugs that I am aware of

…but please do file a bug report if you find anything.