понедельник, 14 июня 2010 г.

Управление временными зонами в веб-приложениях

Задача заключается в том, что каждый пользователь может работать в своей собственной временной зоне, более того, эта зона может изменяться. Широко распространенным вариантом решения проблемы является установка временной зоны непосредственно в настройках каждого пользователя. Минусы этого решения очевидны - эту настройку большинство пользователей никогда не найдет и не выставит в нужное значение, а при перемещении в другой город/страну и тем более никто об этом не вспомнит и не сменит свою временную зону. Другой вариант - использовать настройки ОС. Но увы, в стандартных HTTP-заголовках параметры временной зоны не передаются. Получить временную зону из яваскрипта напрямую тоже не удастся - можно узнать лишь временное смещение, которым могут соответствовать много зон. Что же делать? "Остается одно - только лечь помереть" (с). Или использовать смещение для локального времени, полученное на яваскрипт, в пределах одной сессии, а на стороне сервера применять соответствующее смещение. Например, для Москвы мы можем получить часовой пояс MSK или MSD (зимнее и летнее московское время), которым соответствуют смещения 3 или 4 часа. Чтобы было еще смешнее, в 1921-м году московское летнее время (MSD) имело смещение 5 часов от Гринвича, а в 2010-м году изменены часовые пояса для части субъектов РФ. Так что необходимо еще и текущую дату учитывать.

Код на яваскрипт тривиален:
getZone = function() {
tz = new Date().toString().replace(/^.*\(|\)$/g, "");
return tz;
}
alert(getZone());
Записать текущее значение часового пояса в кукисы также не представляет сложности. А вот на сервере все интереснее - нам нужна таблица всех временных зон и правил получения временного смещения для них. В линуксе есть пакет tzdata с нужной нам информацией, из которого несложно извлечь нужную информацию. В том числе, в тикле 8.5 есть соответствующие утилиты для преобразования в текстовые файлы. Подробнее см. здесь: Sources for Time Zone and Daylight Saving Time Data
Для OpenACS нашелся вот такой скрипт: http://pedroliska.com/files/load-timezones.tcl.txt Чтобы не лезть в код самого скрипта, запускаем его через враппер:
#!/usr/bin/tclsh8.5

proc ad_page_contract {args} {}

proc acs_root_dir {} {
  exec mkdir -p ./packages/ref-timezones/sql/common
  return "."
}
proc ReturnHeaders {args} {}
proc ns_write {line} {
  puts -nonewline $line
}
proc with_catch {errmsg if else} {
  uplevel 1 $if
}

proc ad_decode { args } {
#    this procedure is analogus to sql decode procedure. first parameter is
#    the value we want to decode. this parameter is followed by a list of
#    pairs where first element in the pair is convert from value and second
#    element is convert to value. last value is default value, which will
#    be returned in the case convert from values matches the given value to
#    be decoded
    set num_args [llength $args]
    set input_value [lindex $args 0]

    set counter 1

    while { $counter < [expr $num_args - 2] } {
 lappend from_list [lindex $args $counter]
 incr counter
 lappend to_list [lindex $args $counter]
 incr counter
    }

    set default_value [lindex $args $counter]

    if { $counter < 2 } {
 return $default_value
    }

    set index [lsearch -exact $from_list $input_value]
    
    if { $index < 0 } {
 return $default_value
    } else {
 return [lindex $to_list $index]
    }
}

source load-timezones.tcl.txt
В результате получаем два файла с нужной нам информацией: timezones.dat и timezone-rules.dat В файлике timezones.dat содержатся наименования временных зон - из яваскрипта мы эту информацию получить не можем, разве что предложим пользователю список выбора для ручного указания зоны, так что нам этот файл в настоящий момент не интересен (притом, если мы знаем наименование временной зоны, достаточно назвать ее тиклю или постгресу для пересчета, так что временное смещение уже не требуется, все совершенно тривиально). А вот timezone-rules.dat предоставляет все правила пересчета, с ним нам и придется работать. Как пример:
$ grep 2010 timezone-rules.dat |grep MSD
#tz_id, abbrev, utc_start, utc_end, local_start, local_end, gmt_offset, isdst
391,MSD,Sat Mar 27 23:00:00 2010,Sat Oct 30 22:59:59 2010,Sun Mar 28 03:00:00 2010,Sun Oct 31 02:59:59 2010,14400,t
428,MSD,Sat Mar 27 23:00:00 2010,Sat Oct 30 22:59:59 2010,Sun Mar 28 03:00:00 2010,Sun Oct 31 02:59:59 2010,14400,t
Здесь строчку комментария я добавил для удобства просмотра. Мы видим 2 временные зоны (а для зимнего времени MSK и более того - найдутся 4 зоны), несложно получить и их названия:
$ grep 391 timezones.dat 
391,W-SU,+030000
veter@veter-eeepc:/srv/work/tcl_tz/packages/ref-timezones/sql/common$ grep 428 timezones.dat 
428,Europe/Moscow,+030000
Нужная нам информация о временном смещении содержится в предпоследнем поле файла timezone-rules.dat, в данном случае это 14400 секунд от UTC. Флаг в последнем поле этого же файла подсказывает, что время летнее. Значит, на серевере для текущей сессии пользователя следует использовать временное смещение +4 часа, что и требовалось узнать. Upd. На практике, раз нам все равно не удается узнать временную зону, достаточно воспользоваться вот таким яваскрипт-кодом для получения временного смещения в секундах:
getZone = function() {
  return -(new Date()).getTimezoneOffset()*60;
}
alert(getZone());
Полученное значение следует прибавить на сервере ко всем значениям времени в UTC, передаваемым клиенту. Можно также хранить в кукисах и аббевиатуру, чтобы выводить время в виде "2010-06-01 12:00:00 MSD" (заметим, что в России не принято отображать время в подобном формате). Upd. В очередной попсовой статейке в тему: Автоматическое определение часового пояса пользователя попался интересный комментарий:
Все даты у меня возвращаются пользователю, как Unixtime в UTC. После, скармливаю этот таймштамп объекту new Date(timestamp), а Javascript знает, что Unixtime должен быть в UTC и сам корректирует дату с учетом текущего часового пояса пользователя. И в конце можно отформатировать дату как угодно, перед отображением пользователю. Таким образом, я никогда у пользователя не спрашиваю его часовой пояс, но даты ему отображаются корректно.

За неимением повсеместной поддержки HTML5 тоже решение.

Комментариев нет:


(C) Alexey Pechnikov aka MBG, mobigroup.ru