Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Daylight Saving Time and UTC-to-local time conversions with WinAPIs

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?

like image 493
ahmd0 Avatar asked Feb 13 '23 03:02

ahmd0


2 Answers

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.

like image 134
Crowman Avatar answered Feb 15 '23 16:02

Crowman


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;
}
like image 25
ahmd0 Avatar answered Feb 15 '23 17:02

ahmd0