Вкратце о том, для чего и в приложении к какой задаче это написано
Теория представления времени - обширный раздел, отнюдь не ограничивающийся сведениями, обозначенными в этом тексте. Более того - здесь приведена мизерная часть, имеющая лишь отношение к проблеме, общими словами описываемой фразой "какими программными средствами сохранять отметки времени, чтобы иметь возможность дальнейшего вычисления временных промежутков между ними с точностью до одной секунды, чтобы на правильность этих временных промежутков не влияли законодательные акты о всяких искусственных сдвигах временной шкалы - летнее время, перевод региона в другой часовой пояс и прочее".
Если вы не понимаете, почему в принципе невозможно напрямую вычислять временные интервалы между отметками времени, выраженными в местном времени, во времени разных часовых поясов и между отметками времени, данные о часовых поясах для которых утрачены - то информацию стоит искать в местах иных.
Опуская всяческие рассуждения и размышления, имеем следующее:
Поскольку представленное здесь - в основном для собственного упорядочения информации, то обширного охвата информации ожидать не стоит. Здесь нет, например, особенностей работы с временем для FreeBSD, MACOS и Android, всяких python, Ruby и VBA, динозавров типа Windows CE и Windows Mobile по банальной причине - в настоящее время они вне зоны моих интересов. Разве что можно отметить Android - в этой ОС программисту не стоит рассчитывать на поддержку временных зон от операционной системы, а программисту Java - на поддержку временных зон виртуальной машиной. И всё о нём.
Версии до Windows XP здесь не рассматриваются
В реестре windows существует специальная ветка HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\ назначением которой и является хранение поддерживаемых временных зон и истории изменения для них
Используются данные следующих структур:
typedef struct _SYSTEMTIME { WORD wYear; // Год. Годные значения - от 1601 по 30827 WORD wMonth; // Месяц. Нумерация начинается с единицы WORD wDayOfWeek; // День недели, как обычно, неделя начинается с воскресения, его код = 0 WORD wDay; // День месяца. Нумерация от единицы. WORD wHour; // Час. От 0 до 23 WORD wMinute; // Минута. От 0 до 59 WORD wSecond; // Секунда. От 0 до 59 WORD wMilliseconds; // Миллисекунды. От 0 до 999 } SYSTEMTIME, *PSYSTEMTIME; typedef struct _REG_TZI_FORMAT { LONG Bias; // Смещение локального времени компьютера относительно базового (UTC). В минутах! LONG StandardBias; // Значение смещения, (минуты) использующееся при конвертации зимнего времени. // В норме, это поле равно нуля. Добавляется к Bias (общему смещению зоны отн. UTC) // в период зимнего (стандартного времени) // Игнорируется, если не определено поле StandardDate LONG DaylightBias; // Значение смещения (минуты), использующееся при конвертации летнего времени // Добавляется к Bias в период летнего времени. Обычно равно "-60" // Игнорируется, если не определено поле DaylightDate SYSTEMTIME StandardDate; // Локальная дата и локальное время, начиная с которого смещение от UTC отсчитывается // для зимнего (если хотите - стандартного) времени. Если временная зона не // поддерживает переход на летнее время, или по какой-то причине этот перевод // заблокирован, поле wMonth будет нулевым. Если эта дата указана, то // DaylightDate тоже должно быть определено // *** Если wYear==0, то остальные поля структуры определяют относительное смещение // от начала года. wHour и wMinute - местное время перехода, wDayOfWeek - номер дня // недели, когда осуществляется перевод, wDay - номер недели в месяце. // wDay=5 означает "последнюю неделю месяца". // Т.е., чтобы указать "первое воскресение апреля, 2:00" - поля задаются так: // wHour = 2, wMonth = 4, wDayOfWeek = 0, wDay = 1 // "Последний четверг октября": wHour = 2, wMonth = 10, wDayOfWeek = 4, wDay = 5 // Заданное таким образом событие перевода отрабатывается каждый год. // *** Если wYear!=0, то дата является абсолютной и отрабатывается единожды. SYSTEMTIME DaylightDate; // Локальная дата и локальное время, начиная с которого применяется смещение // относительно UTC для летнего времени. Правила те же, что и для предыдущего поля. } REG_TZI_FORMAT; // Общая структура регистровой записи: typedef struct _TIME_ZONE_INFORMATION { LONG Bias; // Нет в реестре, вынесено в бинарное поле TZI WCHAR StandardName[32]; // Поле Std реестра, наименование для зимнего времени, // Для Новосибирска, например, выглядит в начале 2016 года как // "RTZ 5 (зима)" // Обратите внимание на длину поля SYSTEMTIME StandardDate; // Нет в реестре, вынесено в бинарное поле TZI LONG StandardBias; // Нет в реестре, вынесено в бинарное поле TZI WCHAR DaylightName[32]; // Поле Dlt реестра. Наименование летнего времени // Для Новосибирска, например, выглядит в начале 2016 года как // "RTZ 5 (лето)" SYSTEMTIME DaylightDate; // Нет в реестре, вынесено в бинарное поле TZI LONG DaylightBias; // Нет в реестре, вынесено в бинарное поле TZI } TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION; // поля вне структуры: // MUI_Display, MUI_Dlt, MUI_Std - Наименования соответствующих полей в случае, если установлен MUI. // В виде ссылок на номера ресурсов tzres.dll // Display - Высвечиваемое в GUI программах наименование зоны "(UTC+06:00) Новосибирск (RTZ 5)" // (Сама то ветка реестра для Новосибирска, напомним, носит имечко "N. Central Asia Standard Time")
Для реестра Windows есть интересная особенность: кроме того, что в нём хранится история для тех зон, которые MS соизволили определить и прописать (а они не совпадают ни по названиям, ни по history с базой Олсона), в базе ветки воткнуто единичное поле бинарного формата TIME_ZONE_INFORMATION, являющееся копией правила преобразования, действующего на текущий год. Есть весьма уверенное подозрение, что в ряде случаев преобразования работают только по этому единственному правилу.
Когда-то, во времена былые, на сайте microsoft фигурировала утилита для ручного исправления временных зон. Точнее, правилась только запись о "текущей годовой зоне". На настоящий момент эта утилита тщательно вымарана из всех доступных мест. Впрочем, практической пользы от неё не было ни тогда, ни сейчас. Название файла утилитки tzedit.exe, 85 килобайт, датируется 1999 годом.
Резюме для windows:
На файловой системе FAT16 и FAT32 время создания файлов хранится в виде поля word, отображающего абстрактную локальную дату. Соответственно, при разных настройках часовых поясов, для одного и того же файла будет отображена одна и та же дата, и одно и то же время, в целом не имеющие отношения к текущим показаниям часов, если файлы были порождены на компьютере с другой временной зоной
Файловая система NTFS хранит время создания и модификации файлов в UTC. Технически, однако, отсчёт ведётся от 1601 года в 100-миллисекундных интервалах. Видимо, потому что в 1601 году Средневековье впервые открыло для себя Окна ))
Linux имеет полную поддержку временных зон базы данных Олсона, с отслеживанием всех правил на все заданные периоды. Все программы, работающие через libc, конверсии времени осуществляют с учетом текущей временной зоны
Чтобы установить актуальные временные зоны для дистрибутива Linux, на который кончился срок поддержки, либо временные зоны в нужные моменты забыли обновить или обновили косоруко, необходимо - скачать архив действующих временных зон с iana.org, распаковать его где-нибудь в удобном месте, затем под правами root дать команду zic название_файла_с_нужным_описанием_зоны
Что интересно, зоны для Новосибирска и Барнаула, именуемые в базе Asia/Novosibirsk и Asia/Barnaul, находятся в файле "europe".
Результаты компиляции перемещаются в каталог /usr/share/zoneinfo/ в соответствующий подраздел. Для Asia/Barnaul это будет файл: /user/share/zoneinfo/Asia/barnaul
На указанный файл необходимо переписать символическую ссылку /etc/localtime
Далее, необходимо проверить название временной зоны в файле /etc/timezone (все дебианоподобные дистрибутивы) и при необходимости поправить его (в случае, когда машина переводится в другую временную зону, а не просто меняются правила старые на новые).
Следующим шагом лучше всего сделать перезагрузку операционной системы. Как минимум - необходимо перезапустить все службы, так или иначе зависящие от времени - cron, mysql, apache, почтовые сервисы и прочее.
Тестовая программа, работающей с датой и временем в пределах необходимой нам задачи:
#include <time.h> #include <stdio.h> void printout(time_t t) { printf("%d : %s",t,ctime(&t)); } time_t FromStringToTime(char *s) { struct tm tresult; strptime(s,"%Y-%m-%dT%T %Z",&tresult); return mktime(&tresult); } int main(int argc, char** argv) { setenv("TZ",":Asia/Barnaul",1); tzset(); time_t t; time(&t); printout(t); printout(1459022400); printout(1459022399); t=1459022399; struct tm tx=*localtime(&t); char sbu[256]; strftime(sbu,sizeof(sbu)-1,"%Y-%m-%dT%T %Z",&tx); printf("Formatted: %s\n",sbu); printout(FromStringToTime(sbu)); printout(FromStringToTime("2016-03-27T03:00:00 Asia/Barnaul")); return 0; }
Отметить стоит следующее: при необходимости работать с временной зоной, отличающейся от системной используется несколько странный способ: сначала программа должна установить переменную окружения TZ в название файла используемой временной зоны (начинается с двоеточия), а далее - вызовом tzset применить установки для дальнейшего кода программы:
setenv("TZ",":Asia/Barnaul",1); tzset();
Что делают вызовы остальных функции, понятно без дополнительных комментариев
Программа, приведенная в подразделе для Linux, формально может быть использована и для тестирования Visual C++, с некоторыми поправками:
В результате тестирования выявляется следующее: правила перехода учитываются только для текущего года, временные отметки за его пределами конвертируются некорректно.
Более того (справка MSDN): "Во всех версиях Microsoft C/C++, кроме версии Microsoft C/C++ 7.0, и во всех версиях Visual C++ time() возвращает текущее время как количество секунд, прошедших с полуночи 1-го января 1970 года. В версии Microsoft C/C++ 7.0 time() возвращает текущее время как число секунд, истекших с полуночи 31-го декабря 1899. "
Если возникает такая необходимость, все функции, имеющие отнощение к дате и времени с точки зрения Microsoft, можно посмотреть в справочной системе Visual Studio в разделе "Управление временем". Их там не так уж много.
Время в php представляет собой классический unix timestamp. Набор базовых функций, работающих с временем, практически повторяет функции аналогичного назначения из Си. При попытке использовать функции работы с датой и временем, php подразумевает, что для него в обязательном порядке должна быть явным образом определена временная зона по умолчанию, иначе выдаётся следующее сообщение:
phpinfo(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected the timezone 'UTC' for now, but please set date.timezone to select your timezone.
PHP, начиная с определённой версии (imho 5.2>), поддерживает два варианта работы с базой временных зон:
Временную зону по умолчанию можно задать в php.ini добавлением например строки date.timezone=Asia/Barnaul в раздел [date] (Шаблон по умолчанию может отсутствовать)
Тестовый примерчик:
date_default_timezone_set("Asia/Barnaul"); echo ($d=mktime(3,1,0,3,27,2016))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(3,0,0,3,27,2016))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(2,0,0,3,27,2016))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(1,59,59,3,27,2016))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(0,0,0,1,1,2015))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(0,3,10,10,26,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(1,59,37,10,26,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(2,0,7,10,26,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(2,31,48,10,26,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(3,18,30,10,26,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(0,1,03,08,02,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(0,1,51,12,1,2008))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo ($d=mktime(0,2,5,2,16,2014))."\n"; echo date("Y-m-d H:i:s",$d)."\n\n"; echo timezone_version_get();
И результат его работы на машине с правильно установленными зонами (timezonedb):
1459022460 2016-03-27 03:01:00 1459022400 2016-03-27 03:00:00 1459022400 2016-03-27 03:00:00 1459022399 2016-03-27 01:59:59 1420048800 2015-01-01 00:00:00 1414256590 2014-10-26 00:03:10 1414267177 2014-10-26 01:59:37 1414267207 2014-10-26 02:00:07 1414269108 2014-10-26 02:31:48 1414271910 2014-10-26 03:18:30 1385917263 2013-12-02 00:01:03 1228068111 2008-12-01 00:01:51 1392483725 2014-02-16 00:02:05 2016.3
Если используется привязка к операционной системе и timezonedb не инсталлирован, в последней строке будет выведено наименование "0.system" вместо действительной версии базы tzdata.
(версии 5.х, состояние в предыдущих не отслеживалось)
Поддерживаются два вида работы с временными зонами:
Собственные справочники в исходной инсталляции MySQL всегда пусты, sql скрипт для заполнения генерируется на unix-системе с корректно установленной tzdata командой
mysql_tzinfo_to_sql /usr/share/zoneinfo/ > tzdata_sql_script.sql
Далее скрипт применяется к базе "mysql" под рутом:
mysql -u root -p mysql < tzdata_sql_script.sql
Поскольку в windows, как вы понимаете, правильную информацию о временных зонах взять неоткуда, то по адресу, похожему на этот расположены ссылки, по которым можно скачать архив с бинарным (!!!) представлением необходимых таблиц. Или воспользоваться результатом скрипта с unix-машины. Таблицы дат как таковых не содержат, поэтому перенос должен быть корректным, даже если windows-носитель mySQL находится в чёрти-каком часовом поясе по настройкам.
В обычном mySQL с настройками по умолчанию картина с временными зонами выглядит следующим образом:
mysql> select @@global.time_zone,@@session.time_zone; +--------------------+---------------------+ | @@global.time_zone | @@session.time_zone | +--------------------+---------------------+ | SYSTEM | SYSTEM | +--------------------+---------------------+
Как нетрудно догадаться, для каждого сеанса можно поставить собственное значение временной зоны, например так:
set @@session.time_zone='+07:00';
в случае, когда mySQL настроен на использование системных временных зон, он ничего не знает об их аббревиатурах, кроме наименования зоны "SYSTEM". Значит что? Если надо работать с разными зонами из сеансов по миру - что? - импортировать таблицы временных зон!
После импортирования, возможны следующие фокусы:
set @@session.time_zone='Asia/Barnaul';
При установленных таблицах временных зон можно задать состояние по умолчанию:
mysql> select @@global.time_zone,@@session.time_zone; +--------------------+---------------------+ | @@global.time_zone | @@session.time_zone | +--------------------+---------------------+ | Asia/Barnaul | Asia/Barnaul | +--------------------+---------------------+Делается это внесением в файл конфигурации my.ini строки:
default_time_zone = 'Asia/Barnaul'
Следует отметить, что закомментированного шаблона под директиву default_time_zone в my.ini обычно нет, и документация mySQL очень неохотно распространяется на эту тему и совсем не там, где информация ожидаема.
Что ж, попробуем проверить корректность работы. Для смены часового пояса "один час вперед" (то же будет при переходе на летнее время):
mysql> select from_unixtime(1459022460),unix_timestamp(from_unixtime(1459022460)); +---------------------------+-------------------------------------------+ | from_unixtime(1459022460) | unix_timestamp(from_unixtime(1459022460)) | +---------------------------+-------------------------------------------+ | 2016-03-27 03:01:00 | 1459022460 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1459022400),unix_timestamp(from_unixtime(1459022400)); +---------------------------+-------------------------------------------+ | from_unixtime(1459022400) | unix_timestamp(from_unixtime(1459022400)) | +---------------------------+-------------------------------------------+ | 2016-03-27 03:00:00 | 1459022400 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1459022399),unix_timestamp(from_unixtime(1459022399)); +---------------------------+-------------------------------------------+ | from_unixtime(1459022399) | unix_timestamp(from_unixtime(1459022399)) | +---------------------------+-------------------------------------------+ | 2016-03-27 01:59:59 | 1459022399 | +---------------------------+-------------------------------------------+
И "несуществующее" время:
mysql> select unix_timestamp("2016-03-27 02:30:00"); +---------------------------------------+ | unix_timestamp("2016-03-27 02:30:00") | +---------------------------------------+ | 1459022400 | +---------------------------------------+
Прошлая смена пояса, 2014 год, 26 октября:
mysql> select from_unixtime(1414256590),unix_timestamp(from_unixtime(1414256590)); +---------------------------+-------------------------------------------+ | from_unixtime(1414256590) | unix_timestamp(from_unixtime(1414256590)) | +---------------------------+-------------------------------------------+ | 2014-10-26 00:03:10 | 1414256590 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1414267177),unix_timestamp(from_unixtime(1414267177)); +---------------------------+-------------------------------------------+ | from_unixtime(1414267177) | unix_timestamp(from_unixtime(1414267177)) | +---------------------------+-------------------------------------------+ | 2014-10-26 01:59:37 | 1414267177 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1414267207),unix_timestamp(from_unixtime(1414267207)); +---------------------------+-------------------------------------------+ | from_unixtime(1414267207) | unix_timestamp(from_unixtime(1414267207)) | +---------------------------+-------------------------------------------+ | 2014-10-26 02:00:07 | 1414267207 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1414269108),unix_timestamp(from_unixtime(1414269108)); +---------------------------+-------------------------------------------+ | from_unixtime(1414269108) | unix_timestamp(from_unixtime(1414269108)) | +---------------------------+-------------------------------------------+ | 2014-10-26 02:31:48 | 1414269108 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1414271910),unix_timestamp(from_unixtime(1414271910)); +---------------------------+-------------------------------------------+ | from_unixtime(1414271910) | unix_timestamp(from_unixtime(1414271910)) | +---------------------------+-------------------------------------------+ | 2014-10-26 03:18:30 | 1414271910 | +---------------------------+-------------------------------------------+
И некоторые даты из предшествующих годов:
mysql> select from_unixtime(1385917263),unix_timestamp(from_unixtime(1385917263)); +---------------------------+-------------------------------------------+ | from_unixtime(1385917263) | unix_timestamp(from_unixtime(1385917263)) | +---------------------------+-------------------------------------------+ | 2013-12-02 00:01:03 | 1385917263 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1228068111),unix_timestamp(from_unixtime(1228068111)); +---------------------------+-------------------------------------------+ | from_unixtime(1228068111) | unix_timestamp(from_unixtime(1228068111)) | +---------------------------+-------------------------------------------+ | 2008-12-01 00:01:51 | 1228068111 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1392483725),unix_timestamp(from_unixtime(1392483725)); +---------------------------+-------------------------------------------+ | from_unixtime(1392483725) | unix_timestamp(from_unixtime(1392483725)) | +---------------------------+-------------------------------------------+ | 2014-02-16 00:02:05 | 1392483725 | +---------------------------+-------------------------------------------+ mysql> select from_unixtime(1420048800),unix_timestamp(from_unixtime(1420048800)); +---------------------------+-------------------------------------------+ | from_unixtime(1420048800) | unix_timestamp(from_unixtime(1420048800)) | +---------------------------+-------------------------------------------+ | 2015-01-01 00:00:00 | 1420048800 | +---------------------------+-------------------------------------------+
Ok, казалось бы, всё прекрасно и замечательно, однако -
mysql> select 1459022399-1459022400; +-----------------------+ | 1459022399-1459022400 | +-----------------------+ | -1 | +-----------------------+
- что есть правильно, и на представленческом аналоге тех же таймштампов:
mysql> select timediff("2016-03-27 01:59:59","2016-03-27 03:00:00"); +-------------------------------------------------------+ | timediff("2016-03-27 01:59:59","2016-03-27 03:00:00") | +-------------------------------------------------------+ | -01:00:01 | +-------------------------------------------------------+
- что есть абсолютно неправильно (сосчитан один час одна секунда разницы, если не столь заметно с первого взгляда)
Тем не менее:
mysql> select unix_timestamp("2016-03-27 01:59:59")-unix_timestamp("2016-03-27 03:00:00"); +-----------------------------------------------------------------------------+ | unix_timestamp("2016-03-27 01:59:59")-unix_timestamp("2016-03-27 03:00:00") | +-----------------------------------------------------------------------------+ | -1 | +-----------------------------------------------------------------------------+
И тот же эффект с другого конца:
mysql> select adddate("2016-03-27 01:59:59",interval 2 second); +--------------------------------------------------+ | adddate("2016-03-27 01:59:59",interval 2 second) | +--------------------------------------------------+ | 2016-03-27 02:00:01 | +--------------------------------------------------+
- выдано несуществующее время для текущих данных временной зоны
Из этого следует: Все функции работы с датой и временем mysql, кроме прямого преобразования в unix timestamp и обратно, игнорируют таблицы временных зон и оперируют с абстрактными датами. Впрочем, если считать что эти даты в UTC, то операции условно можно признать правильными
Нещадно лажают, несмотря на названия, и вот эти функции:
mysql> select timestampadd(second,2,"2016-03-27 01:59:59"); +----------------------------------------------+ | timestampadd(second,2,"2016-03-27 01:59:59") | +----------------------------------------------+ | 2016-03-27 02:00:01 | +----------------------------------------------+ mysql> select timestampdiff(second,"2016-03-27 01:59:59","2016-03-27 03:00:01"); +-------------------------------------------------------------------+ | timestampdiff(second,"2016-03-27 01:59:59","2016-03-27 03:00:01") | +-------------------------------------------------------------------+ | 3602 | +-------------------------------------------------------------------+
Казалось бы, после внесения в базу данных, все поля timestamp "представлены в виде целого числа"(С), однако ноль разницы:
mysql> create table test(t1 timestamp, t2 timestamp) engine myISAM; mysql> insert into test (t1,t2) values ( "2016-03-27 01:59:59","2016-03-27 03:00:01"); mysql> select * from test; +---------------------+---------------------+ | t1 | t2 | +---------------------+---------------------+ | 2016-03-27 01:59:59 | 2016-03-27 03:00:01 | +---------------------+---------------------+ mysql> select timestampdiff(second,t1,t2) from test; +-----------------------------+ | timestampdiff(second,t1,t2) | +-----------------------------+ | 3602 | +-----------------------------+ mysql> select timediff(t1,t2) from test; +-----------------+ | timediff(t1,t2) | +-----------------+ | -01:00:02 | +-----------------+
Но можно путем плохо читабельных выкрутасов заставить функции вычисления временных промежутков работать правильно:
mysql> select timediff(convert_tz(t1,"SYSTEM","+00:00"),convert_tz(t2,"SYSTEM","+00:00")) from test; +-----------------------------------------------------------------------------+ | timediff(convert_tz(t1,"SYSTEM","+00:00"),convert_tz(t2,"SYSTEM","+00:00")) | +-----------------------------------------------------------------------------+ | -00:00:02 | +-----------------------------------------------------------------------------+ mysql> select timediff(convert_tz("2016-03-27 01:59:59","SYSTEM","+00:00"),convert_tz("2016-03-27 03:00:01","SYSTEM","+00:00")); +-------------------------------------------------------------------------------------------------------------------+ | timediff(convert_tz("2016-03-27 01:59:59","SYSTEM","+00:00"),convert_tz("2016-03-27 03:00:01","SYSTEM","+00:00")) | +-------------------------------------------------------------------------------------------------------------------+ | -00:00:02 | +-------------------------------------------------------------------------------------------------------------------+
Все примеры будут приведены на C#, как наиболее читабельном языке программирования для .net
Для работы с датой и временем библиотеки .net содержат структуры данных DateTime и DateTimeOffset (непосредственно хранение данных), класс TimeSpan, являющийся результатом и аргументом всего, что задано с временными промежутками, и класс TimeZoneInfo, представляющий собой как раз набор правил и истории предобразования дат и времени. В MSDN упомянуто, что данные для правил берутся из реестра операционной системы. Однако, для mono все эти данные, похоже, берутся из локальной tzdata.dll, и ко ОС не имеют никакого отношения.
Разница между DateTime и DateTimeOffset состоит в том, что каждый экземпляр DateTimeOffset содержит данные по смещению относительно UTC для содержащегося значения даты и времени. Но кроме смещения этот тип данных не знает о временной зоне ничего - ни ее названия, ни истории. DateTime смещения не содержит. Однако, в ней есть флаг TimeKind, который может показывать три принадлежности - либо это время в UTC, либо это время в локали, либо непонятно что (ага, именно так - т.е. undefined). Некоторые функции преобразования за этим флагом следят и не дают, например преобразовать время из UTC в UTC, пользуясь правилами какой-нибудь сторонней зоны.
Формат базы данных временных переходов в целом очень похож на структуры, хранимые в реестре. Средствами объектов .net можно прочитать все доступные зоны и вывести их правила. Чем и займёмся:
public static void AboutTimeZone(TimeZoneInfo timeZone) { string[] monthNames = CultureInfo.CurrentCulture.DateTimeFormat.MonthNames; TimeZoneInfo.AdjustmentRule[] adjustments = timeZone.GetAdjustmentRules(); // Display message for time zones with no adjustments Console.WriteLine("{0} ({1}):", timeZone.StandardName, timeZone.DisplayName); Console.WriteLine("BaseUtcOffset: {0}",timeZone.BaseUtcOffset); Console.WriteLine("SupportsDaylightSavingTime: {0}",timeZone.SupportsDaylightSavingTime); if (adjustments.Length == 0) { Console.WriteLine("{0} has no adjustment rules", timeZone.StandardName); } else { // Handle time zones with 1 or 2+ adjustments differently bool showCount = false; int ctr = 0; string spacer = ""; Console.WriteLine("{0} Adjustment rules", timeZone.StandardName); if (adjustments.Length > 1) { showCount = true; spacer = " "; } // Iterate adjustment rules foreach (TimeZoneInfo.AdjustmentRule adjustment in adjustments) { if (showCount) { Console.WriteLine(" Adjustment rule #{0}", ctr + 1); ctr++; } // Display general adjustment information Console.WriteLine("{0} Start Date: {1:D}", spacer, adjustment.DateStart); Console.WriteLine("{0} End Date: {1:D}", spacer, adjustment.DateEnd); Console.WriteLine("{0} DaylightDelta: {1}:{2:00} hours", spacer, adjustment.DaylightDelta.Hours, adjustment.DaylightDelta.Minutes); // Get transition start information TimeZoneInfo.TransitionTime transitionStart = adjustment.DaylightTransitionStart; Console.Write("{0} DaylightTransitionStart: ", spacer); if (transitionStart.IsFixedDateRule) { Console.WriteLine("(FixedRule) {0} {1} at {2}", monthNames[transitionStart.Month - 1], transitionStart.Day, transitionStart.TimeOfDay); } else { Console.WriteLine("(FloatingRule) {0} [{1}] {2} of {3} at {4}", ((WeekOfMonth)transitionStart.Week).ToString(), transitionStart.Week, transitionStart.DayOfWeek.ToString(), monthNames[transitionStart.Month - 1], transitionStart.TimeOfDay); } // Get transition end information TimeZoneInfo.TransitionTime transitionEnd = adjustment.DaylightTransitionEnd; Console.Write("{0} DaylightTransitionEnd: ", spacer); if (transitionEnd.IsFixedDateRule) { Console.WriteLine("(FixedRule) {0} {1} at {2}", monthNames[transitionEnd.Month - 1], transitionEnd.Day, transitionEnd.TimeOfDay); } else { Console.WriteLine("(FloatingRule) {0} [{1}] {2} of {3} at {4}", ((WeekOfMonth)transitionEnd.Week).ToString(), transitionEnd.Week, transitionEnd.DayOfWeek.ToString(), monthNames[transitionEnd.Month - 1], transitionEnd.TimeOfDay); } } } Console.WriteLine(); } private static void ShowStartAndEndDates() { // Получить все временные зоны от системы ReadOnlyCollectiontimeZones = TimeZoneInfo.GetSystemTimeZones(); // Отобразить каждую временную зону foreach (TimeZoneInfo timeZone in timeZones) { AboutTimeZone(timeZone); } }
Текст программы представляет собой модификацию примерчика на GetSystemTimeZones и генерирует много чего интересного, откуда выкусим временную зону Барнаула. Вот она (.Net 4.6.1):
Алтайское стандартное время ((UTC+07:00) Барнаул, Горно-Алтайск): BaseUtcOffset: 06:00:00 SupportsDaylightSavingTime: True Алтайское стандартное время Adjustment rules Adjustment rule #1 Start Date: 1 января 0001 г. End Date: 31 декабря 2010 г. DaylightDelta: 1:00 hours DaylightTransitionStart: (FloatingRule) Last [5] Sunday of Март at 01.01.0001 2:00:00 DaylightTransitionEnd: (FloatingRule) Last [5] Sunday of Октябрь at 01.01.0001 3:00:00 Adjustment rule #2 Start Date: 1 января 2011 г. End Date: 31 декабря 2011 г. DaylightDelta: 1:00 hours DaylightTransitionStart: (FloatingRule) Last [5] Sunday of Март at 01.01.0001 2:00:00 DaylightTransitionEnd: (FloatingRule) First [1] Saturday of Январь at 01.01.0001 0:00:00 Adjustment rule #3 Start Date: 1 января 2012 г. End Date: 31 декабря 2012 г. DaylightDelta: 0:00 hours DaylightTransitionStart: (FixedRule) Январь 1 at 01.01.0001 0:00:00 DaylightTransitionEnd: (FixedRule) Январь 1 at 01.01.0001 0:00:00 Adjustment rule #4 Start Date: 1 января 2013 г. End Date: 31 декабря 2013 г. DaylightDelta: 0:00 hours DaylightTransitionStart: (FixedRule) Январь 1 at 01.01.0001 0:00:00 DaylightTransitionEnd: (FixedRule) Январь 1 at 01.01.0001 0:00:00 Adjustment rule #5 Start Date: 1 января 2014 г. End Date: 31 декабря 2014 г. DaylightDelta: 1:00 hours DaylightTransitionStart: (FloatingRule) First [1] Wednesday of Январь at 01.01.0001 0:00:00 DaylightTransitionEnd: (FloatingRule) Last [5] Sunday of Октябрь at 01.01.0001 2:00:00 Adjustment rule #6 Start Date: 1 января 2016 г. End Date: 31 декабря 2016 г. DaylightDelta: 1:00 hours DaylightTransitionStart: (FloatingRule) Last [5] Sunday of Март at 01.01.0001 2:00:00 DaylightTransitionEnd: (FloatingRule) First [1] Friday of Январь at 01.01.0001 0:00:00 Adjustment rule #7 Start Date: 1 января 2017 г. End Date: 31 декабря 9999 г. DaylightDelta: 0:00 hours DaylightTransitionStart: (FixedRule) Январь 1 at 01.01.0001 0:00:00 DaylightTransitionEnd: (FixedRule) Январь 1 at 01.01.0001 0:00:00
Чтобы не было кривотолков, скажу прямо, что системное определение работает правильно в пределах всех заданных периодов и на границах их.
Формально, можно создать собственную временную зону и задать для нее правила перехода как душе угодно. Если бы не одно "но". Вы обратили внимание на одинаковые значения начала и конца периода (DaylightTransitionStart и DaylightTransitionEnd) в правилах №3 и №4 системного определения зоны Алтайского Края? Так вот, подобное метод CreateAdjustmentRule при аргументах FixedRule, ввести вам не позволит - с выбросом Exception, в котором будет сказано, что дата конца периода не может быть равной дате начала периода или превышать её. При попытке определить такое периодами с разницей в одну секунду, согласно озвученному правилу, вы заполучите мёртвые зоны перехода на летнее/зимнее время по крайней мере один раз в период, что сделает применение полученного определения неадекватным, то же самое - при игрищах с FloatingRule, где end меньше start на одну секунду (такое контроль пропускает, но смысла в подобном определении всё равно немного). По факту: нормально можно определить только временные зоны с "истинным" переходом на летнее время, и никак иначе. О чём прямым текстом записано в официальной справке MSDN: "Параметр daylightDelta измеряет разницу между стандартным и летним временем для часового пояса. Он не предназначен для определения разницы между стандартным временем часового пояса и всемирным временем (UTC). Класс TimeZoneInfo предполагает, что поправка к всемирному времени (UTC) остается постоянной на протяжении всего срока существования часового пояса. Чтобы отразить изменение разницы между временем часового пояса и всемирным временем UTC, которое не вызвано использованием правила коррекции, следует вызвать метод CreateCustomTimeZone и создать новый настраиваемый часовой пояс.". Однако, создать новый настраиваемый часовой пояс, в котором бы соседствовали периоды, когда есть "летнее время", и периоды, когда перехода на летнее время нет - не получится при всём желании, потому что (сказано выше).
Чтобы информация не пропадала, вот на всякий случай код, повторяющий определение временной зоны Барнаула, как оно представлено в системном обновлении от 13.04.2016:
public TimeZoneBarnaul() { // barnaulTZ = TimeZoneInfo.Local; // return; TimeSpan delta = new TimeSpan(1, 0, 0); TimeSpan delta0 = new TimeSpan(0, 0, 0); TimeZoneInfo.AdjustmentRule adjustment; ListadjustmentList = new List (); // Declare transition time variables to hold transition time information TimeZoneInfo.TransitionTime transitionRuleStart, transitionRuleEnd; // 1 - ok летнее время есть transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 3, 5, DayOfWeek.Sunday); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 3, 0, 0), 10, 5, DayOfWeek.Sunday); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(1, 1, 1), new DateTime(2010, 12, 31), delta, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 2 Первый год, когда отменили летнее время - ok transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 3, 5, DayOfWeek.Sunday); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1, DayOfWeek.Saturday); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2011, 1, 1), new DateTime(2011, 12, 31), delta, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 3 2012 - здесь постоянно летнее время transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1,DayOfWeek.Wednesday); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1, DayOfWeek.Wednesday); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2012, 1, 1), new DateTime(2012, 12, 31), delta0, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 4 2013 - здесь постоянно летнее время transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2013, 1, 1), new DateTime(2013, 12, 31), delta0, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 5. 2014 26.10 - постоянное зимнее время и сдвинули пояс на UTC+6, переход как таковой незаметен transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1, DayOfWeek.Wednesday); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 5, DayOfWeek.Sunday); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2014, 1, 1), new DateTime(2014, 12, 31), delta, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 7. 2016 27.05.2016 с 2:00 пояс UTC+7. Летнего времени нет transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 3, 5, DayOfWeek.Sunday); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1, DayOfWeek.Friday); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2016, 1, 1), new DateTime(2016, 12, 31), delta, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); // 8. 2017-й и далее. Постоянное зимнее время пояса UTC+7 // (т.е. в рамках извращенной логики TimeZoneInfo: постоянное летнее время пояса UTC+6); transitionRuleStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1); transitionRuleEnd = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1); adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2017, 1, 1), new DateTime(9999, 1, 1), delta0, transitionRuleStart, transitionRuleEnd); adjustmentList.Add(adjustment); barnaulTZ = TimeZoneInfo.CreateCustomTimeZone("Barnaul Standard Time", new TimeSpan(6, 0, 0), "(GMT+07:00) Barnaul Time", "Barnaul Standard Time", "Barnaul Daylight Time", adjustmentList.ToArray()); }
Код компиляцию пройдет, но при попытке исполнения вывалит Exception о недопустимых интервалах конца и начала периода. Что характерно, начало и конец периода не контролируются для CreateFloatingDateRule, если только нет абсолютного совпадения в определении, потому что в этой функции они заданы неявно, и это даёт возможность определять периоды с концом раньше начала, но и это не позволит внедрить правило в практику, потому что на "начале периода" сработает "переход на летнее время" с несуществующим временным промежутком - т.е. если "начало периода" совпадёт, скажем, с 1 января, 00:00, то время с 00:00 по 00:01 этого числа станет несуществующим и трансляция в UTC с него станет невозможным - т.е. получите Exception с сообщением "неверное время".
Нам недвусмысленно сказали, что механизм "истории часовых поясов" в .net framework и в Windows (поскольку используется та же методика и структуры хранения (данные берутся из реестра, да) - на практике почти непригоден для обслуживания реальных временных зон.
Можно, конечно, наделать несколько часовых поясов и по условиям анализа дат перебрасывать их, но... Смысл то в чём? Для таких исчислений можно обойтись и без создания структур AdjustmentRule с их наукообразными правилами кодирования и всё сделать гораздо проще.
Интересно, что определенные в реестре windows зоны с "неправильными" правилами, не проходящими контроль, в принципе, нормально отрабатываются и дают корректные результаты перевода - но никогда нельзя быть уверенным, что в windows выставлена именно правильная часовая зона, и что результаты перевода в UTC и обратно корректен для всех действующих промежутков времени.
В mono всё хуже - там действующая зона по умолчанию с именем SYSTEM не содержит правил перехода и считается зоной с международным фиксированным временем, в результате чего неправильно печатается даже текущее время, полученное через DateTime.Now, если на unix-машине не дай боже действующая зона без летнего времени. И определений временных зон для среды исполнения там к тому же гораздо меньше, чем в .net
Для того, чтобы проникнуться осознанием, что всё на самом деле так плохо, как кажется, обратимся к документации MSDN. Это здесь: (Раздельчик Общие сведения о часовых поясах)
Самое важное отсюда:
Однако, на десерт, оставлен пока грязный хак, позволяющий нагло обойти контроль начала и конца Правила Перехода и позволяющий воссоздать экзотические зоны из реестра, а при вдумчивом использовании - слепить свои собственные. Это методы ToSerializedString/FromSerializedString. Если у нас компьютер с корректной зоной в реестре, можно сделать следующее:
string tzs=TimeZoneInfo.Local.ToSerializedString(); TimeZoneInfo barnaulTZ=TimeZoneInfo.FromSerializedString(tzs);
Если содержимое строки сохранить в удобном месте, при необходимости можно воссоздать зону на компьютере, в реестре которого такой зоны нет. Строчка для Барнаула представляет из себя следующее:
string tzs = "Altai Standard Time;360;(UTC+07:00) Барнаул, Горно-Алтайск;"+ "Алтайское стандартное время;Алтайское летнее время;"+ "[01:01:0001;12:31:2010;60;[0;02:00:00;3;5;0;];[0;03:00:00;10;5;0;];]"+ "[01:01:2011;12:31:2011;60;[0;02:00:00;3;5;0;];[0;00:00:00;1;1;6;];]"+ "[01:01:2012;12:31:2012;0;[1;00:00:00;1;1;];[1;00:00:00.001;1;1;];60;]"+ "[01:01:2013;12:31:2013;0;[1;00:00:00;1;1;];[1;00:00:00.001;1;1;];60;]"+ "[01:01:2014;12:31:2014;60;[0;00:00:00;1;1;3;];[0;02:00:00;10;5;0;];]"+ "[01:01:2016;12:31:2016;60;[0;02:00:00;3;5;0;];[0;00:00:00;1;1;5;];]"+ "[01:01:2017;12:31:9999;0;[1;00:00:00;1;1;];[1;00:00:00.001;1;1;];60;];";
Из чего нетрудно понять, как задавать новые правила перехода при необходимости их кодирования
Хак работает для .net 4.5-4.6.1, ранние версии сбиваются; не факт, что контроль границ не введут в дальнейшем.
Во FreePascal касающееся работы с датами и временем содержится в unitах SysUtils и DateUtils. Количество функций впечатляет, однако при внимательном изучении можно легко придти к мнению, что многие из них бесполезны целиком и полностью, а многие на самом деле будут выдавать далеко не то, что от них ожидается.
Начальная концепция работы с датами и временем заимствована еще из Turbo Pascal и основывается на наивных представлениях, не знающих абсолютно ничего о временных поясах, летнем времени и прочем. Причем, для технического представления даты и времени в TDateTime используется число с плавающей точкой, что характеризует точность и сохранность представляемого. Точнее, неоднозначность. Кроме TDateTime используется структура SystemTime, на самом деле представляющая нечто типа struct tm из Си, назначение которой - "представление даты и времени в читабельном виде" - т.е. чтобы не плодить отдельных переменных для года, месяца, числа и т.д. - они там представлены скопом и определяются единой переменной. Еще есть некое образование в виде FileDate, скорее всего представляющее собой комбинацию полей даты и времени изменения файла во внутреннем представлении файловой системы FAT, для практики не имеющее никакого смысла, однако навязываемое для выполнения определенных действий и манипуляций с файлами, - подозреваю, что по чисто историческим причинам и для совместимости.
В DateUtils функции дельфийского периода истории. Здесь уже проскакивает нечто более полезное для наших целей:
function UnixToDateTime(timestamp:Int64):TDateTime; function DateTimeToUnix(tm:TDateTime):Int64;
Да, именно. Переводят Unix timestamp в TDateTime для UTC. Именно в UTC, без учета смещений и временных зон. Чтобы получить локальное время, придётся ввестви в действие вот эти перегруженные функции:
function UniversalTimeToLocal(UT: TDateTime):TDateTime; function UniversalTimeToLocal(UT: TDateTime; TZOffset: Integer):TDateTime; Обратные функции: function LocalTimeToUniversal(LT: TDateTime):TDateTime; function LocalTimeToUniversal(LT: TDateTime; TZOffset: Integer):TDateTime;
TZOffset здесь - смещение в минутах для текущего момента текущего часового пояса, и может быть считано как результат вызова GetLocalTimeOffset из SysUtils. Если функции перевода используются без задания смещения, таковое определяется единожды при старте программы. Если во время исполнения смещение меняется - программа об этом не узнает. Говоря более понятно: перечисленные функции непригодны для перевода величин из UTC в локальное время, потому что используют одно и то же смещение, считанное при старте программы, не учитывают временных зон и правил перехода даже при работе под управлением операционных систем, обеспечивающих полную поддержку tzdata;
Для работы под linux существует также набор непортабельных функций взаимного преобразования unix timestamp и TDateTime. Посмотрим:
uses unixutil; ... procedure EpochToLocal( epoch: LongInt; var year: Word; var month: Word; var day: Word; var hour: Word; var minute: Word; var second: Word ); function LocalToEpoch( year: Word; month: Word; day: Word; hour: Word; minute: Word; second: Word ):LongInt;
И для работы с временными зонами тоже:
uses unix; ... procedure ReReadLocalTime; function fpgettimeofday(tp: ptimeval; tzp: ptimezone):cint; procedure GetLocalTimezone(timer: cint; var leap_correct: cint; var leap_hit: cint); procedure GetLocalTimezone(timer: cint); function GetTimezoneFile: string; procedure ReadTimezoneFile(fn: string);
Все эти функции - deprecated, и, несмотря на объявления в платформозависимом модуле для операционной системы, поддерживающей нормальную работу с временными зонами, перевод локального времени в UTC и обратно осуществляют неправильно.
Я тут хотел написать про функции из модуля libc... Но, поскольку он deprecated, непортируем и frozen во времена царя Гороха, то оставлю эту неблагодарную затею
Резюме по FreePascal: Концепция работы с датами и временем для практических действий неприемлема. Работа с временными зонами в принципе возможна, если программист изобретёт собственную поддержку вычисления смещения относительно UTC в зависимости от показания локального времени или timestamp'a; либо воспользуется сторонним кодом. В справке freepascal упоминаются нативный модуль поддержки баз tzdata, работающий по оригинальным базам. В базовой поддержке - этого функционала нет. Корректный перевод в UTC и обратно возможен только для текущего момента времени и с условием, что программа в процессе выполнения не преодолевала точек перехода.
Java, в отличие (или в подобие) многого другого, информацию о временных зонах носит с собой. База данных правил перехода полностью совпадает с базой Олсона, более того - напрямую генерируется из базы Олсона, и во всех пакетах дистрибуции от Oracle находится в гарантированно неактуальном состоянии, о чём беззастенчиво сообщается тут (лучше искать поиском по заголовку "Timezone Data Versions in the JRE Software").
Для подгона tzdb java под актуальное состояние существует специальный инструмент, о наличии которого нам сообщать явно не спешат, но при желании его можно всегда найти. Это -jar - программка tzupdater. Находится обычно на страничке загрузки инструментариев для разработчиков, в разделе "Additional Resources" и его подразделе "Java Time Zone Updater Tool".
Java Time Zone Updater Tool с собою несёт экземпляр tzdb для обновления, который традиционно всегда находится в неактуальном состоянии, причем отставание может быть куда большим, чем для пакетов дистрибьюции.
Единственный документированный способ посмотреть версию tzdb из установленных Oracle Java - это воспользоваться tzupdater c ключом -V
java -jar tzupdater.jar -V
Результатом исполнения будет что-то подобное:
tzupdater version 2.0.3-b01 JRE tzdata version: tzdata2016d tzupdater tool would update with tzdata version: tzdata2015b
Следует понимать, какой именно экземпляр виртуальной машины при этом вызывается на исполнение (и контролируется)! Под windows, например, их может быть несколько штук в разных местах - там, где JRE 32x, где JRE 64x, и еще там где JDK, если он установлен; не говоря уж о более экзотических инсталляциях.
Восполнение tzdata до актуальной версии делается следующим образом (запуск производится под правами администратора системы!):
java -jar tzupdater.jar -l https://www.iana.org/time-zones/repository/releases/tzdata2016d.tar.gz -u
В строке запуска, соответственно, указывается конкретный файл содержимого временных зон, до которого должно обновляться. В принципе tzupdater формально должен работать и с предварительно загруженным файлом, при этом якобы нужно просто напечатать в строке запуска file://имя_файла, но однако у меня он это делать не хотел ни в какую.
Сами данные содержатся в файле tzdb.dat, расположенном по относительному пути jre/lib/. Пикантность в том, что при каждом обновлении java база данных часовых поясов будет стёрта и заменена на неактуальную, соответствующую версии пакета обновления. Так что... Обновил яву - не забудь запустить tzupdater!
Описание взаимодействия языка программирования с датами и временем ограничим нашей задачей, сводящейся к следующему:
Здесь для общения с системой служит класс java.util.Date, большинство методов которого украшены пометкой "deprecated". Итак, Вывод текущей даты (заодно выведем временную зону, как её определила среда исполнения).
System.out.println(java.util.TimeZone.getDefault()); Date d=new Date(); System.out.println(d); ----------- sun.util.calendar.ZoneInfo[id="GMT+07:00",offset=25200000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null] Mon May 09 10:21:27 GMT+07:00 2016
Создаём Date из unix timestamp (не забываем, timestamp в java в миллисекундах, т.е. значение в секундах, умноженное на 1000:
d=new Date(1459022400000L); Date d2=new Date(1459022399000L); Date d1=new Date(d.getTime()-1); System.out.println(d+"\n"+d1+"\n"+d2);
...И убеждаемся в полном непотребстве: для работы учитывается строго текущее смещение часового пояса и история временных зон не работает. О чём недвусмысленно сообщает и строка "transitions=0" в результатах предшествующего кода
Sun Mar 27 03:00:00 GMT+07:00 2016 Sun Mar 27 02:59:59 GMT+07:00 2016 Sun Mar 27 02:59:59 GMT+07:00 2016
Однако, если вместо системной зоны поставить корректную зону из tzdb, показания времени в выводе изменятся на правильные, хотя смещение (идентификатор зоны) для нужного периода будут перевраны всё равно.
Sun Mar 27 03:00:00 GMT+07:00 2016 Sun Mar 27 01:59:59 GMT+07:00 2016 Sun Mar 27 01:59:59 GMT+07:00 2016Обратный парсинг можно сделать через статический deprecated метод Date.parse из строк, формат которых наблюдается в примере выше, но поскольку перевод годится только для текущих показателей часового пояса - действие малоактуальное.
Вердикт: для нашей цели использование данного интерфейса непригодно
В java.time представлены несколько классов для работы с датой и временем.
Итак, текущее время и его unix timestamp (временная метка):
Instant i=Instant.now(); System.out.println(i.getEpochSecond());
Перевод из unix timestamp в строку с необходимым нам форматом:
Instant i=Instant.ofEpochSecond(1459022399L); ZonedDateTime z3=i.atZone(ZoneId.of("Asia/Barnaul")); System.out.println(z3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh.mm.ss")));
Текущее время и его полное зонное представление в виде строки (заодно выводим и unix timestamp):
ZonedDateTime zd=ZonedDateTime.now(); System.out.println(zd+" "+zd.toEpochSecond());
Текущее время, его представление в виде строки со смещением и unix timestamp
OffsetDateTime of=OffsetDateTime.now(); System.out.println(of+" "+of.toEpochSecond());
Обратный парсинг из строки не столь тривиален. Нам надо преобразовать дату и время из строки "20160327 015959" в unix timestamp для зоны "Asia/Barnaul". Придётся сделать следующее:
LocalDateTime ld=LocalDateTime.parse("20160327 015959", DateTimeFormatter.ofPattern("yyyyMMdd HHmmss")); ZonedDateTime z=ld.atZone(ZoneId.of("Asia/Barnaul")); long unixts=z.toEpochSecond(); System.out.println(z+" "+unixts);
Можно пойти дальше и допреобразовываться до Instant, если это необходимо.
Обратите внимание: под Windows java определяет временную зону по идентификатору, берущемуся из реестра. Этот идентификатор не совпадает с идентификатором базы данных Олсона (и, как говорилось ранее, далеко не всегда соответствует реальной зоне для истории изменений - в нашем случае укажет на азиатский аналог GMT+7 временной зоны без истории и летнего времени). По данной причине, я бы не рекомендовал опираться на данные операционной системы и ссылаться на идентификацию зон исходной базы. Применение например ZoneId.systemDefault() вместо прямого указания зоны по Олсону даст в нашем последнем коде совершенно неправильные результаты, несмотря на правильную зону в Windows (если зона по умолчанию не была корректно предложена программе заранее).
Установка временной зоны по умолчанию для последующего кода программы вместо того, что хочется исполняющей среде (действует как на устаревший интерфейс, так и на новый):
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Barnaul"));
Напоследок. Я искал долго, и так и не нашел никакой возможности в коде определить версию базы tzdata, использующуюся средой исполнения. Прямой поиск залезть в файл tzdata.db и считать строку с соответствующим смещением (она там есть, да) - не в счёт, потому что ограничения, наложенные на виртуальную машину, скорее всего вам это сделать не дадут. Остаётся только создавать некую процедуру тестирования, которая будет сравнивать перевод в unix timestamp для нужных зон на границах замечательных периодов и извещать программу, когда этот перевод работает неправильно. Необходимость такого контроля я бы назвал обязательной, исходя из любви исполняющих сред java апгрейдиться автоматически или полуавтоматически. Напомню еще раз что при обновлении JRE базы tzdata обычно убиваются.
Текст опубликован: 2016-05-11
Последние изменения текста: 2016-05-11
Вы можете добавить свои комментарии.
Комментарий появится на сайте только после того, как он будет проверен администрацией на запрещённую законодательством информацию.
Для возможности внесения комментариев в браузере должна быть включена поддержка JavaScript. Реклама и ссылки на сайты, не относящиеся к делу, являются прямым основанием удаления комментария. Поля "E-mail" и "WWW" обязательными для заполнения не являются, поле E-Mail не публикуется. Если хотите просто что-то написать автору статьи, без публикации на сайте - воспользуйтесь специальной формой под пунктом меню "О сайте". Администрация оставляет за собой право публиковать или не публиковать адреса, введенные в поле www, а также при необходимости редактировать текст вашего сообщения. Ответы на ваши сообщения по введенному вами E-mail автоматически сайтом не высылаются. Теги PHPBB и HTML не действуют.