程式設計師如何和“美國時間”愉快的玩耍

cauchydean發表於2016-10-31

  請原諒我這裡標題黨,其實本文只是想分享一下c++程式設計場景下如何解決“美國時間”與時間戳轉換的經驗,大家輕拍 ?

時間戳與夏令時的宿怨

  在程式的世界裡,我們更喜歡和系統時間戳玩耍,因為全世界所有計算機的系統時間戳不會因各自時區設定差異而不同,而時間戳是自1970年1月1日到當下的秒數(實際上時間戳的資料型別是time_t,time_t在大多數c/cpp實現裡是long型別)。

  而由於各種政治、風俗等原因,世界上有少部分國家好“夏令時”這一口,導致這些國家的全國標準時間在一年時間裡,並不是總取的是固定的時區,比如老美……這導致美國的全國標準時間在一年裡和我們的時差並不總是16個小時的,存在冬令時和夏令時的區別。

  而對於做國際業務的同學,大多數情況下資料結算的時間都是按美國時間來計算。我們的程式跟客戶打交道的時候,在某些情況下基於使用者友好的需要,不得不用美國時間作為時間配置的格式。比如說,控制大促演算法何時生效,就不得不按美國時間0點0分0秒開始算。

  這裡涉及在c/cpp程式裡如何將美國時間 “2015-11-11 00:00:00″轉換對應系統時間戳的問題,就是本文要討論的問題。

爺爺留下來的寶貝

  要解決這個問題,先看看linux爺爺輩都留下了哪些寶貝只可以供我們使用的,常用的時間與時間戳轉換函式如下:

// 獲取系統當前時間戳(自格林威治時間1970年1月1日凌晨至現在所經過的秒數),並將該值存於timer所指的單元中(如果timer非NULL的話)
time_t time (time_t* timer);

// 將timep指向的時間戳轉換為當地時間(依賴於執行時的時區設定),並將結果儲存在result所指的單元中
struct tm *localtime_r(const time_t *timep, struct tm *result);
// 將timep指向的時間戳轉換為格林威志時間(GMT),並將結果儲存在result所指的單元中
struct tm *gmtime_r(const time_t *timep, struct tm *result);

// 將tm指向的時間(理解為當地時間),轉換成時間戳
time_t mktime(struct tm *tm);
// 將tm指向的時間(理解為格林威治時間),轉換成時間戳,注意這個函式是GNU的擴充套件
time_t timegm (struct tm *tm);

  在我能搜尋到的資料裡面,沒有找到任何一個系統自帶函式是可以做指定時區的時間轉換的。這個也能理解,如果源時間和目標時間的時區差是固定的,將源時間對應的時間戳加上時區相差的秒數,再呼叫timegm函式就能得到目標時間。

  但對於實施夏/冬令時差異的國家而言,由於其與格林威治時間的差異是隨著季節會有所變化,就不能簡單的通過固定的時間偏差方式去解決“時間”與“時間戳”轉換的問題。

聚集美國夏令時

  由於不同國家採用的夏冬令時切換規則不一樣,這裡只聚集到美國時間夏冬令時轉換的解決方案。

夏/冬令時的規律

  到這裡不得不仔細介紹一下老美的夏/冬令時的具體切換規則:

  • 夏令時從3月份第2個週日的凌晨2點開始,全美國採用西7區的時間,為了適應這個變化,在凌晨2點的時候時鐘調快1小時,即直接從01:59:59跳到03:00:00;
  • 冬令時從11月份的第1個週日的凌晨2點開始,全美國使用西8區時間,為了適應這個變化 ,在凌晨2點的時候時鐘調慢1小時,即直接從01:59:59跳回01:00:00。

  這雖然只是兩句話的事,但對於伺服器的日期計算就是一件很頭疼的事……

執行緒不安全的解決辦法

  如上,目前系統自帶函式裡,沒有一個可移植的解決方案,在沒有執行緒安全要求場景下能解決問題的方法倒有一個現成的:

setenv("TZ","America/Los_Angeles",1);
tzset();

  然後再呼叫如上localtime_r、mktime函式,就是按美國時間進行“時間”與“時間戳”轉換的。
  不幸的是,上述設定環境變數TZ的過程其實是執行緒不安全的,在多執行緒的情況下是無法保證結果的,而且如果是多模組獨立開發的話,在當前執行緒下野蠻地修改時區設定,會給同程式下其他模組的執行結果也會帶來不確定性。所以不得不放棄,這裡也不推薦大家使用,給別人留坑就是給自己抹黑。

自己動手豐衣足食

  那就別無選擇了,只能模擬如上美國夏冬令時的轉換規則,自己寫一個美國時間轉換的函式。

  解決所有場景下的問題總是先從研究個體情況開始,這裡先以2015年為例,夏冬令時是如何切換的:
us_time
   核心就是根據tm結構體裡提供的日期和時間,判定是應該使用GMT-7還是GMT-8來進行時間轉換的計算。具體相差的程式碼如下:

long us_time2timestamp(const char *szTime)
{
    long timeSec   = 0;
    int  nTimeZone = 0;

    struct tm tmTmp;
    char *szRet = strptime(szTime, "%Y-%m-%d %H:%M:%S", &tmTmp);
    if (szRet && szRet[0]==` ` && whichTimeZoneAtUsTm(tmTmp, nTimeZone)) {
        tmTmp.tm_isdst = -1; // set day lighting time flag
        // consider szTime as GMT
        timeSec  = static_cast<long>(timegm(&tmTmp));
        // fix timeSec as GMT-8
        timeSec -= 3600*nTimeZone;
    }

    return timeSec;
}

void timestamp2us_time(const time_t lTime, std::string &strTime)
{
    char szBuf[32];
    memset(szBuf, 0, sizeof(szBuf));

    struct tm tRet;
    time_t lGmtTime  = lTime;
    int    nTimeZone = 0;
    gmtime_r(&lGmtTime, &tRet);
    if (whichTimeZone4UsAtGmtTm(tRet, nTimeZone)) {
        lGmtTime += 3600*nTimeZone;
    }
    gmtime_r(&lGmtTime, &tRet);
    int nCnt = snprintf(
            szBuf, sizeof(szBuf),
            "%04d-%02d-%02d %02d:%02d:%02d",
            1900+tRet.tm_year,
            1+tRet.tm_mon,
            0+tRet.tm_mday,
            0+tRet.tm_hour,
            0+tRet.tm_min,
            0+tRet.tm_sec);
    if (nCnt >= 0 && static_cast<size_t>(nCnt) < sizeof(szBuf)) {
        strTime.assign(szBuf);
    } else {
        strTime.clear();
    }
}

  可以看到程式碼邏輯都不復雜,關鍵點根據當前日期和時間,判斷美國時間應該使用哪個時區,然後對時間戳做相應的偏移。
  判斷時區的兩個函式whichTimeZoneAtUsTm、whichTimeZone4UsAtGmtTm如下:

bool whichTimeZoneAtUsTm(tm &_time, int &nTimeZone)
{
    bool bRet = true;
    nTimeZone = -8;
    time_t timestamp1 = timegm(&_time);
    /////////////////////////////////////////////////////////////////////
    // week_current: week-day for current month-day
    // week_month_1: week-day for 1st day in current month
    // day_sunday1:  month-day for 1st sunday in current month
    // day_sunday2:  month-day for 2nd sunday in current month
    const long _start_week_1970_01_01 = 4;
    long week_current = (_start_week_1970_01_01+timestamp1/(3600*24))%7;
    long week_month_1 = (7+(week_current-_time.tm_mday+1)%7)%7;
    long day_sunday1  = 1+(7-week_month_1)%7;
    long day_sunday2  = day_sunday1+7;
    /////////////////////////////////////////////////////////////////////
    if (3<_time.tm_mon+1 && _time.tm_mon+1<11) {
        nTimeZone = -7;
    }
    else if ( 3==_time.tm_mon+1) {
        if (_time.tm_mday>day_sunday2) {
            nTimeZone = -7;
        } else if (_time.tm_mday==day_sunday2) {
            if (2==_time.tm_hour) {
                bRet = false;
            } else if (_time.tm_hour>=3) {
                nTimeZone = -7;
            }
        }
    }
    else if (11==_time.tm_mon+1) {
        if (_time.tm_mday<day_sunday1) {
            nTimeZone = -7;
        } else if (_time.tm_mday==day_sunday1 && _time.tm_hour<2) {
            nTimeZone = -7;
        }
    }

    return bRet;
}

bool whichTimeZone4UsAtGmtTm(tm &_time, int &nTimeZone)
{
    bool bRet = true;
    nTimeZone = -8;
    time_t timestamp1 = timegm(&_time);
    /////////////////////////////////////////////////////////////////////
    // week_current: week-day for current month-day
    // week_month_1: week-day for 1st day in current month
    // day_sunday1:  month-day for 1st sunday in current month
    // day_sunday2:  month-day for 2nd sunday in current month
    const long _start_week_1970_01_01 = 4;
    long week_current = (_start_week_1970_01_01+timestamp1/(3600*24))%7;
    long week_month_1 = (7+(week_current-_time.tm_mday+1)%7)%7;
    long day_sunday1  = 1+(7-week_month_1)%7;
    long day_sunday2  = day_sunday1+7;
    /////////////////////////////////////////////////////////////////////
    if (3<_time.tm_mon+1 && _time.tm_mon+1<11) {
        nTimeZone = -7;
    }
    else if ( 3==_time.tm_mon+1) {
        if (_time.tm_mday>day_sunday2) {
            nTimeZone = -7;
        } else if (_time.tm_mday==day_sunday2 && _time.tm_hour>9) {
            nTimeZone = -7;
        }
    }
    else if (11==_time.tm_mon+1) {
        if (_time.tm_mday<day_sunday1) {
            nTimeZone = -7;
        } else if (_time.tm_mday==day_sunday1 && _time.tm_hour<9) {
            nTimeZone = -7;
        }
    }

    return bRet;
}

  以上程式針對[2015-03-01 00:00:00,2015-04-01 00:00:00]和[2015-11-01 00:00:00,2015-12-01 00:00:00]時間段內測試過,結果是正確的,且實現過程中沒有使用執行緒不安全的操作,大家應該可以放心在多執行緒的環境下使用。

  如果大家發現程式中有什麼邏輯上的錯誤,也請指教,其他國家的夏冬令時轉換,參照如上例子應該也能解決。

  最後給大家提供一個步技巧linux shell下面如何指定時區進行時間操作,希望能幫助大家後面更愉快的玩耍~~~

$TZ=`America/Los_Angeles` date -d "2015-11-01 02:00:00" +%s
1446372000


相關文章