(這是一篇積存的舊文,原始發想時間大約在 2007 年,但精確的日子已不可考。)

這是怎樣,平均每個月可以遇到一個 VC6 的問題,真苦。XD

這次的問題出在 strftime() 這個標準 C 的函式。依據 C99,若傳進 strftime() 的 format specifier 有 %Z%z 的話,分別應該被代換為:

%Z is replaced by the locale’s time zone name or abbreviation, or by no characters if no time zone is determinable. [tm_isdst]

%z is replaced by the offset from UTC in the ISO8601 format ‘‘-0430’’ (meaning 4 hours 30 minutes behind UTC, west of Greenwich), or by no characters if no time zone is determinable. [tm_isdst]

也就是說,%Z 會被代換成,目前 locale 所規定的,各個 timezone 的名稱或縮寫,而 %z 則會被代換成,ISO8601 裡規定的格式:±NNNN,如果無法確定目前的 timezone,%Z 與 %z 兩者皆會被代換成空字串。

可是,MSDN 裡卻這麼說 (VS.60/VS.71/VS.80斜體標示的是 VS.60 沒有的部份):

%z, %Z

Either the time-zone name or time zone abbreviation, depending on registry settings; no characters if time zone is unknown

我手邊找不到 C89 Standard 全文,所以不確定 C89 的規定如何。但 K&R 的《C Programming Language 2nd Ed.》這本聖經裡只有提到(pp.234):

%Z time zone name, if any.

並沒有小寫的 %z 這種 format specifier。

瞭解了各方說法之後,我寫了以下的測試程式,分別用 VC6 與 VC8[1] 來測試:

#include <stdio.h>
#include <time.h>

int main()
{
    time_t now;
    char buf[128];
    struct tm* ptm = NULL;

    time(&now);
    ptm = localtime(&now);

    strftime(buf, sizeof(buf), "%Z", ptm);
    printf("%%Z: %s\n", buf);

    strftime(buf, sizeof(buf), "%z", ptm);
    printf("%%z: %s\n", buf);

    return 0;
}

VC6 和 VC8 的執行結果都是:

%Z: 台北標準時間
%z: 台北標準時間

因為 C89[2] 只有規定大寫 %Z 要代換成 timezone name,也沒有規定 timezone name 的格式,「台北標準時間」可以是一個 timezone name,故 1998 年出的 VC6,符合標準。然而,在 C99 裡已經明確規定了,大寫 %Z 代換成 timezone name,但小寫 %z 要代換成 ISO8601 的 ±NNNN 格式,因此,在 2005 年出的 VC8,其小寫 %z 的處理方式,是不符合標準的。

所以,結論是,如果我要做出符合 ISO8601 格式的 time-stamp,不能光用 strftime(),還必須自己從 struct tm.tm_gmtoff 轉出來。

2008-10-08 補充:如何模擬 %z±NNNN

要模擬出 ±NNNN,必須取得 local time 對 gmt time 的 offset 來。在 Visual C++ 下可以用 _timezone 這個全域變數,或是 _get_timezone() 這個函式,後者在新一點的 Visual C++ 版本,才有提供。如下:

std::string get_timestamp()
{
    // Get local time
    time_t now;
    time(&now);
    struct tm* ptm = localtime(&now);

    // Get offset from local time to GMT time
    long gmtoff = 0;
#if defined(_MSC_VER)
#   if (_MSC_VER > 1200) // VC6 above, not including VC6
    _get_timezone(&gmtoff);
#   else
    gmtoff = _timezone;
#   endif
#elif defined(__GNUC__)
    gmtoff = ptm->tm_gmtoff;
#endif

    // Compose the format string, we simulate %z here.
    char fmt[20]; // "%Y%m%dT%H%M%S+NNNN" (18 chars)
    memset(fmt, 0, sizeof(fmt));
    snprintf(
        fmt, (sizeof(fmt) - sizeof(fmt[0])),
        "%%Y%%m%%dT%%H%%M%%S%+03d%02d"
        //                  ^^^^^^^^^ = %z
        , -(gmtoff / (60 * 60))
        , (gmtoff % (60 * 60))
    );

    // Format the string
    char str[21]; // YYYYMMDDTHHMMSS+NNNN (20 chars)
    strftime(str, sizeof(str), fmt, ptm);

    return string(str);
}

關鍵就在註解裡 =%z 的那邊,要注意的是,必須要用 %+03d 而不是 %+02d,因為正、負號與兩位數字,加起來寬度應該是 3

相關資料:

 

 


  1. 即 Visual Studio .Net 2005 裡面的 Visual C++。
  2. 採 K&R 的說法。