вторник, 1 февраля 2011 г.

UTC and Local Time Conversion

Однажды мне понадобилась функция, которая пересчитывает дату/время из UTC в локальное время. Ничего вразумительного в сети найти мне не удалось, поэтому пришлось открыть справку (win32.hlp) и написать функцию самостоятельно…

Итак, код функций. Преобразование времени UTC к локальному времени и обратно, с учётом Windows-настроек локального GMT-смещения и правилами перехода на летнее время.

unit DelphiNotesUTC;

interface

// Преобразование времени UTC к локальному времени и обратно
// с учётом Windows-настроек локального GMT-смещения и правилами перехода на летнее время
function UTCToLocalTime(AValue: TDateTime): TDateTime;
function LocalTimeToUTC(AValue: TDateTime): TDateTime;

implementation

uses
  SysUtils, Windows;

function UTCToLocalTime(AValue: TDateTime): TDateTime;
// AValue - время UTC
// Result - время с учётом локального GMT-смещения и правилами перехода на летнее время
var
  ST1, ST2: TSystemTime;
  TZ: TTimeZoneInformation;
begin
  // TZ - локальные настройки Windows
  GetTimeZoneInformation(TZ);

  // Преобразование TDateTime к WindowsSystemTime
  DateTimeToSystemTime(AValue, ST1);

  // Применение локальных настроек ко времени
  SystemTimeToTzSpecificLocalTime(@TZ, ST1, ST2);

  // Приведение SystemTime к TDateTime
  Result := SystemTimeToDateTime(ST2);
end;

function LocalTimeToUTC(AValue: TDateTime): TDateTime;
// AValue - локальное время
// Result - время UTC
var
  ST1, ST2: TSystemTime;
  TZ: TTimeZoneInformation;
begin
  // TZ - локальные (Windows) настройки
  GetTimeZoneInformation(TZ);
  // т.к. надо будет делать обратное преобразование - инвертируем bias
  TZ.Bias := -TZ.Bias;
  TZ.StandardBias := -TZ.StandardBias;
  TZ.DaylightBias := -TZ.DaylightBias;

  DateTimeToSystemTime(AValue, ST1);

  // Применение локальных настроек ко времени
  SystemTimeToTzSpecificLocalTime(@TZ, ST1, ST2);

  // Приведение WindowsSystemTime к TDateTime
  Result := SystemTimeToDateTime(ST2);
end;

end.

Пример использования.

var
  D: TDateTime;
begin
  Memo1.Lines.Clear;
  D := EncodeDate(2011, 1, 1) + Frac(Now);
  Memo1.Lines.Add(DateTimeToStr(D));                    // 01.01.2011 19:53:04
  Memo1.Lines.Add(DateTimeToStr(LocalTimeToUTC(D)));    // 01.01.2011 16:53:04
  Memo1.Lines.Add(DateTimeToStr(UTCToLocalTime(D)));    // 01.01.2011 22:53:04

  D := EncodeDate(2011, 6, 1) + Frac(Now);
  Memo1.Lines.Add(DateTimeToStr(D));                    // 01.06.2011 19:53:04
  Memo1.Lines.Add(DateTimeToStr(LocalTimeToUTC(D)));    // 01.06.2011 15:53:04
  Memo1.Lines.Add(DateTimeToStr(UTCToLocalTime(D)));    // 01.06.2011 23:53:04
end;

Чтобы понять суть работы функций, приведу такой пример. Допустим у Вас есть удалённый сервер, на нём часы выровнены по UTC, т.е. в независимости от того, где находится сервер, он оперирует временем по Гринвичу. На сервере происходят какие-то события и он их протоколирует.

И есть Ваша машина, на которой настроено локальное время (для Москвы/Питера – это часовой пояс UTC+03:00, ну и плюс не забываем про зимнее/летнее время). Допустим, сервер возвращает Вам информацию, что в 17:00 произошло такое-то событие, а Вам надо понять, где Вы были в это время (в офисе, или уже по дороге домой). Можно конечно посчитать в голове: прибавить 3 часа (или 4, в зависимости от даты)… а если событий много? А если я вот не помню, когда осуществляется отмена зимнего времени?

Вобщем, для решения схожих проблем и были написаны приведённые функции.

P.S.: Кстати у этих функций есть особенность: если у Вас в настройках даты и времени ОС снят флаг "Автоматический переход на летнее время и обратно", то эти функции не будут учитывать правила перехода на летнее время. Хорошо это, или плохо – зависит от ситуации…

14 коммент.:

GunSmoker комментирует...

Кстати, в JclDateTime это:

function DateTimeToLocalDateTime(DateTime: TDateTime): TDateTime;
и
function LocalDateTimeToDateTime(DateTime: TDateTime): TDateTime;

Реализованы примерно так:

function DateTimeToLocalDateTime(DateTime: TDateTime): TDateTime;
var
TimeZoneInfo: TTimeZoneInformation;
begin
ResetMemory(TimeZoneInfo, SizeOf(TimeZoneInfo));
case GetTimeZoneInformation(TimeZoneInfo) of
TIME_ZONE_ID_STANDARD, TIME_ZONE_ID_UNKNOWN:
Result := DateTime - (TimeZoneInfo.Bias + TimeZoneInfo.StandardBias) / MinutesPerDay;
TIME_ZONE_ID_DAYLIGHT:
Result := DateTime - (TimeZoneInfo.Bias + TimeZoneInfo.DaylightBias) / MinutesPerDay;
else
raise EJclDateTimeError.CreateRes(@RsMakeUTCTime);
end;
end;

Николай Зверев комментирует...

Да, не надо инвертировать делфовое время в виндовое и обратно. Плюс я не учитываю, что системные вызовы могут вернуть ошибку.

Роман комментирует...

У функции из JclDateTime есть один ОГРОМНЫЙ недостаток: ее нельзя использовать для произвольного времени, только для текущего.

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

Т.е. если сейчас зима, а вы подадите в функцию июльское UTC время, то она выдаст неверный результат.

Николай Зверев комментирует...

Роман, спасибо за комментарий.

Я этот момент, кстати, тестировал, и функция SystemTimeToTzSpecificLocalTime учитывает именно передаваемую в функцию дату, и именно от передаваемой даты она смотрит, использовать ли зимнее/летнее время.

P.S.: обновил пример в заметке.

Анонимный комментирует...

// т.к. надо будет делать обратное преобразование - инвертируем bias
TZ.Bias := -TZ.Bias;
TZ.StandardBias := -TZ.StandardBias;
TZ.DaylightBias := -TZ.DaylightBias;
...
SystemTimeToTzSpecificLocalTime(@TZ, ST1, ST2);

Есть же TzSpecificLocalTimeToSystemTime, зачем такой огород

Николай Зверев комментирует...

Есть же TzSpecificLocalTimeToSystemTime

Да, сейчас такая функция есть, спасибо.

Но на момент написания кода - такой функции не было, она появилась в WindowsXP (а первая - в Windows2000), и, соответсвенно, её объявления нет в Windows.pas Delphi7.
На сегодня, конечно же, лучше использовать TzSpecificLocalTimeToSystemTime, чем приведёный "огород". Ещё раз спасибо за внимательность.

Сергей комментирует...

А кто заботится о том, что в России отменили зимнее время - все эти (и не только, а давно прописанные в миллионы строк кода) преобразования в с ноября этого года будут врать на час.

Николай Зверев комментирует...

Операционная система об этом позаботится. Вы же не забываете обновлять Windows?

Правда тут останется проблема со старыми датами, поэтому в Windows Vista появилась функция GetTimeZoneInformationForYear, можно использовать её.

Сергей комментирует...

Microsoft официально прекратил поддержку XP с лета прошлого года, а Windows 7, при всем уважении, не для всех компьютеров. А что касается новых функций, да, на будущее учтем,но использовать GetTimeZoneInformationForYear для ранее написанных программ проблематично.

Сергей комментирует...

Я просто не знаю, как без особых хлопот обратить внимание широких кругов на эту проблему - а проблема есть. Может масштаб не тот, что у "проблемы 2000 года", но иметь последствия может. Хотя бы знающие люди заверьте меня в том, что серьезные программисты, пишущие важные программы связанные с вопросами времени, не используют системное его представление - а может они не задумываются что его используют. Я сам только недавно понял что можно нарваться, когда попытался посчитать дни прибавлением 24 часов выраженных в миллисекундах на рубеже зимнего времени.

Николай Зверев комментирует...

Сергей, если для Вас это серъёзная проблема, то могу предложить такой вариант.
1. Почитать http://msdn.microsoft.com/en-us/library/ms724253(v=VS.85).aspx (раздел Remarks)
2. Реализовать свою версию процедуры GetTimeZoneInformationForYear для ОС ниже Vista SP1 Процедура будет сведена к двум вещам:
а) определение текущей зоны, выбранной в настройках ОС
б) чтение из реестра ветки, соответствующей выбранной зоне.
3. Когда выйдет обновление для Windows Vista/7, учитывающее изменения в правилах перевода часов в России, скопировать эту часть ветки реестра и предложить её пользователям Windows XP.

P.S.: Лично для меня это _пока_ не проблема, хотя я думал об этом, когда президент отменял эти переводы. Если вопрос станет на повестку дня - то я бы пошёл описанным путём.
Но, скорее всего, мы посоветуем пользователям отключить флаг "автоматический переход на летнее время и обратно" (если они этого ещё не сделали сами). А погрешность в 1 час за прошлый год (и предыдущие) - это вполне терпимо (хотя, согласен, есть виды деятельности, где такое недопустимо).

Анонимный комментирует...

Вот так и задумаешься, что надо всё и вся привязывать к UTC, а локальное время юзать только для отображения.

Николай Зверев комментирует...

Кстати, для Windows XP вышло обновление (и кстати своевременное), которое отменило перевод часов назад в этом году.
Кстати, у нас ведь GMT смещение +3 часа всегда было (зимой +3, а летом +4 часа), а теперь GMT смещение стало +4 часа. Немного не привычно..

Анонимный комментирует...

Обнаружил проблему. Если на компьютере выбрана временная зона "(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London" и передано "летнее" время, то функция LocalTimeToUTC вернет неправильное значение (со сдвигом на час вперед, а должно быть назад). Почему-то в таких условиях SystemTimeToTzSpecificLocalTime прокалывается.
Возможно это зависит от версии системы, под WinXP SP3 четко воспроизводится.

Отправить комментарий

.

.