Javascript и часовые пояса — правильное время на сайте

До сих пор существует путаница при реализации местного времени на сайте. Изрядный вклад в эту путаницу внесли российские законодатели с периодической отменой перехода на зимнее время. Вот Вы знаете какой сейчас у нас часовой пояс +3 или +4 часа? Вот и большинство пользователей этого не знают. Но есть очень простое решение как не отягощать пользователя этой проблемой! Нужно использовать время устройства (исходим из того, что это корректное местное время). Очевидное решение использовать функцию javascript getTimezoneOffset является в корне неправильным. Почему? Читайте дальше.

Как всё должно работать? Если пользователь отправляет своё сообщение например на форуме в 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.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 28

  • UFO just landed and posted this here
      +5
      var time_zone = ((<? echo time();?> - loc/1000)/60).toFixed(0);

      offset для временных зон не всегда кратен одному часу
        +3
        В примере time_zone в минутах считается.
          +1
          И правда, виноват.
          +2
          В работе со временем вообще много приколов.

          +1
          Исходим из того, что на сервере все даты хранятся в базе данных в формате GMT (и это правильно).

          Это правильно только для дат в прошлом или ближайшем будущем. Для дат в отдалённом будущем, например, «17 июля 2018 г., 15:00 по Москве» хранить только дату в GMT недостаточно, т.к. теряется привязка к местности и неизвестно, в каком часовом поясе эта местность будет находиться через 3 года.
            0
            В случае расписаний поездов и самолётов (или чего-то подобного) подход используемый в статье неприменим. Там другая идеология, Вы правы.
            0
            Почему Вы использовали Date.UTC вместо getTime()?
            Функция так же возвращается значение миллисекунд, прошедших с полуночи 01.01.1970 в UTC.
            И для вывода в mydate я советовал бы посмотреть в сторону toLocaleFormat. Сайт может быть для нескольких стран, а следовательно, и время должно выдаваться в разных форматах, привычных жителям той или иной страны.
              0
              На вскидку:
              Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()) != d.getTime();

              Сам удивился, что не смог функцию попроще найти в javascript. Может плохо искал. Но именно вышеприведённое выражение (полученное чуть ли не методом тыка) правильно работает.
              +2
              0.
              // 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 не работает — то он видит стандартное время.
                0
                В PostgreSQL для этих целей time with time zone.
                В случае с timestamp он всегда хранит UTC, но при отображении либо учитывает (with time zone), либо не учитывает (without time zone) временную зону PsostgreSQL-клиента.
                  0
                  1. Идеологически хранить часовой пояс очень часто не нужно. И никаких костылей тут я не вижу.
                  2. Вроде именно этот вариант тоже приведён (функция mydate).
                    +1
                    1. Я тоже так раньше делал. Если задача «наго-кодить по-быстрому», то самое оно. Пока не сел и не потратил какое-то время, чтобы разобраться с нормальными инструментами.

                    Весь вопрос в том, видите вы какое-то развитие у системы вперед или нет. Если это конечная точка, то ок.
                    Время, которое мы используем, имеет свои законы — свои часовые пояса, свои лимиты, свои проблемы подсчета, свои невозможные даты и свой характер изменения всего этого — все это не так просто. Это не Медведеву часами щелкнуть, тут думать надо. Разработчики проделали большую работу, чтобы можно было легко и удобно использовать это в программах. Когда, вы все примитивизируете до int, вы теряете намного больше.

                    Вот уже привели пример, что будет с датами через несколько лет. В системе работа, которой планируется только на ближайший год, такого вопроса не стоит. Потом возникнет вопрос, как быть с датами выходящими за интервал int — меньше 1970 и больше 2038 годов. Как это починить? Костылем. То есть у вас в системе уже зарыт архитектурный костыль, который вылезет еще 100 раз.
                      –1
                      Проекты разные бывают, для нашего этот алгоритм идеально подошёл. У нас — мессенджер мгновенных сообщений. Изложенный алгоритм не претендует на академизм и хорош именно своей простотой и понятностью. Мне вообще очень нравятся простые решения — «в одну строчку».
                    +1
                    Где-нибудь вначале можно передать серверное время. На стороне клиента вычислили смещение и подправили даты в нужных спанах.

                    Согласен. Я тоже всегда так делаю :)
                    +2
                    Тут есть еще ряд проблем:

                    Таким образом для правильного вывода дат (времени) в браузере пользователя нужно вычислить смещение между временем браузера и сервера.

                    Часы переводят в разных странах в разное время. Таким образом сдвиг будет разным для разного времени.

                    return two(d.getUTCDate())+'.'+ two(d.getUTCMonth()+1)+'.'+d.getUTCFullYear()+' '+ two(d.getUTCHours())+':'+ two(d.getUTCMinutes());

                    Можно получить «несуществующее» время для дат, которые были близко к переводу стрелок на час вперед (часы перевели в 02:00 на час вперед, и время 02:15 не будет корректным).
                      +2
                      Пример с timestamp сообщения не очень удачный. Имхо, эту информацию сервер и так знает, ее не нужно отправлять от клиента и переводить куда-то. А вот если клиент вводит какое-то время, например, время события в будущем, то важно знать что он имел в виду, т.е. его смещение от GMT.
                        +3
                        А почему нельзя хранить в базе все даты в UTC? И на клиенте просто делать Date.UTC(dateFromServer), браузер сам приведёт к локальному времени.
                          –1
                          Например такое может быть. На Вашем устройстве отключена сихронизация времени (это часто используется). Вы отправили сообщение в чат в Москве, но часовой пояс на Вашем устройстве стоял для Вьетнама. Потом улетели в США. Опять поменяли время на местное, но часовой пояс опять неверный на устройстве остался. Какое время для отправленого сообщения Вы увидите?
                            0
                            Когда я отправлял сообщение, в базе сохранилось серверное UTC-время. И при любых манипуляциях с настройками устройства и смене поясов я увижу то же самое время конвертированное к моей текущей локали. На моём телефоне сейчас 14:24 и, если я отправил сообщение час назад, то я увижу 13:24, независимо от того, с какого пояса я отправлял.
                              0
                              Надо подумать… Отвечу вечером.
                                0
                                Что-то у меня не получается сходу, то что Вы советуете. (про UTC на сервере и Date.UMC)
                                Приведите пожалуйста конкретный пример. Как получать время на сервере? И как выводить в браузер?
                                  0
                                  Если, будет работать Ваш вариант (по идее так всё и должно быть), то всё изложенное мною это изобретение велосипеда и надо будет переписать статью.
                                    0
                                    Конкретный пример не приведу, но опишу как реализую этот момент (отображение времени получения информации) сам с поправками на то, что описано в статье (неверно установленный часовой пояс, не настроенная синхронизация времени с 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 объект).

                                    Главное помнить, что серверное время является эталоном, и само значение лучше не менять в процессе преобразований.

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

                                    Таким образом решается проблема «двух-этапного метода» — на сервер нет необходимости передавать данные о клиентском времени.
                                +1
                                У вас, похоже, смешано две проблемы:
                                • проблема часовых поясов;
                                • проблема не верно установленного времени на клиенте.


                                Для решения проблемы не верно установленного времени нужно:
                                1. сохранение для всех отправленных/полученных сообщений только серверного (эталонного) времени, ибо клиенту доверять нельзя;
                                2. вычисление разницы клиентского и эталонного времени перед отображением времени на клиенте;


                                Проблема обработки часовых поясов для отображения местного времени может решаться только на клиенте путем конвертации UTC (с учетом поправки неверно выставленного времени) в местное время. При этом клиентское приложение должно это делать с учетом системных настроек.

                                Выполнять эту операцию на сервере очень не тривиально хотя бы потому что:
                                1. очень трудно выяснить, какая временная зона установлена на клиенте;
                                2. практически нереально убедиться, что установленная на клиенте временная зона имеет актуальные параметры (куча андроидов, к примеру, до сих пор ничего не знает про изменения в зоне Europe/Moscow).
                                  0
                                  Именно про пункты 1 и 2 (для не верно установленного времени) речь и идёт. То есть в моём алгоритме часовые пояса или зоны, как таковые не имеют значение. У нас это решение используется в мессенджере мгновенных сообщений.
                              +2
                              > Вот Вы знаете какой сейчас у нас часовой пояс +3 или +4 часа?
                              Кого у нас? В дефолтсити?
                                0
                                JavaScript поддерживает стандарт rfc822 / ISO 8601. Правда встречаются баги в разных браузерах при некоторых способах записи (codepen).

                                Only users with full accounts can post comments. Log in, please.