Как всё должно работать? Если пользователь отправляет своё сообщение например на форуме в 12:15 (по времени на его устройстве), то и увидеть своё сообщение на сайте он должен с этим же временем. Казалось бы, что если использовать смещение часового пояса (взятое из браузера из getTimezoneOffset), то всё должно работать. Но это не так!
Парадокс в том, что часовой пояс может быть неправильно установлен на устройстве пользователя. И это весьма распространённый случай, так как при смене часового пояса многие просто переводят время на несколько часов не трогая часового пояса. И как следствие мы имеем бесконечные ветки на форумах техподдержки «Настройка времени», которые расцветают при каждом переводе часов зима/лето или в период возвращения из отпусков.
Самое правильное — выставлять местное время на сайте в соответствии с временем установленным на устройстве пользователя не обращая внимание на часовой пояс. Исходим из того, что на сервере все даты хранятся в базе данных в формате GMT (и это правильно). Таким образом для правильного вывода дат (времени) в браузере пользователя нужно вычислить смещение между временем браузера и сервера.
На сервере текущее время в php можно получить функцией time(), в браузере функцией javascript Date(). И тогда смещение часового пояса можно вычислить при помощи javacript так:
// вычисление time_zone в минутах в браузере
var d = new Date();
var loc = Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds());
var time_zone = ((<? echo time();?> - loc/1000)/60).toFixed(0);
Далее нужно передать значение time_zone на сервер и выводить время из БД с учётом этого смещения. Например если время хранится в БД в секундах, то для вывода в браузер используем следующее (на php):
// время для вывода в браузер - вычисляется на сервере
$t = $time_fromDB - $time_zone*60;
Выше изложен двух-этапный метод:
1. Вначале вычисляется time_zone
2. time_zone отправляется на сервер и используется при дальнейшем выводе времени
Но можно всё сделать и в один этап. В этом случае при первой загрузке страницы в браузер так же вычисляется time_zone (см. выше) и далее все значения времени выводятся при помощи javacript. Например при помощи такой функции mydate():
function two(num) { return ("0" + num).slice(-2);} // подставляет недостающий ноль
// t - время в секундах из БД сервера
// mydate возвращает строку в формате например 12.08.2015 19:03
function mydate(t) {
var d = new Date((t-time_zone*60)*1000);
return two(d.getUTCDate())+'.'+ two(d.getUTCMonth()+1)+'.'+d.getUTCFullYear()+' '+ two(d.getUTCHours())+':'+ two(d.getUTCMinutes());
}
Всё вышеизложенное успешно используется в нашем проекте месcенджера, где прекрасно себя зарекомендовало во всех браузерах и операционных системах (мобильных и настольных). Возможно неограниченное использование приведённого здесь алгоритма в ваших проектах. Приветствуется ссылка в исходных кодах на наш проект magdialog.ru.
Комментарии (28)
Artyushov
13.08.2015 12:56+5var time_zone = ((<? echo time();?> - loc/1000)/60).toFixed(0);
offset для временных зон не всегда кратен одному часуm08pvv
13.08.2015 13:54+2В работе со временем вообще много приколов.DeadMoroz
13.08.2015 13:22+1Исходим из того, что на сервере все даты хранятся в базе данных в формате GMT (и это правильно).
Это правильно только для дат в прошлом или ближайшем будущем. Для дат в отдалённом будущем, например, «17 июля 2018 г., 15:00 по Москве» хранить только дату в GMT недостаточно, т.к. теряется привязка к местности и неизвестно, в каком часовом поясе эта местность будет находиться через 3 года.ssp2
13.08.2015 13:38В случае расписаний поездов и самолётов (или чего-то подобного) подход используемый в статье неприменим. Там другая идеология, Вы правы.
CnapoB
13.08.2015 13:37Почему Вы использовали Date.UTC вместо getTime()?
Функция так же возвращается значение миллисекунд, прошедших с полуночи 01.01.1970 в UTC.
И для вывода в mydate я советовал бы посмотреть в сторону toLocaleFormat. Сайт может быть для нескольких стран, а следовательно, и время должно выдаваться в разных форматах, привычных жителям той или иной страны.ssp2
13.08.2015 14:54На вскидку:
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()) != d.getTime();
Сам удивился, что не смог функцию попроще найти в javascript. Может плохо искал. Но именно вышеприведённое выражение (полученное чуть ли не методом тыка) правильно работает.
el777
13.08.2015 13:43+20.
// t — время в секундах из БД сервера
То есть в базе хранится время в виде количества секунд? Не-не-не, оставьте этот колхоз для mysql 3.x.
Сейчас все эти костыли уже надо выкинуть.
1. Любая нормальная база нормально поддерживает время с часовыми поясами. Например, в PostgreSQL есть специальный тип данных timestamp with time zone, который хранит и само время и нужное смещение. И при необходимости корректно все пересчитает в нужный часовой пояс.
Для Mysql и других так же уже все изобретено.
2. Если вы уже посчитали на клиенте — зачем это передавать на сервер? Создавать нагрузку и тормоза? Прямо на клиенте исправьте даты и все.
Например, можно выводить даты так:<span class="date-fixable" data-time="Tue, 18 May 2015 15:35:00+0400">18 мая 15:35</span>
Где-нибудь вначале можно передать серверное время. На стороне клиента вычислили смещение и подправили даты в нужных спанах. Если у клиента JS не работает — то он видит стандартное время.Bozaro
13.08.2015 13:56В PostgreSQL для этих целей time with time zone.
В случае с timestamp он всегда хранит UTC, но при отображении либо учитывает (with time zone), либо не учитывает (without time zone) временную зону PsostgreSQL-клиента.
ssp2
13.08.2015 13:591. Идеологически хранить часовой пояс очень часто не нужно. И никаких костылей тут я не вижу.
2. Вроде именно этот вариант тоже приведён (функция mydate).el777
14.08.2015 12:00+11. Я тоже так раньше делал. Если задача «наго-кодить по-быстрому», то самое оно. Пока не сел и не потратил какое-то время, чтобы разобраться с нормальными инструментами.
Весь вопрос в том, видите вы какое-то развитие у системы вперед или нет. Если это конечная точка, то ок.
Время, которое мы используем, имеет свои законы — свои часовые пояса, свои лимиты, свои проблемы подсчета, свои невозможные даты и свой характер изменения всего этого — все это не так просто. Это не Медведеву часами щелкнуть, тут думать надо. Разработчики проделали большую работу, чтобы можно было легко и удобно использовать это в программах. Когда, вы все примитивизируете до int, вы теряете намного больше.
Вот уже привели пример, что будет с датами через несколько лет. В системе работа, которой планируется только на ближайший год, такого вопроса не стоит. Потом возникнет вопрос, как быть с датами выходящими за интервал int — меньше 1970 и больше 2038 годов. Как это починить? Костылем. То есть у вас в системе уже зарыт архитектурный костыль, который вылезет еще 100 раз.ssp2
14.08.2015 21:07-1Проекты разные бывают, для нашего этот алгоритм идеально подошёл. У нас — мессенджер мгновенных сообщений. Изложенный алгоритм не претендует на академизм и хорош именно своей простотой и понятностью. Мне вообще очень нравятся простые решения — «в одну строчку».
godlin
13.08.2015 16:21+1Где-нибудь вначале можно передать серверное время. На стороне клиента вычислили смещение и подправили даты в нужных спанах.
Согласен. Я тоже всегда так делаю :)
Bozaro
13.08.2015 13:44+2Тут есть еще ряд проблем:
Таким образом для правильного вывода дат (времени) в браузере пользователя нужно вычислить смещение между временем браузера и сервера.
Часы переводят в разных странах в разное время. Таким образом сдвиг будет разным для разного времени.
return two(d.getUTCDate())+'.'+ two(d.getUTCMonth()+1)+'.'+d.getUTCFullYear()+' '+ two(d.getUTCHours())+':'+ two(d.getUTCMinutes());
Можно получить «несуществующее» время для дат, которые были близко к переводу стрелок на час вперед (часы перевели в 02:00 на час вперед, и время 02:15 не будет корректным).
savostin
13.08.2015 13:56+2Пример с timestamp сообщения не очень удачный. Имхо, эту информацию сервер и так знает, ее не нужно отправлять от клиента и переводить куда-то. А вот если клиент вводит какое-то время, например, время события в будущем, то важно знать что он имел в виду, т.е. его смещение от GMT.
admax
13.08.2015 14:10+3А почему нельзя хранить в базе все даты в UTC? И на клиенте просто делать Date.UTC(dateFromServer), браузер сам приведёт к локальному времени.
ssp2
13.08.2015 14:21-1Например такое может быть. На Вашем устройстве отключена сихронизация времени (это часто используется). Вы отправили сообщение в чат в Москве, но часовой пояс на Вашем устройстве стоял для Вьетнама. Потом улетели в США. Опять поменяли время на местное, но часовой пояс опять неверный на устройстве остался. Какое время для отправленого сообщения Вы увидите?
admax
13.08.2015 14:28Когда я отправлял сообщение, в базе сохранилось серверное UTC-время. И при любых манипуляциях с настройками устройства и смене поясов я увижу то же самое время конвертированное к моей текущей локали. На моём телефоне сейчас 14:24 и, если я отправил сообщение час назад, то я увижу 13:24, независимо от того, с какого пояса я отправлял.
ssp2
13.08.2015 15:06Надо подумать… Отвечу вечером.
ssp2
13.08.2015 23:24Что-то у меня не получается сходу, то что Вы советуете. (про UTC на сервере и Date.UMC)
Приведите пожалуйста конкретный пример. Как получать время на сервере? И как выводить в браузер?ssp2
13.08.2015 23:38Если, будет работать Ваш вариант (по идее так всё и должно быть), то всё изложенное мною это изобретение велосипеда и надо будет переписать статью.
CnapoB
16.08.2015 14:45Конкретный пример не приведу, но опишу как реализую этот момент (отображение времени получения информации) сам с поправками на то, что описано в статье (неверно установленный часовой пояс, не настроенная синхронизация времени с ntp-сервером или провайдером, ручная установка неправильного времени на клиенте).
В первую очередь разделяем серверную и клиентскую части. Достаточно только того, что клиент знает, что время всех сообщений, пришедших с сервера, задано в UTC и знает точное серверное время (опять же в UTC) на момент загрузки скрипта:
var deltaUTC = ( new Date() ).getTime() - <?=microtime( true );?>;
Серверу же все равно, какое время у клиента, он все сообщения сохраняет в UTC (для php — это date_default_timezone_set('UTC') и date() (или time())).
Выдача сообщений происходит через обработчик на клиенте (JavaScript-интерфейс), где параметр времени преобразовывается в требуемое значение или с помощью prototype, приведу пример с обычной функцией:
var mydate = function ( date ) { var time = new Date ( date ); time.setTime ( time.getTime() + deltaUTC ); /* в качестве возврата будем использовать значение в привычном для пользователя формате */ return time.toLocaleDateString() + ' ' + time.toLocaleTimeString(); };
Передача сообщений в обработчик может происходить как посредством AJAX, так и в момент загрузки самой страницы (данные представляют собой JavaScript объект).
Главное помнить, что серверное время является эталоном, и само значение лучше не менять в процессе преобразований.
Такой подход решает проблему, когда в одном и том же чате общаются люди из разных часовых поясов, для каждого из них время относительно их самих. И даже если они переедут в другую часовую зону или перенастроят часы, время на сайте изменится, чтобы остаться относительным для клиента.
Таким образом решается проблема «двух-этапного метода» — на сервер нет необходимости передавать данные о клиентском времени.
Bozaro
13.08.2015 17:58+1У вас, похоже, смешано две проблемы:
- проблема часовых поясов;
- проблема не верно установленного времени на клиенте.
Для решения проблемы не верно установленного времени нужно:
- сохранение для всех отправленных/полученных сообщений только серверного (эталонного) времени, ибо клиенту доверять нельзя;
- вычисление разницы клиентского и эталонного времени перед отображением времени на клиенте;
Проблема обработки часовых поясов для отображения местного времени может решаться только на клиенте путем конвертации UTC (с учетом поправки неверно выставленного времени) в местное время. При этом клиентское приложение должно это делать с учетом системных настроек.
Выполнять эту операцию на сервере очень не тривиально хотя бы потому что:
- очень трудно выяснить, какая временная зона установлена на клиенте;
- практически нереально убедиться, что установленная на клиенте временная зона имеет актуальные параметры (куча андроидов, к примеру, до сих пор ничего не знает про изменения в зоне Europe/Moscow).
ssp2
13.08.2015 23:32Именно про пункты 1 и 2 (для не верно установленного времени) речь и идёт. То есть в моём алгоритме часовые пояса или зоны, как таковые не имеют значение. У нас это решение используется в мессенджере мгновенных сообщений.
zag2art
13.08.2015 19:48+2> Вот Вы знаете какой сейчас у нас часовой пояс +3 или +4 часа?
Кого у нас? В дефолтсити?
scrutari
Время устройства может быть UTC.
И вот еще полезная штука: moment-timezone