Peter Marklund

Peter Marklund's Home

Mon Nov 05 2007 15:50:24 GMT+0000 (Coordinated Universal Time)

Rails Gotcha: Date Arithmetic, Time Zones, and Daylight Savings

Timezones and daylight savings can cause us programmers a lot of headache. This is evidenced by the fact that probably the most popular post in this blog ever is the one about a timezone aware datetime picker. Today I spent several hours debugging a problem a client was having with an eternal loop caused by the daylight saving transition October 28/29 in conjunction with 1.day.since. One of the chapters in the Code Review PDF by Geoffrey Grosenbach is about keeping time in your Rails applications in UTC. After todays exercises I must say I could not agree more with Geoffrey. In fact, I would go as far as to say that date arithmetic in Rails 1.2 is not reliable unless you set your ENV['TC'] variable.

In Europe we have daylight savings time between the last sunday in March and the last sunday in October, so this year it was between March the 25th and October 28th. Here in Stockholm this means that in the summer we are UTC+2 and in the winter (i.e. most of the time...) UTC+1. We can see this in the Rails console:

>> ENV['TC']
=> nil
>> Time.parse("2007-03-25")
=> Sun Mar 25 00:00:00 +0100 2007
>> Time.parse("2007-03-25").dst?
=> false
>> Time.parse("2007-03-26")
=> Mon Mar 26 00:00:00 +0200 2007
>> Time.parse("2007-03-26").dst?
=> true
>> Time.parse("2007-10-28")
=> Sun Oct 28 00:00:00 +0200 2007
>> Time.parse("2007-10-28").dst?
=> true
>> Time.parse("2007-10-29")
=> Mon Oct 29 00:00:00 +0100 2007
>> Time.parse("2007-10-29").dst?
=> false

To see how the daylight savings transitions can be a problem. Suppose you have the date string "2007-10-28" (just before the transition) and you want to get the date string for the following day. In Rails 1.2.5 with Ruby 1.8.5 this will happen:

>> 1.day.since Time.parse("2007-10-28")
=> Sun Oct 28 23:00:00 +0100 2007

Notice how we are jumping to 23:00 the same day instead of 00:00 the next day. With Edge Rails (soon to be Rails 2.0), we get this instead:

1.day.since(Time.parse("2007-10-28"))
=> Mon Oct 29 00:00:00 +0100 2007
>> 1.day.since(Time.parse("2007-10-28")).strftime("%Y-%m-%d")
=> "2007-10-29"

In Rails 1.2.5, 1.day.since(date) is the same as doing (date+24*60*60). In Rails 2.0 though 1.day.since(date) will advance only the day part of the date and leave the hours alone, thus making sure the day is incremented even when crossing over a daylight savings transition.

To be sure to avoid any issues related to daylight savings, make sure to set this in your environment.rb:

  ActiveRecord::Base.default_timezone = :utc # Store all times in the db in UTC
  ENV['TZ'] = 'UTC' # Make Time.now return time in UTC

Then use tzinfo or some other library to adjust the time to/from the local time when you display it and retrieve it from the user.

Note: this post was updated the morning after I posted it because when I first wrote it I was apparently too tired to understand what was going on... :-)