Иван Бишевац
Иван Бишевац

Reputation: 14631

Ruby on Rails time zone strange behaviour

There is task model with attributes when and duration.

create_table "tasks", force: true do |t|
  ...
  t.datetime "when"
  t.integer  "duration"
  ...
end

I wrote method for checking if task is active so I can show it on page. This is active method:

  def active?
    if (self.when + self.duration) > Time.now
      true
    end
  end

I tried in console to inspect object:

t.when + t.duration
=> Sun, 08 Sep 2013 01:01:00 UTC +00:00
DateTime.now
=> Sun, 08 Sep 2013 01:57:13 +0200
t.active?
=> true

It's true but I entered 1:00 time and 1 minute for duration and I hoped it shouldn't be true.

It seems that when column in database is not saved in correct time zone, so it gives incorrect results. How to solve this issue?

Upvotes: 0

Views: 641

Answers (2)

7stud
7stud

Reputation: 48599

It seems that when column in database is not saved in correct time zone

1) Rails automatically converts times to UTC time before inserting them in the database (which is a good thing), which means the times have an offset of "+0000" . That means if you save a time of 8pm to the database, and your server is located in a timezone with an offset of "+0600", then the equivalent UTC time is 2pm, so 2pm gets saved in the database. In other words, your local server's time is 6 hours ahead of UTC time, which means that when it's 8pm in your server's time zone, it's 2pm in the UTC timezone.

2) When you compare dates, ruby takes the timezone offset into account--in other words ruby converts all times to the same timezone and then compares the times. Here is an example:

2.0.0p247 :086 > x = DateTime.strptime('28-01-2013 08:00:00 PM +6', '%d-%m-%Y %I:%M:%S %p %z')
 => Mon, 28 Jan 2013 20:00:00 +0600 
2.0.0p247 :087 > y = DateTime.strptime('28-01-2013 08:20:00 PM +7', '%d-%m-%Y %I:%M:%S %p %z')
 => Mon, 28 Jan 2013 20:20:00 +0700 
2.0.0p247 :088 > x < y
 => false 

If you just compare the times of the two Datetime objects, x is less than y. However, y has a time of 8:20pm in a timezone that has an offset of +7, which is equivalent to the time 7:20pm in a timezone with an offset of +6. Therefore, y is actually less than x. You need to compare apples to apples, which means you need to mentally compare times that have been converted to the same timezone to get the same results as ruby/rails produces.

3) You can convert Time.now to a UTC time using the rails utc() method:

2.0.0p247 :089 > x = Time.now
 => 2013-09-07 8:00:00 +0600 
2.0.0p247 :090 > x.utc
 => 2013-09-07 02:00:00 UTC 

That's what ruby does before comparing Time.now to task.when + task.duration

4) You might find it more convenient to create a DateTime object with the time you want using:

DateTime.strptime('28-01-2013 08:00:00 PM +0', '%d-%m-%Y %I:%M:%S %p %z'

Because you are able to specify the offset as zero, you don't have to create a time that anticipates the conversion to UTC time.

Or you can use the change() method, which causes the offset() to change without converting the time:

2.0.0p247 :011 > x = DateTime.now
 => Sun, 08 Sep 2013 00:34:08 +0600 
2.0.0p247 :012 > x.change offset: "+0000"
 => Sun, 08 Sep 2013 00:34:08 +0000 

Upvotes: 1

Jason N
Jason N

Reputation: 461

ActiveRecord stores timestamps in UTC by default. See How to change default timezone for Active Record in Rails? for changing default time zone.

You can also just use Time#in_time_zone to convert t.when to your timezone, see http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html.

Upvotes: 0

Related Questions