с обработкой времени: • Пост, опубликованный в час ночи, уехал в календаре на вчера? • Времена в приложении скачут на три часа туда и обратно? • Пользователи яростно хотят видеть всё в своём времени и негодуют, что всё по Москве?
Олсона • Идентификатор часового пояса — строка вида Регион/Место – Мы сейчас в Europe/Moscow • Хранит всю историю о часовых поясах с 1 января 1970 • Используется в *nix системах, большинстве СУБД и языков • Стандарт де-факто, если вы не на Windows :-)
tzinfo) • Предоставляет методы для разбора времени в контексте данного часового пояса • Использует свои имена для обозначения часовых поясов (Moscow вместо Europe/Moscow, но есть маппинг) • Набор часовых поясов по умолчанию… странный time_zone = ActiveSupport::TimeZone['Novosibirsk']
данном часовом поясе – time_zone.now # => Fri, 19 Jun 2015 15:50:00 NOVT +06:00 • #parse(string) парсит время и переводит его в этот часовой пояс, умеет учитывает летнее время и смещение от UTC – time_zone.parse("2015-06-19T12:50:00") # => Fri, 19 Jun 2015 12:50:00 NOVT +06:00 • #at переводит Unix timestamp во время в часовом поясе • #local(*args) позволяет составить время из компонентов • И многие другие!
часового пояса который сейчас используется для обработки запроса (используется глобально) • Time.zone_default — часовой пояс из config/application.rb • Time.with_zone(&block) меняет Time.zone внутри блока. • Time.current и Date.current работают в часовом поясе приложения, а не ОС, в отличие от Time.now и Date.today • Time#in_time_zone(tz) переводит время в заданный часовой пояс (принимает как объект, так и идентификаторы Time.parse('2015-06-19T12:50:00').in_time_zone('Asia/Tokyo') # => Fri, 19 Jun 2015 18:50:00 JST +09:00
для работы с # часовым поясом, хранящимся в БД как идентификатор TZ database. def time_zone unless @time_zone tz_id = read_attribute(:time_zone) as_name = ActiveSupport::TimeZone::MAPPING.select do |_,v| v == tz_id end.sort_by do |k,v| v.ends_with?(k) ? 0 : 1 end.first.try(:first) value = as_name || tz_id @time_zone = value && ActiveSupport::TimeZone[value] end @time_zone end
из tzdata «из коробки»: SELECT '2015-06-19T12:13:14Z' AT TIME ZONE 'Europe/Moscow'; • Может интерпретировать время как в UTC либо как локальное. • Типы timestamp и прочие хранят данные без обработки. • Те же типы, но с «with time zone» в названии не хранят часовой пояс, а только время в UTC и автоматически его конвертируют! • Резюме: в целом неплохо, но есть подводные камни.
но из коробки данных может и не быть: SELECT CONVERT_TZ('2015-06-19 12:13:14', 'UTC', 'Europe/Moscow'); • Тип datetime хранит время как есть, никак не обрабатывает. • Тип timestamp автоматически конвертирует значение в UTC для хранения и обратно в локальное время для отображения • Резюме: примерно так же — жить можно.
к СУБД устанавливает часовой пояс в UTC, и все времена хранятся тоже в UTC, поэтому запрос: News.where('published_at >= ? AND published_at <= ?', Date.today, Date.tomorrow) не вернёт записи за первые три часа суток (UTC+3, все дела) • Необходимо прямо указать момент времени в нужном часовом поясе, чтобы ActiveRecord его правильно сконвертировал: News.where('published_at >= ? AND published_at <= ?', Time.now.beginning_of_day, Time.now.end_of_day)
UTC и ничего более. • Всё остальное — только внешними библиотеками. • Пока что лучше всех Moment Timezone (включает в себя tzdata) – moment("2015-06-19T10:05:00Z").tz('Europe/Moscow') – moment.parseZone("2015-06-19T13:05:00+03:00") • Для больших фреймворков, смотрите библиотеки, обеспечивающие эту всю красоту, такие как angular-moment
new Date(); и результат отправить в Rails: Time.parse('Mon May 18 2015 22:16:38 GMT+0600 (NOVT)') вернёт 2015-11-01 22:16:38 +0600 • Решение — использовать формат по стандарту ISO8601: Time.iso8601('2015-05-18T22:16:38+06:00') выдаст ожидаемое 2015-05-18 22:16:38 +0600 • Разрабатывая в Москве вы никогда не обнаружите этот баг! • Установите на CI-сервере другой часовой пояс (например, UTC) • А лучше используйте в тестах специальные гемы типа timecop • https://bugs.ruby-lang.org/issues/11261
в UTC (он же RFC 3339): '2015-05-18T22:16:38Z' • Если нужно именно локальное время, смещение обязательно: '2015-05-19T01:16:38+03:00' • На клиенте, если у вас moment.js, то вам нужен метод toISOString() • Angular.js сериализует в ISO 8601 по умолчанию. • Соответственно нужно парсить на стороне сервера: Time.iso8601(params[:from]) rescue Time.parse(params[:from]) (но я бы лучше вернул код ошибки 400, ей богу)
передаётся обычно только в UTC! (ISO8601 или Unix Timestamp) • Для дат в будущем нужно думать. Серебряной пули нет. • Что бы одно вы ни хранили — что-нибудь обязательно «съедет» • В идеале нужно сохранять всё: [UTC, локальное время, часовой пояс] • Если есть информация о часовом поясе — сохраняется его id из tzdata • Если нет, а локальное время хранить нужно — сохраняете смещение. • По возможности времени с клиента не верьте, всё делайте на сервере. • Всегда держите на сервере настроенный NTP и самую последнюю версию гема tzinfo-data или системного пакета tzdata
http://habrahabr.ru/company/mailru/blog/242645/ • The Problem with Time & Timezones https://youtu.be/-5wpm-gesOY • Документация! http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.h tml http://api.rubyonrails.org/classes/Time.html