Сэр Серж aka Sir Serge (Сергей Лебедев) - official site
Статьи и заметкиРасчетыО сайте

Представление о времени в операционных системах и языках программирования

Вкратце о том, для чего и в приложении к какой задаче это написано

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

Если вы не понимаете, почему в принципе невозможно напрямую вычислять временные интервалы между отметками времени, выраженными в местном времени, во времени разных часовых поясов и между отметками времени, данные о часовых поясах для которых утрачены - то информацию стоит искать в местах иных.

Опуская всяческие рассуждения и размышления, имеем следующее:

  1. Для хранения отметок времени годится любая временная шкала, которую можно посчитать за абсолют, то есть обладающая непрерывностью числового ряда. На настоящее время такой шкалой будем считать Универсальное Тихоокеанское Время (UTC). Благодаря тому, что в этой временной зоне принципиально отсутствуют разные сезонные и политические корректировки (всякие там leap seconds и техническую разницу с GMT попросту игнорируем - для нашей прикладной задачи это не имеет значения).
  2. Самое простое и распространенное представление времени UTC - UNIX timestamp, выражающий интервал в секундах от 1970-01-01 00:00:00. Поэтому задачу сводим к трём маленьким подзадачам:
  3. Получить текущую отметку времени в исчислении UTC, сохранить её
  4. Сохранённую отметку времени преобразовать в UNIX timestamp (и обратно, если необходимо)
  5. Unix timestamp преобразовать в местное время (и обратно - местное время в Unix timestamp) с учётом всех сезонных переходов и истории изменения исчисления для политической точки, в которой работает наш компьютер, занимающийся расчетами. Для упрощения считаем, что он никогда не перемещается географически - иначе надо еще и иметь историю перемещения, чтобы получить какие-то адекватные данные - а это уже за пределами задачи.
  6. Относительно достоверным источником правил временных переходов (более того - единственным источником, заслуживающим доверия), к тому же напрямую технически привязанным к представлению данных о времени в Unix, будем считать так называемую "базу Олсона", которая содержится на iana.org

Поскольку представленное здесь - в основном для собственного упорядочения информации, то обширного охвата информации ожидать не стоит. Здесь нет, например, особенностей работы с временем для FreeBSD, MACOS и Android, всяких python, Ruby и VBA, динозавров типа Windows CE и Windows Mobile по банальной причине - в настоящее время они вне зоны моих интересов. Разве что можно отметить Android - в этой ОС программисту не стоит рассчитывать на поддержку временных зон от операционной системы, а программисту Java - на поддержку временных зон виртуальной машиной. И всё о нём.

Microsoft Windows

Версии до 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:

  1. В общем случае, инфрмация о временных зонах в реестре Windows содержится.
  2. Нет никаких гарантий, что имеющаяся в реестре информация о правилах соотношения сдвига времени UTC и локального времени будет правильно использована прикладными программами и что эти правила вообще учитываются за пределами текущего года. Отсюда стоит считать единственно достоверным соответствие перевода только текущей локальной даты в UTC. Для прошлого и будущего такой перевод может быть неправильным.
  3. Распространенным решением для пользователей Windows и администраторов некоторых windows-серверов является тупая перестановка часовых поясов на "подходящий", вообще без учёта какой либо истории.
  4. Более того, действия по предыдущему пункту являются рекомендованными техподдержкой MS. Коррекция правил перехода и временных зон не являются приоритетными обновлениями, как и внесение новых временных зон, поэтому всегда будут отложены до следующего 13-го числа, когда очередные обновления будут выданы для массового скачивания. Зона Asia/Barnaul была известна еще в середине марта 2016 года, однако внедрена будет только с 12 апреля. В каком виде - посмотрим.
  5. Учитывая сказанное выше, следует считать поддержку временных зон в Windows информацией, которой доверять нельзя, за исключением преобразования времени в UTC строго на момент запроса к таймеру ОС. Все остальные конверсии следует рассматривать недостоверными.

Время на файловых системах Windows

На файловой системе FAT16 и FAT32 время создания файлов хранится в виде поля word, отображающего абстрактную локальную дату. Соответственно, при разных настройках часовых поясов, для одного и того же файла будет отображена одна и та же дата, и одно и то же время, в целом не имеющие отношения к текущим показаниям часов, если файлы были порождены на компьютере с другой временной зоной

Файловая система NTFS хранит время создания и модификации файлов в UTC. Технически, однако, отсчёт ведётся от 1601 года в 100-миллисекундных интервалах. Видимо, потому что в 1601 году Средневековье впервые открыло для себя Окна ))

Linux, gcc/glibc

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();

Что делают вызовы остальных функции, понятно без дополнительных комментариев

Windows, Visual C/C++

Программа, приведенная в подразделе для 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

Время в 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>), поддерживает два варианта работы с базой временных зон:

  1. Настройка по умолчанию во всех инсталляциях - используется tzdata компьютера, на котором запущен PHP. Оттуда импортируются все поддерживаемые символические наименования временных зон и истории переходов. Корректность напрямую зависит от корректности поддержки tzdata конкретным экземпляром ОС.
  2. Через модуль timezonedb, содержащийся в репозитории pecl.php.net; С выходом новых версий баз данных на сайте IANA, код его аккуратно обновляется и подлежит ручному скачиванию и инсталляции. Обычный extension module для php. В windows процесс заключается в поиске нужного на http://pecl.php.net/package/timezonedb, скачивании, распаковывании нужных php_timezonedb.dll и php_timezonedb.pdb в ветку ext/ дерева каталогов php и прописывании в действующем php.ini строчки extension=..../php/ext/php_timezonedb.dll. После чего необходимо перезапустить все зависимые сервисы.

Временную зону по умолчанию можно задать в 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.

Вердикт и особые замечания для PHP

  1. Под windows при работе с PHP недопустимо опираться на базу данных временных зон операционной системы. Т.е., если требуется корректная работа с зонными датами, (а это де-факто - ВСЕГДА) - необходимо инсталлировать расширение timezonedb. Вариантов нет.
  2. Набор базовых функций php для работы с временем и датой довольно рудиментарен, однако полагаться на правильное преобразование между unix timestamp и строковым представлением даты/времени можно, с учётом правильности выставления tzdata для самого php.
  3. Поскольку внутреннее представление временных переменных - unix timestamp, вычисление корректных интервалов между двумя отметками времени в php возможно (в общем случае, хотя можно найти примеры, когда это не работает).
  4. При работе скрипта PHP с mySQL через интерфейсы mysql_ и mysqli_ все поля результатов запросов, имеющих отношение ко времени, приходят в виде строк. Поля timestamp тоже. О чём не стоит забывать.
  5. Изменил PHP или его конфигурацию - перезапусти Apache. Конфигурация модуля считывается единожды при старте сервера. Программы - апдейтеры unix-систем перезапуск зависимых сервисов делают самостоятельно, под windows - всё в руках хозяина сервера.

mySQL

(версии 5.х, состояние в предыдущих не отслеживалось)

Поддерживаются два вида работы с временными зонами:

  1. С использованием справочника временных зон операционной системы (всегда именно это включено по умолчанию)
  2. С использованием собственных справочников

Собственные справочники в исходной инсталляции 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                                                                                                         |
+-------------------------------------------------------------------------------------------------------------------+

Вердикт и особые замечания по mySQL

  1. Все функции вычисления промежутков времени в mySQL работают корректно только с датами во временном поясе UTC. Даже при правильных установках для других временных зон - лажают.
  2. Как бы там не заявлялось про внутреннее устройство полей timestamp, реальное пользовательское отличие их только в том, что они всегда представляют собой "внутри" дату в UTC. Однако, при каждой выборке, конвертируются в локальное время и все операции происходят именно над результатом преобразования.
  3. Остальные timedata не конвертируются. И правила текущей временной зоны mySQL к ним не применяет. Еще раз - вся арифметика с датами - только для UTC!
  4. В исследованиях выше не указано - но есть верный способ исказить все показания timestamp'ов: это утилита mysqldump. Казалось бы: поле внутреннего формата, выводи его в дамп в виде целого или какого-там-числа - но в дампе будет именно локальная дата со всеми вытекающими последствиями. Переводы там переводами, но "мёртвые" участки времени, связанные с "переводом стрелки назад", когда дважды в сутках одно и то же показание часов - будут испорчены гарантированно.
  5. Опять про mysqldump. На серверах экспорт и импорта будут разные временные зоны - и искажения времени вам гарантированны. Установка текущей временной зоны, кстати в дампе закомментировна. Почему ее не раскомментировать вручную - это сугубо технический вопрос, особенно странный, если в базе поля типа blob.
  6. Из сказанного выше происходит следствие: единственно гарантированная возможность сохранить timestamp неизменным в таблицах mysql - это не использовать для его хранения поля, так или иначе связанные с датами, а хранить целое число в поле типа int. И управляться с таковыми данными за рамками стандартных средств mySQL.
  7. Напоследок стоит напомнить опять: в результатах запросов данные в php передаются из полей, связанных с датами, в виде строки текста. Поэтому не стоит строить иллюзий о какой-то там сохранности и неизменности, в том числе при трансляции полей из одной базы в другую через скрипты php. Не берусь утверждать стопроцентно, но подозреваю, что ситуация строго сохраняется и для любых других интерфейсов с mySQL. На это наталкивают особенности поведения его собственного клиента командной строки.

.NET Framework и mono

Все примеры будут приведены на 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()
{
    // Получить все временные зоны от системы
    ReadOnlyCollection timeZones = 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;

    List adjustmentList = 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. Это здесь: (Раздельчик Общие сведения о часовых поясах)

Самое важное отсюда:

  1. .NET Framework полагается на сведения часовых поясов, предоставляемые операционной системой Windows и хранящиеся в реестре.
  2. Из-за большого количества часовых поясов в реестре представлены не все существующие часовые пояса.
  3. Реестр не обязательно содержит исторические данные часовых поясов
  4. В Windows XP реестр содержит данные только об одном наборе настроек часовых поясов (это они так обтекаемо хотят сказать, что в Windows XP в реестре достоверно представлена только запись, касающаяся текущего года; все даты за этим периодом не гарантируются. В отношении .NET такое поведение (imho) означает версию 2.0 и ниже.
  5. Windows Vista поддерживает динамические данные часовых поясов, что означает, что один часовой пояс может иметь несколько правил коррекции, которые применяются к конкретным интервалам лет.Однако большинство часовых поясов, которые определены в реестре Windows Vista и поддерживают летнее время, имеют только один или два предопределенных правила коррекции. (imho) поведение .NET версий 3.5-4.0 - невозможность задания сезонов без "мёртвых зон" якобы летнего времени.
  6. Зависимость класса TimeZoneInfo от реестра означает, что приложение, работающее с часовыми поясами, не может полагаться на то, что конкретный часовой пояс определен в реестре. (!!!)
  7. ...И чёрта с два вы сможете определить собственный часовой пояс через CreateCustomTimeZone (текст мой), если в нём содержатся сезоны, в которых нет истинного летнего времени. Почему - см. выше.

Однако, на десерт, оставлен пока грязный хак, позволяющий нагло обойти контроль начала и конца Правила Перехода и позволяющий воссоздать экзотические зоны из реестра, а при вдумчивом использовании - слепить свои собственные. Это методы 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

Во 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 и обратно возможен только для текущего момента времени и с условием, что программа в процессе выполнения не преодолевала точек перехода.

Oracle JAVA

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!

Описание взаимодействия языка программирования с датами и временем ограничим нашей задачей, сводящейся к следующему:

  1. Получить unix timestamp из текущих показаний часов компьютера;
  2. Получить unix timestamp из даты/времени, заданной строкой по определённому шаблону;
  3. Из unix timestamp получить время и дату в читабельном формате, желательно для произвольной временной зоны - но на худой конец сойдёт и текущая системная.

Oracle Java. Старый интерфейс работы с датой/временем

Здесь для общения с системой служит класс 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 из строк, формат которых наблюдается в примере выше, но поскольку перевод годится только для текущих показателей часового пояса - действие малоактуальное.

Вердикт: для нашей цели использование данного интерфейса непригодно

Oracle Java 8. Интерфейс java.time

В 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 обычно убиваются.

Perl

Неплохая статья представлена на Хабре по поводу работы в perl с временными зонами и преобразованиями. От дополнительного анализа представления времени для этого языка уклонюсь. В целом, базисные средства практически повторяют Си - корректность работы сильно зависит от корректности поддержки временных зон операционной системой. Да. Так и читать - под windows их не стоит использовать.

Текст опубликован: 2016-05-11

Последние изменения текста: 2016-05-11


Вы можете добавить свои комментарии.

Комментарий появится на сайте только после того, как он будет проверен администрацией на запрещённую законодательством информацию.

Для возможности внесения комментариев в браузере должна быть включена поддержка JavaScript. Реклама и ссылки на сайты, не относящиеся к делу, являются прямым основанием удаления комментария. Поля "E-mail" и "WWW" обязательными для заполнения не являются, поле E-Mail не публикуется. Если хотите просто что-то написать автору статьи, без публикации на сайте - воспользуйтесь специальной формой под пунктом меню "О сайте". Администрация оставляет за собой право публиковать или не публиковать адреса, введенные в поле www, а также при необходимости редактировать текст вашего сообщения. Ответы на ваши сообщения по введенному вами E-mail автоматически сайтом не высылаются. Теги PHPBB и HTML не действуют.


Ещё тексты по этой теме:

Отображение графики SVG, получаемой со скрипта на сайте (2019-04-29/2019-04-29)
64-битный компилятор MinGW + NetBeans 8.2. Установка под Windows 10 x64 (2017-11-06/2017-11-06)
Использование строк UnicodeString и маркированных кодовой страницей AnsiString/RawByteString в приложениях Lazarus/LCL (2012-12-14/2012-12-14)
Символы и строки в Unicode-версиях FreePascal (2012-07-19/2012-07-23)
Национальный вопрос в C/C++ (2011-12-09/2011-12-09)
Lazarus :: Resurrection :: прикладная кадаврология (2011-11-11/2011-11-11)
Халява, сэр. Бесплатные средства кроссплатформенной разработки (2010-01-11/2010-01-11)
Copyright © 2003-2023 by Sir Serge