Peter Marklund

Peter Marklund's Home

Mon November 05, 2007
Programming

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... :-)

Comments

dln said over 6 years ago:

Indeed, however, note that:

>> t1=Time.parse("2007-10-10")
=> Wed Oct 10 00:00:00 0200 2007
>> t2=Time.parse("2007-11-10")
=> Sat Nov 10 00:00:00 0100 2007
>> t1.dst?
=> true
>> t2.dst?
=> false

--------------------------------------------------------------------------------

Peter Marklund said over 6 years ago:

dln: you are absolutely right about the daylight savings transition. I realized that myself and updated the post. Also, thanks for pointing out to the dst method.

--------------------------------------------------------------------------------

unknown said over 6 years ago:

hello! I'm new to Rails and I'm wondering how to to do some time arithmetic.. I want to subtract two time formats and get their duration. Thanks!

--------------------------------------------------------------------------------