I'm trying to see if WindAPIs that convert from local to UTC time and vice versa are Daylight Saving Time accurate. For instance, let's take LocalFileTimeToFileTime API. Its description states:
LocalFileTimeToFileTime uses the current settings for the time zone and daylight saving time. Therefore, if it is daylight saving time, this function will take daylight saving time into account, even if the time you are converting is in standard time.
So I'm testing it with this code:
//Say, if DST change takes place on Mar-8-2015 at 2:00:00 AM
//when the clock is set 1 hr forward
//Let's check the difference between two times:
SYSTEMTIME st1_local = {2015, 3, 0, 8, 1, 30, 0, 0}; //Mar-8-2015 1:30:00 AM
SYSTEMTIME st2_local = {2015, 3, 0, 8, 3, 30, 0, 0}; //Mar-8-2015 3:30:00 AM
//Convert to file-time format
FILETIME ft1_local, ft2_local;
VERIFY(::SystemTimeToFileTime(&st1_local, &ft1_local));
VERIFY(::SystemTimeToFileTime(&st2_local, &ft2_local));
//Then convert from local to UTC time
FILETIME ft1_utc, ft2_utc;
VERIFY(::LocalFileTimeToFileTime(&ft1_local, &ft1_utc));
VERIFY(::LocalFileTimeToFileTime(&ft2_local, &ft2_utc));
//Get the difference
LONGLONG iiDiff100ns = (((LONGLONG)ft2_utc.dwHighDateTime << 32) | ft2_utc.dwLowDateTime) -
(((LONGLONG)ft1_utc.dwHighDateTime << 32) | ft1_utc.dwLowDateTime);
//Convert from 100ns to seconds
LONGLONG iiDiffSecs = iiDiff100ns / 10000000LL;
//I would expect 1 hr
ASSERT(iiDiffSecs == 3600); //But I get 7200, which is 2 hrs!
So what am I missing here?
SystemTimeToFileTime()
interprets its first argument as a UTC time (which has no concept of DST), so your ft1_local
and ft2_local
objects will always be two hours apart since you're changing the data format, but not the actual point in time. LocalFileTimeToFileTime()
will then apply the same offset to whatever you pass to it, so ft1_utc
and ft2_utc
will always end up two hours apart, also.
As the documentation says, "LocalFileTimeToFileTime
uses the current settings for the time zone and daylight saving time" (emphasis mine), so if at the current time you're four hours behind UTC, for instance, it'll just deduct four hours from whatever time you pass to it, regardless of whether that time originally represented some time at the other side of DST.
EDIT: Per the comments, here's how you'd get the seconds difference between two local times in standard C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
struct tm start_time;
start_time.tm_year = 115;
start_time.tm_mon = 2;
start_time.tm_mday = 8;
start_time.tm_hour = 1;
start_time.tm_min = 30;
start_time.tm_sec = 0;
start_time.tm_isdst = -1;
struct tm end_time;
end_time.tm_year = 115;
end_time.tm_mon = 2;
end_time.tm_mday = 8;
end_time.tm_hour = 3;
end_time.tm_min = 30;
end_time.tm_sec = 0;
end_time.tm_isdst = -1;
time_t start_tm = mktime(&start_time);
time_t end_tm = mktime(&end_time);
if ( start_tm == -1 || end_tm == -1 ) {
fputs("Couldn't get local time.", stderr);
exit(EXIT_FAILURE);
}
double seconds_diff = difftime(end_tm, start_tm);
printf("There are %.1f seconds difference.\n", seconds_diff);
return EXIT_SUCCESS;
}
which outputs:
paul@thoth:~/src$ ./difftime
There are 3600.0 seconds difference.
paul@thoth:~/src$
as you're expecting.
Note that, with struct tm
:
tm_year
is expressed in years since 1900, so to get 2015 we write 115
tm_mon
is in the range 0 though 11, so March is 2, not 3.
The other time members are as you'd expect
When tm_isdst
is set to -1
, mktime()
will attempt to find out for itself whether DST was in effect at the local time we supplied, which is what we want it to do, here.
In despite of all the beauty of Paul Griffiths' solution, I can't use it due to an apparent locale limitation. (C is obviously showing its age.) So I had to go with a pure WinAPI approach. Next is what I came up with. Correct me if I'm wrong (especially people with access to time zones other than the US one that Microsoft's mktime
seems to be favoring):
SYSTEMTIME st1 = {2015, 3, 0, 8, 1, 30, 0, 0}; //Mar-8-2015 1:30:00 AM
SYSTEMTIME st2 = {2015, 3, 0, 8, 3, 30, 0, 0}; //Mar-8-2015 3:30:00 AM
LONGLONG iiDiffNs;
if(GetLocalDateTimeDifference(&st1, &st2, &iiDiffNs))
{
_tprintf(L"Difference is %.02f sec\n", (double)iiDiffNs / 1000.0);
}
else
{
_tprintf(L"ERROR (%d) calculating the difference.\n", ::GetLastError());
}
Then this is the actual implementation. One important aspect to note here is that the method below may not work reliably on Windows XP due to the lack of APIs to retrieve time zone info for a specific year.
Some declarations first:
enum DST_STATUS{
DST_ERROR = TIME_ZONE_ID_INVALID, //Error
DST_NONE = TIME_ZONE_ID_UNKNOWN, //Daylight Saving Time is NOT observed
DST_OFF = TIME_ZONE_ID_STANDARD, //Daylight Saving Time is observed, but the system is currently not on it
DST_ON = TIME_ZONE_ID_DAYLIGHT, //Daylight Saving Time is observed, and the system is currently on it
};
#define FILETIME_TO_100NS(f) (((LONGLONG)f.dwHighDateTime << 32) | f.dwLowDateTime)
BOOL GetLocalDateTimeDifference(SYSTEMTIME* pStBegin_Local, SYSTEMTIME* pStEnd_Local, LONGLONG* pOutDiffMs = NULL);
BOOL ConvertLocalTimeToUTCTime(SYSTEMTIME* pSt_Local, SYSTEMTIME* pOutSt_UTC = NULL);
DST_STATUS GetDSTInfoForYear(USHORT uYear, TIME_ZONE_INFORMATION* pTZI = NULL);
And the implementation:
BOOL GetLocalDateTimeDifference(SYSTEMTIME* pStBegin_Local, SYSTEMTIME* pStEnd_Local, LONGLONG* pOutDiffMs)
{
//Calculate difference between two local dates considering DST adjustments between them
//INFO: May not work correctly on Windows XP for a year other than the current year!
//'pStBegin_Local' = local date/time to start from
//'pStEnd_Local' = local date/time to end with
//'pOutDiffMs' = if not NULL, receives the difference in milliseconds (if success)
//RETURN:
// = TRUE if success
// = FALSE if error (check GetLastError() for info)
BOOL bRes = FALSE;
LONGLONG iiDiffMs = 0;
int nOSError = NO_ERROR;
if(pStBegin_Local &&
pStEnd_Local)
{
//Convert both dates to UTC
SYSTEMTIME stBeginUTC;
if(ConvertLocalTimeToUTCTime(pStBegin_Local, &stBeginUTC))
{
SYSTEMTIME stEndUTC;
if(ConvertLocalTimeToUTCTime(pStEnd_Local, &stEndUTC))
{
//Then convert into a more manageable format: FILETIME
//It will represent number of 100-nanosecond intervals since January 1, 1601 for each date
FILETIME ftBeginUTC;
if(::SystemTimeToFileTime(&stBeginUTC, &ftBeginUTC))
{
FILETIME ftEndUTC;
if(::SystemTimeToFileTime(&stEndUTC, &ftEndUTC))
{
//Now get the difference in ms
//Convert from 100-ns intervals = 10^7, where ms = 10^3
iiDiffMs = (FILETIME_TO_100NS(ftEndUTC) - FILETIME_TO_100NS(ftBeginUTC)) / 10000LL;
//Done
bRes = TRUE;
}
else
nOSError = ::GetLastError();
}
else
nOSError = ::GetLastError();
}
else
nOSError = ::GetLastError();
}
else
nOSError = ::GetLastError();
}
else
nOSError = ERROR_INVALID_PARAMETER;
if(pOutDiffMs)
*pOutDiffMs = iiDiffMs;
::SetLastError(nOSError);
return bRes;
}
BOOL ConvertLocalTimeToUTCTime(SYSTEMTIME* pSt_Local, SYSTEMTIME* pOutSt_UTC)
{
//Convert local date/time from 'pSt_Local'
//'pOutSt_UTC' = if not NULL, receives converted UTC time
//RETURN:
// = TRUE if success
// = FALSE if error (check GetLastError() for info)
BOOL bRes = FALSE;
SYSTEMTIME stUTC = {0};
int nOSError = NO_ERROR;
if(pSt_Local)
{
//First get time zone info
TIME_ZONE_INFORMATION tzi;
if(GetDSTInfoForYear(pSt_Local->wYear, &tzi) != DST_ERROR)
{
if(::TzSpecificLocalTimeToSystemTime(&tzi, pSt_Local, &stUTC))
{
//Done
bRes = TRUE;
}
else
nOSError = ::GetLastError();
}
else
nOSError = ::GetLastError();
}
else
nOSError = ERROR_INVALID_PARAMETER;
if(pOutSt_UTC)
*pOutSt_UTC = stUTC;
::SetLastError(nOSError);
return bRes;
}
DST_STATUS GetDSTInfoForYear(USHORT uYear, TIME_ZONE_INFORMATION* pTZI)
{
//Get DST info for specific 'uYear'
//INFO: Year is not used on the OS prior to Vista SP1
//'pTZI' = if not NULL, will receive the DST data currently set for the time zone for the year
//RETURN:
// = Current DST status, or an error
// If error (check GetLastError() for info)
DST_STATUS tzStat = DST_ERROR;
int nOSError = NO_ERROR;
//Define newer APIs
DWORD (WINAPI *pfnGetDynamicTimeZoneInformation)(PDYNAMIC_TIME_ZONE_INFORMATION);
BOOL (WINAPI *pfnGetTimeZoneInformationForYear)(USHORT, PDYNAMIC_TIME_ZONE_INFORMATION, LPTIME_ZONE_INFORMATION);
//Load APIs dynamically (in case of Windows XP)
HMODULE hKernel32 = ::GetModuleHandle(L"Kernel32.dll");
ASSERT(hKernel32);
(FARPROC&)pfnGetDynamicTimeZoneInformation = ::GetProcAddress(hKernel32, "GetDynamicTimeZoneInformation");
(FARPROC&)pfnGetTimeZoneInformationForYear = ::GetProcAddress(hKernel32, "GetTimeZoneInformationForYear");
TIME_ZONE_INFORMATION tzi = {0};
//Use newer API if possible
if(pfnGetDynamicTimeZoneInformation &&
pfnGetTimeZoneInformationForYear)
{
//Use new API for dynamic time zone
DYNAMIC_TIME_ZONE_INFORMATION dtzi = {0};
tzStat = (DST_STATUS)pfnGetDynamicTimeZoneInformation(&dtzi);
if(tzStat == DST_ERROR)
{
//Failed -- try old method
goto lbl_fallback_method;
}
//Get TZ info for a year
if(!pfnGetTimeZoneInformationForYear(uYear, &dtzi, &tzi))
{
//Failed -- try old method
goto lbl_fallback_method;
}
}
else
{
lbl_fallback_method:
//Older API (also used as a fall-back method)
tzStat = (DST_STATUS)GetTimeZoneInformation(&tzi);
if(tzStat == DST_ERROR)
nOSError = ::GetLastError();
else
nOSError = ERROR_NOT_SUPPORTED;
}
if(pTZI)
{
*pTZI = tzi;
}
::SetLastError(nOSError);
return tzStat;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With