使用 WinAPI 进行夏令时和 UTC 到本地时间的转换

Posted

技术标签:

【中文标题】使用 WinAPI 进行夏令时和 UTC 到本地时间的转换【英文标题】:Daylight Saving Time and UTC-to-local time conversions with WinAPIs 【发布时间】:2014-06-11 20:26:28 【问题描述】:

我正在尝试查看从本地时间转换为 UTC 时间(反之亦然)的 WindAPI 是否准确。例如,让我们以LocalFileTimeToFileTime API 为例。它的描述是:

LocalFileTimeToFileTime 使用时区的当前设置 和夏令时。因此,如果是夏令时, 此功能将考虑夏令时,即使 您转换的时间是标准时间。

所以我用这段代码测试它:

//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!

那么我在这里错过了什么?

【问题讨论】:

Paul 的回答很准确,但您也可以考虑使用Boost C++ Library 的日期和时间 API。 @MattJohnson:是的,它有效。不过,除了 C 实现之外,LocalFileTimeToFileTime API 本身有什么问题——它真的在“现在”调用它的时候使用 DST 调整吗? @ahmd0:是的,如文档所述。 【参考方案1】:

SystemTimeToFileTime() 将其第一个参数解释为 UTC 时间(没有 DST 的概念),因此您的 ft1_localft2_local 对象将始终相隔两个小时,因为您正在更改数据格式,但不是实际的时间点。然后LocalFileTimeToFileTime() 将对您传递给它的任何内容应用相同的偏移量,因此ft1_utcft2_utc 也总是会相隔两个小时。

正如文档所说,“LocalFileTimeToFileTime 使用当前设置 时区和夏令时”(强调我的),所以如果在当前时间你比 UTC 晚四个小时,例如,它只会从您传递给它的任何时间中减去四个小时,无论该时间最初是否代表 DST 另一边的某个时间。

编辑:根据 cmets,这是您如何获得标准 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;

哪个输出:

paul@thoth:~/src$ ./difftime
There are 3600.0 seconds difference.
paul@thoth:~/src$ 

如你所愿。

注意,struct tm:

tm_year 表示自 1900 年以来的年数,所以要获得 2015,我们写 115

tm_mon 的范围是 0 到 11,所以三月是 2,而不是 3。

其他时间成员如你所愿

tm_isdst 设置为-1 时,mktime() 将尝试自行确定 DST 在我们提供的当地时间是否有效,这就是我们希望它在此处执行的操作。

【讨论】:

SystemTimeToFileTime 将其第一个参数解释为 UTC 时间。它按原样转换它,或者换句话说,1:30 AM 变为 1:30 AM。您可以通过手动将 FILETIME 值转换为人类可读的格式来检查。表示为100-nanosecond intervals since January 1, 1601 如何从第一个参数的文档中解释这一点,然后:“指向 SYSTEMTIME 结构的指针,该结构包含要从 UTC 转换为文件时间格式的系统时间。” 我该如何解释?搞砸了微软文档:) 我喜欢你的第二个论点,关于uses the current settings。似乎LocalFileTimeToFileTime 确实采用了当前的 DST 设置并应用它,而不考虑转换的日期。哇,这很糟糕。那你是如何调整我找出两个日期之间时差的方法的? FILETIME 结构包含自 1601 年 1 月 1 日 UTC 以来的间隔,因此如果 1:30AM 总是变为 1:30AM,这表明 SystemTimeToFileTime() 将其第一个参数解释为 UTC,它不会驳斥这一点。【参考方案2】:

尽管Paul Griffiths' 解决方案非常漂亮,但由于明显的语言环境限制,我无法使用它。 (C 显然已经过时了。)所以我不得不采用纯粹的 WinAPI 方法。接下来是我想出的。如果我错了,请纠正我(尤其是那些可以访问微软mktime 似乎偏爱的美国以外时区的人):

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());

那么这就是实际的实现。这里需要注意的一个重要方面是,由于缺少用于检索特定年份的时区信息的 API,以下方法可能无法在 Windows XP 上可靠地运行。

先声明一些:

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);

以及实现:

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;

【讨论】:

无需调用 GetDynamicTimeZoneInformation()。只需将 NULL 传递给 GetTimeZoneInformationForYear() 的第二个参数即可。

以上是关于使用 WinAPI 进行夏令时和 UTC 到本地时间的转换的主要内容,如果未能解决你的问题,请参考以下文章

在观察夏令时的同时解析本地时间的有序时间戳(到 UTC)

UTC 到夏令时的当地时间

尽管 UTC,Java / Hibernate Timestamp 仍受到夏令时的影响

夏令时和 UTC 时间

是否有“本地”时间戳?

R将年-月-日-小时本地标准时间转换为UTC