Однажды мне понадобилась функция, которая пересчитывает дату/время из 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 коммент.:
Кстати, в 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 четко воспроизводится.
Отправить комментарий