程序员如何和“美国时间”愉快的玩耍

简介:   请原谅我这里标题党,其实本文只是想分享一下c++编程场景下如何解决“美国时间”与时间戳转换的经验,大家轻拍 :) # 时间戳与夏令时的宿怨   在程序的世界里,我们更喜欢和系统时间戳玩耍,因为全世界所有计算机的系统时间戳不会因各自时区设置差异而不同,而时间戳是自1970年1月1日到当下的秒数(实际上时间戳的数据类型是time_t,time_t在大多数c/cpp实现里是long类型)

  请原谅我这里标题党,其实本文只是想分享一下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]=='\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
目录
相关文章
|
安全 程序员 网络安全
花无涯带你走进黑客世界之深夜编程的程序员
有些人,家境贫寒,有强烈的发大财的欲望,渴望出人头地,让父母过上好日子。说起来永远踌躇满志,日常却是逛淘宝刷b站,基础工作嫌没劲,难的项目接不住,深夜会被上进心折磨,下着要努力的决心,被自己感动得睡着。沉沦在「我告诉自己要努力就等于我努力了」的生活里,日复一日,年复一年。
|
3月前
|
算法 安全
关于我用半个月过了软件设计师这件事
这篇文章分享了作者在半个月内通过软件设计师考试的经验,包括快速刷视频了解知识体系、针对性地刷历年真题并根据错题加强知识点巩固、进行模拟考试和总结,以及使用笔记软件记录重要知识点的方法。
关于我用半个月过了软件设计师这件事
|
3月前
|
设计模式 算法 安全
2023下半年软件设计师 关于我用了半个月过了软件设计师这件事
这篇文章分享了作者在仅用半个月的业余时间备考并通过2023下半年软件设计师考试的经历,包括备考策略、所用资料和个人建议,同时提供了相关备考资料的获取方式。
|
3月前
|
存储 前端开发 JavaScript
太爽了!这12个前端库,帮我在工作中赢得了不少摸鱼时间!!
太爽了!这12个前端库,帮我在工作中赢得了不少摸鱼时间!!
好家伙!30% 国外程序员每天“摸鱼”四五个小时,国内似乎更严重
2020 年全球爆发新冠疫情后,很多国家和地区的企业支出远程办公,除了一些必须在现场办公的岗位之外,很多岗位的员工都可以居家办公。
|
6月前
|
人工智能 物联网 大数据
创作活动(五十)———还记得当初自己为什么选择计算机?
创作活动(五十)———还记得当初自己为什么选择计算机?
39 0
|
移动开发 前端开发 JavaScript
花无涯带你走进黑客世界16 前端攻城狮资源
Web前端开发是从网页制作演变而来的,在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。
|
人工智能 IDE 算法
【周末闲谈】新的编程方式,程序员的未来何在?
【周末闲谈】新的编程方式,程序员的未来何在?
123 0
|
机器学习/深度学习 人工智能 自然语言处理
【paddlehubOCR项目】网课手酸酸,眼花花,救星来啦!
大家好这里是三岁,今天给大家带来的是在AiStudio项目平台的一个精选项目,虽然很短,但是效果拔群,使用到了最近特别火的paddleOCR~~~
274 0
【paddlehubOCR项目】网课手酸酸,眼花花,救星来啦!
假期愉快!
之前在的文章中已经写了公平锁、非公平锁,独享锁、共享锁,那么接下来我们就得介绍互斥锁和读写锁了。那我们我就来了解一波把!