0

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?

Иван Бишевац
  • 13,811
  • 21
  • 66
  • 93
  • How did you store "1 minute" in the `duration`? – mu is too short Sep 08 '13 at 00:32
  • I showed my migration related to those fields, it's stored in integer, so 60. – Иван Бишевац Sep 08 '13 at 10:29
  • And what happens when you add `60` to a `DateTime`? Just do a quick `DateTime.now + 60` in your console and see what `+ 60` does to the timestamp. – mu is too short Sep 08 '13 at 17:29
  • `[7] pry(main)> DateTime.now => Sun, 08 Sep 2013 19:52:51 +0200 [8] pry(main)> DateTime.now + 60 => Thu, 07 Nov 2013 19:52:59 +0200` But that's not related to my problem. Take a look: `[9] pry(main)> Task.last.when => Sun, 08 Sep 2013 16:58:00 UTC +00:00 [10] pry(main)> Task.last.when + Task.last.duration => Sun, 08 Sep 2013 16:59:00 UTC +00:00` It's because `when` column is `TimeWithZone`, take a look: `[11] pry(main)> Task.last.when.class => ActiveSupport::TimeWithZone ` – Иван Бишевац Sep 08 '13 at 17:55

2 Answers2

1

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 
7stud
  • 46,922
  • 14
  • 101
  • 127
  • What solution you recommend? I am wondering why it just doesn't work by default. I am wondering if I change default time zone when I migrate to another server (for example Heroku) if it will work. – Иван Бишевац Sep 08 '13 at 12:27
  • Everything works, it's just that you don't understand how things work. For instance, would you add 1/8 + 1/4 and get 2/8. No, you have to convert 1/8 and 1/4 to a common denominator before adding, so you do this 1/8 + 2/8 = 3/8. It's the same with times. rails converts all times to a common denominator, UTC, before comparing times. But you are not doing that mentally with your times. You have a task with, say, a start time of 8pm and an offset of "+0000" and you think that a time of 9pm with an offset of "+0200" should be larger than the 8pm time--but you are ignoring the offset. – 7stud Sep 08 '13 at 18:36
  • To correctly compare times, you have to take into account the offset, and rails does that. Is it really that difficult to write `Time.now.utc`? – 7stud Sep 08 '13 at 18:37
  • It's same again. Something that I don't understand is when I try to save 21:00 UTC+2 it should be saved in database like 19:00 UTC+0, but it's not saved like this. It's value is 21:00 UTC+0 in database. – Иван Бишевац Sep 08 '13 at 19:07
0

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.

Community
  • 1
  • 1
Jason N
  • 463
  • 3
  • 10