5

I need UTC variants of the functions DateTimeToUnix and UnixToDateTime, so a Chinese customer is able to interact with the server in Germany. Both sides should be able to exchange Unix timestamps (in UTC, without DST) and be able to communicate through this way.

In a bugreport of HeidiSQL , users discussed that DateTimeToUnix and UnixToDateTime do not care about the time zone, and there I have found following code:

function DateTimeToUTC(dt: TDateTime): Int64;
var
  tzi: TTimeZoneInformation;
begin
  Result := DateTimeToUnix(dt);
  GetTimeZoneInformation(tzi);
  Result := Result + tzi.Bias * 60;
end;

MSDN explains twi.Bias as follows:

All translations between UTC time and local time are based on the following formula:

UTC = local time + bias

The bias is the difference, in minutes, between UTC time and local time.

This sounds logical, but since I was unsure if the code above was correct, I made following program to check it:

// A date in summer time (DST)
Memo1.Lines.add('1401494400'); // 31 May 2014 00:00:00 GMT according to http://www.epochconverter.com/
Memo1.Lines.add(inttostr(DateTimeToUnixUTC(StrToDate('31.05.2014'))));

// A date in winter time
Memo1.Lines.add('567302400'); // 24 Dec 1987 00:00:00 GMT according to http://www.epochconverter.com/
Memo1.Lines.add(inttostr(DateTimeToUnixUTC(StrToDate('24.12.1987'))));

The output in Germany (GMT+1+DST) is currently:

1401494400
1401490800
567302400
567298800

I expected the output being:

1401494400
1401494400
567302400
567302400

What am I doing wrong?

PS: For this project I am bound to Delphi 6.

Daniel Marschall
  • 3,739
  • 2
  • 28
  • 67
  • I'm not sure if I understand fully, but what you're doing in last line of that function is adding your timezone bias to result? If you commend out that line, it will return proper UTC times. – rsrx May 31 '14 at 10:37
  • if i comment out that line, the test output will be equal, but then the DateTimeToUnixUTC (Now) will be different in Germany and China. I am really confused. – Daniel Marschall May 31 '14 at 10:47
  • You need to check function return value, esp. when working with this function. – Free Consulting May 31 '14 at 11:13
  • I didn't understand. In my first program version I used DateTimeToUnix(Now) and the Chinese customer had totally different timestamps than the German server. The same issue that was discussed in the link shown above. They said too, that Delphi's function does not care about UTC. – Daniel Marschall May 31 '14 at 11:24
  • BTW: This `DateTimeToUTC()` function completely ignores daylight saving time (that's why the difference in your example is the same, DST or not). Note that `GetTimeZoneInformation()` returns information whether DST is in effect at the time of calling, but if you pass a different datetime you will need to calculate yourself whether that falls into the range where DST is in effect. – mghie May 31 '14 at 11:25
  • Actually, I do only need two functions: one shows the current time in unix time in UTC (without DST ofc) and another function which turns such a UTC linux timestamp into the users locale string representation. – Daniel Marschall May 31 '14 at 11:40
  • You need to separate time zone conversion from TDateTime/Unix time conversions. Try get get a clear picture of what time zone any value is relative to. – David Heffernan May 31 '14 at 12:10

4 Answers4

5

You have already found DateTimeToUnix and UnixToDateTime. So that part of the conversion is taken care of.

All you need to do now is convert between local and UTC time. You can do that using DateUtils.TTimeZone class. Specifically DateUtils.TTimeZone.ToUniversalTime and DateUtils.TTimeZone.ToLocalTime.

These four functions give you all that you need.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thank you very much for this solution. Alas, my version of Delphi does not have TTimeZone. I need to stay compatible with Delphi 6 for this project :-/ Is there any easy workaround? – Daniel Marschall May 31 '14 at 14:36
  • Try the code here: http://stackoverflow.com/questions/15567194/how-to-convert-local-time-to-utc-time-in-delphi-xe2-and-how-to-convert-it-back – David Heffernan May 31 '14 at 14:47
  • Ok, I have understood that TDateTime/Unixtime has nothing to do with GMT and DST - The conversation needs to be taken before resp. after. I tried your function. `DateTimeToStr(LocalDateTimeFromUTCDateTime(UnixToDateTime(1401494400)))` will be `31 May 2014 02:00:00` which is correct. But `DateTimeToStr(LocalDateTimeFromUTCDateTime(UnixToDateTime(567302400)))` will show `24 Dec 1987 02:00:00` which is not correct (or the timestamp from epochconverter.com is wrong), since December has DST=0, so GMT+1+DST should be +1 and not +2. – Daniel Marschall May 31 '14 at 17:12
  • (I understand that 24 Dec 1987 02:00 is the time based on my CURRENT GMT-Bias + DST, but of course it is necessary to know, what time it was when the timestamp was captured. When working with timestamps of the past, the user needs to know, when he e.g. edited his post. And it is not OK if there is standing "2:00" when it was "1:00" on his clock, when he edited something during winter time) – Daniel Marschall May 31 '14 at 17:18
  • The blog http://goo.gl/t94UNi explains the behavior: "This technique doesn't work in general because System­Time­To­Tz­Specific­Local­Time uses the time zone in effect at the time being converted, whereas the File­Time­To­Local­File­Time function uses the time zone in effect right now. Furthermore, it doesn't take into account changes in daylight savings rules that may have historically been different from the current set of rules. (Though this is easily repaired by switching to System­Time­To­Tz­Specific­Local­Time­Ex.) The trick works here because the time we are converting is right now." – Daniel Marschall May 31 '14 at 18:17
  • I don't understand what you mean by those comments. Perhaps you need to work out a precise specification of your problem. – David Heffernan May 31 '14 at 18:19
  • 2
    Your solution http://stackoverflow.com/a/15567387/3544341 is not correct. When the UTC unix time was 567302400, on my clock was 1:00 because we had winter time. But your function outputs 2:00 . However, I have found 2 other solutions, which show the correct time (see my own answer). These solutions are TzSpecificLocalTimeToSystemTime (which requires WinXP+), or a Delphi workaround for older Windows versions, or TTimeZone which also works fine, but not for my special case because I am bound to Delphi 6 for this project. – Daniel Marschall May 31 '14 at 19:05
  • @rinntech You are confusing "not correct" with "does not solve my problem". Note also that your question does not state precisely what conversion to do, and in any case my answer gives you a solution that does happen to meet your needs. It's also no use adding on constraints like Delphi 6 after the event. – David Heffernan May 31 '14 at 21:18
  • Ok, it was my error that I forgot to mention Delphi 6 in my initial question. Because of that, and since the `TTimeZone` functions are working fine, and because you have helped me understanding that `TDateTime` and Unix-Timestamps are timezone-less in Delphi, I will mark your answer als solution. (Note: I still think that this is a mess in Delphi, because the Unix-Time is defined as Jan 1st 1970 0:00:00 **UTC** , so Delphi is providing no real Unix-Timestamps using `DateTimeToUnix`, which confused me in the beginning) – Daniel Marschall May 31 '14 at 23:14
5

I think I have found some solutions for my question. All 3 solutions gave the same output, but I will try to find out which one is best and I will test it on several machines with different locales.

Solution #1 using TzSpecificLocalTimeToSystemTime and SystemTimeToTzSpecificLocalTime works fine, but requires Windows XP and above:

(Source: https://stackoverflow.com/a/15567777/3544341 , modified)

// Statically binds Windows API functions instead of calling them dynamically.
// Requires Windows XP for the compiled application to run.
{.$DEFINE USE_NEW_WINDOWS_API}

{$IFDEF USE_NEW_WINDOWS_API}
function SystemTimeToTzSpecificLocalTime(lpTimeZoneInformation: PTimeZoneInformation; var lpUniversalTime,lpLocalTime: TSystemTime): BOOL; stdcall; external kernel32 name 'SystemTimeToTzSpecificLocalTime';
{$ELSE}
function SystemTimeToTzSpecificLocalTime(lpTimeZoneInformation: PTimeZoneInformation; var lpUniversalTime,lpLocalTime: TSystemTime): BOOL; stdcall;
var
  h: HModule;
  f: function(lpTimeZoneInformation: PTimeZoneInformation; var lpUniversalTime,lpLocalTime: TSystemTime): BOOL; stdcall;
begin
  h := LoadLibrary(kernel32);
  if h = 0 then RaiseLastOSError;

  @f := GetProcAddress(h, 'SystemTimeToTzSpecificLocalTime');
  if @f = nil then RaiseLastOSError;

  result := f(lpTimeZoneInformation, lpUniversalTime, lpLocalTime);
end;
{$ENDIF}

{$IFDEF USE_NEW_WINDOWS_API}
function TzSpecificLocalTimeToSystemTime(lpTimeZoneInformation: PTimeZoneInformation; var lpLocalTime, lpUniversalTime: TSystemTime): BOOL; stdcall; external kernel32 name 'TzSpecificLocalTimeToSystemTime';
{$ELSE}
function TzSpecificLocalTimeToSystemTime(lpTimeZoneInformation: PTimeZoneInformation; var lpLocalTime, lpUniversalTime: TSystemTime): BOOL; stdcall;
var
  h: HModule;
  f: function(lpTimeZoneInformation: PTimeZoneInformation; var lpLocalTime, lpUniversalTime: TSystemTime): BOOL; stdcall;
begin
  h := LoadLibrary(kernel32);
  if h = 0 then RaiseLastOSError;

  @f := GetProcAddress(h, 'TzSpecificLocalTimeToSystemTime');
  if @f = nil then RaiseLastOSError;

  result := f(lpTimeZoneInformation, lpLocalTime, lpUniversalTime);
end;
{$ENDIF}

function UTCToLocalDateTime_WinXP(d: TDateTime): TDateTime;
var
  TZI: TTimeZoneInformation;
  LocalTime, UniversalTime: TSystemTime;
begin
  GetTimeZoneInformation(tzi);
  DateTimeToSystemTime(d,UniversalTime);
  SystemTimeToTzSpecificLocalTime(@tzi,UniversalTime,LocalTime);
  Result := SystemTimeToDateTime(LocalTime);
end;

function LocalDateTimeToUTC_WinXP(d: TDateTime): TDateTime;
var
  TZI: TTimeZoneInformation;
  LocalTime, UniversalTime: TSystemTime;
begin
  GetTimeZoneInformation(tzi);
  DateTimeToSystemTime(d,LocalTime);
  TzSpecificLocalTimeToSystemTime(@tzi,LocalTime,UniversalTime);
  Result := SystemTimeToDateTime(UniversalTime);
end;

Solution #2 as workaround for older operating systems does also work fine:

(Source: http://www.delphipraxis.net/299286-post4.html )

uses DateUtils;

function GetDateTimeForBiasSystemTime(GivenDateTime: TSystemTime; GivenYear: integer): TDateTime;
var
  Year, Month, Day: word;
  Hour, Minute, Second, MilliSecond: word;
begin
  GivenDateTime.wYear := GivenYear;
  while not TryEncodeDayOfWeekInMonth(GivenDateTime.wYear, GivenDateTime.wMonth, GivenDateTime.wDay, GivenDateTime.wDayOfWeek, Result) do
    Dec(GivenDateTime.wDay);

  DecodeDateTime(Result, Year, Month, Day, Hour, Minute, Second, MilliSecond);
  Result := EncodeDateTime(Year, Month, Day, GivenDateTime.wHour, GivenDateTime.wMinute, GivenDateTime.wSecond, GivenDateTime.wMilliseconds);
end;

function GetBiasForDate(GivenDateTime: TDateTime): integer;
var
  tzi: TIME_ZONE_INFORMATION;
begin
  GetTimeZoneInformation(tzi);
  if (GivenDateTime < GetDateTimeForBiasSystemTime(tzi.StandardDate, YearOf(GivenDateTime))) and
     (GivenDateTime >= GetDateTimeForBiasSystemTime(tzi.DaylightDate, YearOf(GivenDateTime))) then
    Result := (tzi.Bias + tzi.DaylightBias) * -1
  else
    Result := (tzi.Bias + tzi.StandardBias) * -1;
end;

function UTCToLocalDateTime_OldWin(aUTC: TDateTime): TDateTime;
begin
  Result := IncMinute(aUTC, GetBiasForDate(aUTC));
end;

function LocalDateTimeToUTC_OldWin(aLocal: TDateTime): TDateTime;
begin
  Result := IncMinute(aLocal, GetBiasForDate(aLocal) * -1);
end;

Solution #3 using TTimeZone for users of newer versions of Delphi, does give the same results as the codes above:

(Solution by David Heffernan, alas not possible in my current project, because I am bound to Delphi 6)

uses DateUtils;

{$IF Declared(TTimeZone)}
function UTCToLocalDateTime_XE(aUTC: TDateTime): TDateTime;
begin
  result := TTimeZone.Local.ToLocalTime(aUTC);
end;

function LocalDateTimeToUTC_XE(aLocal: TDateTime): TDateTime;
begin
  result := TTimeZone.Local.ToUniversalTime(aLocal);
end;
{$IFEND}

Now we can put all 3 solutions together! :-)

function UTCToLocalDateTime(aUTC: TDateTime): TDateTime;
begin
  {$IF Declared(UTCToLocalDateTime_XE)}
  result := UTCToLocalDateTime_XE(aUTC);
  {$ELSE}
    {$IFDEF USE_NEW_WINDOWS_API}
    result := UTCToLocalDateTime_WinXP(aUTC);
    {$ELSE}
    try
      result := UTCToLocalDateTime_WinXP(aUTC);
    except
      on E: EOSError do
      begin
        // Workaround for Windows versions older than Windows XP
        result := UTCToLocalDateTime_OldWin(aUTC);
      end
      else raise;
    end;
    {$ENDIF}
  {$IFEND}
end;

function LocalDateTimeToUTC(aLocal: TDateTime): TDateTime;
begin
  {$IF Declared(LocalDateTimeToUTC_XE)}
  result := LocalDateTimeToUTC_XE(aLocal);
  {$ELSE}
    {$IFDEF USE_NEW_WINDOWS_API}
    result := LocalDateTimeToUTC_WinXP(aLocal);
    {$ELSE}
    try
      result := LocalDateTimeToUTC_WinXP(aLocal);
    except
      on E: EOSError do
      begin
        // Workaround for Windows versions older than Windows XP
        result := LocalDateTimeToUTC_OldWin(aLocal);
      end
      else raise;
    end;
    {$ENDIF}
  {$IFEND}
end;

An easy method to get the current UTC unix timestamp is

function NowUTC: TDateTime;
var
  st: TSystemTime;
begin
  GetSystemTime(st);
  result := EncodeDateTime(st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
end;

function CurrentUnixUTCTimestamp: int64;
begin
  result := DateTimeToUnix(NowUTC);
end;
Community
  • 1
  • 1
Daniel Marschall
  • 3,739
  • 2
  • 28
  • 67
0

DateTimeToUnix and UnixToDateTime have got a second argument now:

function DateTimeToUnix(const AValue: TDateTime; AInputIsUTC: Boolean): Int64;
function UnixToDateTime(const AValue: Int64; AReturnUTC: Boolean): TDateTime;

So, you can easily choose between UTC and local time.

КуЪ
  • 101
  • 2
0

Using kbmMW's TkbmMWDateTime class it is very easy as it is always timezone aware:

var
  dt:TkbmMWDateTime;
  unix:int64;
begin
   dt:=TkbmMWDateTime.Now;
   unix:=dt.UTCSinceEpoch;
end;

And it also goes the other way around. In fact there are many such epoch variations and time formats supported in TkbmMWDateTime.

I would recommend, if you any place need to exchange a string with date/time info in it, to use ISO8601 format. In kbmMW you do like this:

var
  s:string;
begin
  s:=TkbmMWDateTime.Now.ISO8601String;
...
end;

It also goes two ways.

You can read a bit more about kbmMW's DateTime handling here:

https://components4developers.blog/2018/05/25/kbmmw-features-3-datetime/

kbmMW is a toolbox that fully supports Delphi including all platforms.

Kim Madsen
  • 242
  • 2
  • 2