Timezone vs Ruby && Rails

Подводные камни и true way :)

При использовании класса Time в ruby используется серверное время, как точка отсчета времени.

$ date
Sat Sep 21 16:49:56 CEST 2013
$ irb
2.0.0-p247 :001 > Time.now
=> 2013-09-21 16:49:59 +0200

Чем это может быть опасно?

Допустим вы разрабатываете программу, которая печатает время прилета самолетов. В вашем часовом поясе сейчас 17:00, а на компьютере установлено правильное время.

time = Time.new(2013, 09, 21, 16, 49, 59)

puts "#{time} - #{plane.name} now #{message}"

2.0.0-p247 :001 > plane_message (plane, "landed")
=> 2013-09-21 16:49:59 +0100 - Airbus 22 now landed

С этой программой все хорошо, пока мы исполняем ее у себя на сервере. Стоит нам ее вылить, например, на продакшен, который находится в другом часовом поясе или случайно не правильно настроен, как мы получим странный баг – время будет отличным от ожидаемого. И все потому, что ruby взяло за точку отсчета таймзону сервера.

Получить ожидаемый результат довольно просто – нужно просто передавать нужное смещение в конструктор:

time = Time.new(2013, 09, 21, 16, 49, 59, "+02:00")

Над полученным объектом можно выполнять различные преобразования и все они будут представлены в указанной таймзоне.

Согласитесь, что каждый раз помнить о таймзоне рано или поздно надоест! :)

Например, в фреймворке Ruby on Rails эта проблема решается конфигурируемой таймзоной.

#config/application.rb
config.time_zone = 'Europe/Moscow'

В фреймворке переопределен класс Time (rails Time) и умеет работать с нашей конфигурируемой таймзоной! Теперь, чтобы всегда использовать нужный часовой пояс, не нужно передавать смещение в конструктор класса Time. Просто вызывайте метод zone():

2.0.0-p247 :001 > Time.now()
=> 2013-09-21 17:42:33 +0200
2.0.0-p247 :002 > Time.zone.now()
=> Sat, 21 Sep 2013 19:42:38 MSK +04:00

Ну и если очень хочется сменить точку отсчета времени, например парсить время относительно Новосибирска, находясь в Московской зоне, можно либо вручную переписать таймзону (и не забыть ее вернуть обратно), либо использовать возможности rails:

Time.use_zone('Novosibirsk') do
  Time.zone.now
end

Что хотелось бы отметить – рельсовый ActiveRecord умеет работать с временными зонами и использует зону из application.rb. В базу данных, по умолчанию, сохраняется время в UTC, т.е. переводится в +0. Для этого используют метод to_s(:db). В большинстве случаев вызывать его нет необходимости, так как rails сами его вызовут.

Исходя из того, что рельсы сохраняют время в UTC, то и из базы они хотят получить время в UTC. А это значит, что хранить таймзону в бд нам не нужно.

UPD 1:
В качестве альтернативы к Time.zone.now можно использовать Time.current. Его результат – вызов Time.zone.now, но только при установленной таймзоне в конфигурационном файле (или напрямую через Time.zone), иначе возвращает Time.now. На мой взгляд, поведение этого метода в некоторой степени неявно – необходимо понимать, какой результат будет в конкретном случае использования.

Comments