1、問題
在開發雲平台程序的時候,經常會碰到時間區域轉換的問題。比如,任何網絡存儲的文檔的metadata都自己記錄了編輯時間。但是,雲平台記錄時需要把這個時間轉成標准時間,便於管理。但是用戶使用的時候卻是根據他自己的時間來的。比如,
- 某人需要在北京時間12/31:11:59把新年短信發給女朋友。太早發,太晚發都會惹人不高興。因此,系統搜索或安排任務的時候,需要根據某個時區,或是新年短信內部時間設定。
- 再比如,在搜索電郵或短信的時候,根據RFC的定義,時間的搜索需要按照收到時間的字面數值。比如一個電郵的收到時間為Mon, 14 May 2018 23:23:27 -0700,那么搜索1 SEARCH BEFORE 14-May-2018必須返回這個電郵。但是,如果我們在標准時區,當地時間已經是5月15了。
2、概念與API
在最早的Unix中,一台機器只能處理UTC以外的一個時區。時區偏移量和名稱字符串(實際上是一對名稱字符串,一個用於夏季,一個用於冬季)已配置到內核中,並可供C程序使用。后來(1982)System III通過修改名為TZ的環境變量並調用函數tzset(3),可以設置session時區。從此,解釋TZ值區域規范的規則成為POSIX標准的一部分。
V7和較早的BSD Unix有其他各種配置本地時區的方法,這些方法不涉及使用或解釋TZ。他們與POSIX TZ的解釋有一個相同的致命缺陷,那就是它們不是為了應對時區系統的歷史不穩定而設計的。他們無法解釋一整套歷史位移/ DST的規則,從而正確表達過去本地時間和現在時刻。
在現代Unix系統上,TZ變量可能根本就沒有設置,但在任何進程中可以通過明確設置TZ來覆蓋系統默認時區。在啟動或通過重寫值TZ時,可以根據地理位置配置時區指示符(通常但不總是地區/主要城市對),例如“America / New_York”或“Europe / Vienna”或“Asia /台北”。如果標識符是通過TZ設置的,為了與POSIX標准向后兼容,可能需要以冒號開頭,以區別於舊式的時區規范。
基於位置的區域命名方案[IANA-ZONES]由互聯網號碼分配機構IANA管理。
2.1、Unix中時間日期的格式
作為現在大多數操作系統的鼻祖,Unix中時間日期的format是相當亂的。這也反映了系統發展的歷程和不同貢獻者的設計偏好:
時間日期 |
解釋 |
1526924458 |
Unix UTC seconds |
Mon May 21 11:20:32 PDT 2018 |
|
2018-05-21 11:23:27-07:00 |
$ date --rfc-3339=seconds
|
Mon, 21 May 2018 11:23:27 -0700 |
e-mail RFC-822/RFC-2822 format |
Fri, 24 Oct 2014 19:32:27 GMT |
HTTP (RFC-2616/RFC-7231) format |
20141024192327.000000Z |
LDAP (RFC-2252/X.680/X.208) format |
2014-10-24 15:32:27 |
Modified ISO-8601 local time |
2014-10-24T15:32:27 |
Strict ISO-8601 local time |
2014-10-24T19:32:27Z |
RFC-3339 time, always UTC and marked Z |
在用unix程序時,兩個使用最多的數據結構是time_t和struct tm,time_t其實就是int32_t,而struct tm的定義如下,我們發現這個結構有個問題,沒有時區信息:
struct tm { int tm_sec; /* seconds [0,60] (60 for + leap second) */ int tm_min; /* minutes [0,59] */ int tm_hour; /* hour [0,23] */ int tm_mday; /* day of month [1,31] */ int tm_mon ; /* month of year [0,11] */ int tm_year; /* years since 1900 */ int tm_wday; /* day of week [0,6] (Sunday = 0) */ int tm_yday; /* day of year [0,365] */ int tm_isdst; /* daylight saving flag */ };
時區是通過環境變量"TZ"來定義的,使之生效需要調用tzset函數,查看時區需要使用tzname:
setenv("TZ", "Australia/Currie", 1); tzset(); printf ("tz: [%s:%s]\n", tzname[0], tzname[1]);
特別需要注意的是 char * tzname [2]
數組tzname包含兩個字符串,它們是用戶選擇的時區(標准和夏令時)的標准名稱。 tzname [0]是標准時區的名稱(例如“EST”),tzname [1]是使用夏令時的時區名稱(例如“EDT”)。這些對應於來自TZ環境變量的std和dst字符串(分別)。如果從不使用夏令時,則tzname [1]是空字符串。
tzname數組在tzset,ctime,strftime,mktime或localtime被調用時從TZ環境變量初始化。如果使用了多個縮寫(例如美國東部時間和東部夏令時的“EWT”和“EDT”),則該數組包含最近的縮寫。
tzname數組對於POSIX.1兼容性是必需的,但在GNU程序中,最好使用分解時間結構的tm_zone成員,因為即使它不是最新的縮寫,tm_zone也會報告正確的縮寫。
2.2、API
- Unix時間:
#include <time.h> /* Obtain current time. */ time_t current_time = time(NULL);
C標准定義time()返回的time_t值不是特定時區的,自1970年1月1日00:00 UTC時刻以來流逝的秒數。使用時不能假設其內部實現,需要使用C標准庫中適當的函數(如gmtime和localtime)將time_t轉換為struct tm並獲得對時間戳細節的訪問。
- 設置時區:tzset
#include <time.h> void tzset (void); extern char *tzname[2]; /* time zone name */ extern long timezone; /* seconds west of UTC, *not* DST-corrected */ extern int daylight; /* nonzero if DST is ever in effect here */
- 獲取標准或本地時間
- gmtime/gmtime_r:獲取標准時間,不會修改時區變量
- localtime/localtime_r:獲取本地時間,如果本地時間未設置,會根據地理位置修改時區變量
#include <time.h> struct tm *gmtime(const time_t *); struct tm *gmtime_r(const time_t *, struct tm *); struct tm *localtime(const time_t *); struct tm *localtime_r(const time_t *, struct tm *);
- 從本地時間轉為Unix時間(秒):mktime
#include <time.h> time_t mktime(struct tm *tm);
mktime函數是一個標准函數,是localtime的反函數,將輸入本地時間struct tm轉換為Unix時間time_t。
- 其它函數
其它還有輔助函數幫助時間轉換為字符串或從字符串轉換成時間,因為和時區的關系不大,我們不詳細解釋。
- asctime將struct tm對象轉換為文本表示(不建議使用)
- ctime將time_t值轉換為文本表示
- strftime將struct tm對象轉換為自定義文本表示
- wcsftime將struct tm對象轉換為自定義寬字符串文本表示
3、用例
我們有一個已知時間和時區位移,需要輸出在該時區的時間,返回值為該天的年月日(yyyyMMdd)。
int timestamp_to_timezonedate(time_t date_timestamp, int32_t tz_offset, char *buf, size_t bufsize) { struct tm dat = {0};
date_timestamp += tz_offset * 60; gmtime_r(&date_timestamp, &dat); if (buf) strftime(buf, bufsize, "%Y-%m-%dT%H:%M:%S", &dat); return ((dat.tm_year+1900) * 10000) + ((dat.tm_mon+1) * 100) + (dat.tm_mday); }
再看看如何獲取當前時區和標准時間的差別(分鍾):
//return local timezone offset in minutes int store_local_tz_offset(void) { time_t gmt, rawtime = time(NULL); struct tm gbuf; gmtime_r(&rawtime, &gbuf); // Request that mktime() looks up dst in timezone database gbuf.tm_isdst = -1; //gmt = rawtime - offset //gbuf is used as local tm, therefore gmt will be shifted back with offset gmt = mktime(&gbuf); //rawtime - gmt return (int)difftime(rawtime, gmt)/60; }
4、One more thing
一般的時區位移是分鍾。比如Mon, 21 May 2018 11:23:27 -0700,最后幾位是時區位移:[+|-]HHMM。
有些地方的時區位移是半小時,所以為了支持這些地區,在小時后面會有分鍾的參數,比如印度 Tuesday 2018-05-22|04:56:39 +0630
參考文獻
[1]https://www.gnu.org/software/libc/manual/html_node/Time-Zone-Functions.html
[2]http://www.catb.org/esr/time-programming/