请原谅我这里标题党,其实本文只是想分享一下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年为例,夏冬令时是如何切换的:
核心就是根据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]=='\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