outcoldman
outcoldman Denis Gladkikh

Разработка веб-приложений с поддержкой временных зон

ASP.NET, JavaScript, Localization, TSQL, and HTML

При разработке веб-приложений важно помнить о том, что клиентские компьютеры могут быть разбросаны достаточно сильно по земному шару. Даже если разрабатывать приложение только для русских пользователей, то наберется 11 временных зон. Меня очень сильно удивило и расстроило, что достаточно известные сайты http://habrahabr.ru и http://gotdotnet.ru об этом вообще не задумываются. На таких сайтах сделать поддержку временных зон еще проще, там есть профили пользователей, в которых пользователи запросто смогли бы выставлять временную зону в которой они находятся. Таким образом, например, реализована поддержка временных зон в Sharepoint, и таким образом реализовывают поддержку в большинстве enterprise приложений. А что же делать когда нет поддержки профайлов на сервере? Что, если это просто новостной сайт или блог, и хочется чтобы пользователи с любой точки земного шара видели время не в будущем, а публиковали комментарии видя свое текущее локальное время.

Задался я этим вопросом не просто так, а потому что решил сделать поддержку временных зон на своем сайте. Мой случай – это ASP.NET MVC приложение и MS SQL Server. Первое, с чего я начал: изучение заголовков запросов к серверу, в них никак не передается информация о том, какая временная зона у посетителя. Потому поиграть на сервере с региональными настройками и методами DateTime.ToLocalTime и DateTime.ToUniversalTime просто не получится для реализации поддержки временных зон. Хотя, конечно, последний нам пригодится. Если у вас уже приложение одно время крутилось с локальным серверным временем, то сначала просто меняете все даты на сервере, в базе данных на UTC (считай тоже самое, что время по Гринвичу) для этого зная серверные текущие настройки и какая временная зона стоит на сервере берем и пишем update для таблиц с датами, примерно, следующего содержания:

update dbo.MyTable set [Date] = dateadd(hour, -4, [Date])

У меня взято –4, так как на время исправления московское время было UTC+4 (летнее время). Я не стал делать поддержку перехода с летнего времени на зимнее и обратно, так как тут больше труда чем пользы. Дальше в коде, везде где вы берете текущее время необходимо будет заменить getdate() на getutcdate() – это в скриптах TSQL, а на сервере к конструкции DateTime.Now (или Today) добавить ToUniversalTime(), то есть получится DateTime.Now.ToUniversalTime(), таким вот чудесным образом на сервере мы будем оперировать с датами только в зоне UTC.

Дальше дело в клиенте, необходимо на клиенте отображать его правильное время. Получить количество часов, которые отделяют пользователя от UTC можно при помощи Javascript функции у объекта типа даты getTimezoneOffset(), она возвращает разницу в минутах. На сервере я стал рендерить все даты в формате “dd.MM.yyyy HH:mm” – этого мне было более чем достаточно (точнее нигде других форматов не требовалось). Все даты поместил в элементы с css классом .utcdate, даты которые не были выделены в отдельные заголовки (h1,h2,h3…) поместил в span и т.д., то есть на сервере у меня код рендерился, примерно, так:

<h3 class="utcdate"><%= Model.BlogPost.Date.ToString("dd.MM.yyyy HH:mm")%></h3>

А css класс определил так:

.utcdate {display: none; }

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

Дальше пишем Javascript:

function utcToLocal(u) {
    var l = new Date(u.substring(6, 10), u.substring(3, 5)-1, u.substring(0, 2), u.substring(11, 13), u.substring(14, 16));
    var d = new Date(l.getTime() + (-l.getTimezoneOffset() * 60 * 1000));
    return wZ(d.getDate()) + '.' + wZ(d.getMonth()+1) + '.' + d.getFullYear() + ' ' + wZ(d.getHours()) + ':' + wZ(d.getMinutes());
}function wZ(x) { if (x <;= 9) return '0' + x; return x; }
function doCurrentDate() {
    $(".utcdate").each(function () {
        if ($(this).css("display") != "inline") {
            this.innerHTML = utcToLocal(this.innerHTML);
            $(this).css("display", "inline");
        }
    });
}$(document).ready(function () {
    doCurrentDate();
});

Признаюсь, что я не профессионал в Javascript, потому может найдутся те, кто меня поправят и скажут как лучше сделать. Здесь используется jQuery, переписать данные функции без использования его будет посложнее, так как тут все компактно при помощи его селекторов. Итак, что же делает это скрипт? Последние 3 строки на состояние загрузки всего документа вызывают функцию doCurrentDate(), которая находит все элементы с указанным классом utcdate и для каждого выполняет: если у стиля не поменяно отображение с none на inline, то при помощи функции utcToLocal меняем дату и выставляем свойству стиля display значение inline.

Функция utcToLocal достаточно проста, она распарсивает входящую строку на отдельные составляющие даты, создает новый объект типа даты, затем создает новый объект типа даты из предыдущего с добавлением необходимого количества времени (getTime возвращает миллисекунды, потому умножаем значение getTimezoneOffset на 60 секунд в минуте и 1000 миллисекунд в секунде), ну а дальше просто форматируем выходное значение – дату, где я добавил еще одну функцию wZ, которая добавляет 0 впереди почти всех составляющих даты, чтобы отображалось всегда 01.01.2010 вместо 1.1.2010.

Так как в методе doCurrentDate есть проверка на то, чтобы два раза не привести дату в нужное состояние (inline еще не выставили), потому у меня есть возможность после обработки или добавления кусков html при помощи AJAX вызывать функцию doCurrentDate еще раз. Так я, например, делаю после добавление комментария.

Конечно, получается плохо, что мы не учитываем  формат вывода даты, ведь в том же US дату выводят в формате MM.dd.yyyy, и разделители там другие, сделать это можно при помощи функции toLocaleString(), но она выводит не совсем мне желаемый формат, включающий и день недели и секунды, в общем кучу лишнего, вырезать это не просто, потому как во многих форматах все может быть по разному расположено. Как вариант можно, конечно, опять же отправлять информацию на сервер и как то строить правила там, как нужно форматировать даты, но не думаю что это того стоит.

Кстати, было бы, по-моему, очень удобно, если в формат HTML 5 добавили бы поддержку преобразования дат. Вроде, пишешь такой html код:

<date>03.04.2010T00:38:00+04:00</date>

А браузер преобразовывает в локальное время. А так же как-нибудь иметь возможность форматировать даты, какой-нибудь атрибут format, который имеет несколько заготовленных форматов. Или может быть такое уже есть?

Have feedback or questions? Looking for consultation?

My expertise: MongoDB, ElasticSearch, Splunk, and other databases. Docker, Kubernetes. Logging, Metrics. Performance, memory leaks.

Send me an email to public@denis.gladkikh.email.

The content on this site represents my own personal opinions and thoughts at the time of posting.

Content licensed under the Creative Commons CC BY 4.0.