Classic Time Programming

遇到的问题

之前的工作中,见到过类似下面的代码:

time_t today() 
{
    time_t now = ::time(nullptr);
    struct tm* tm_loc = ::localtime(&now);
    tm_loc->tm_hour = 0;
    tm_loc->tm_min = 0;
    tm_loc->tm_sec = 0;
    return ::mktime(tm_loc);
}

time_t tomorrow()
{
    return today() + 24*60*60;
}

这个代码片段的目的是获取当地当天和第二天的起始时间对应的 UTC 时间。这两个函数对于我们所在的地区是没有问题的,但是对于使用夏令时的地区来说,tomorrow() 是个错误的实现。对于他们来说,夏令时开始时,当时钟会向前调整一小时,那么这天就会比平常短一小时,不是固定的时间间隔。

那么 tomorrow() 该怎么实现呢?

传统的时间 API

- UTC Local Time Zone
time_t -> tm gmtime() localtime()
tm -> time_t x mktime()
time_t -> local str x ctime()
tm asctime() asctime()

没有 local time_t 这样的东西,time_t 指的是 UTC 时间戳。

从上面的 API 看,我们可以得到一个本地第二天的起始时间的 tm 结构,然后通过 mktime() 来生成对应的 UTC 时间。

std::time_t mktime( std::tm* time );
The values in time are permitted to be outside their normal ranges.

time_t floor_day(int steps)
{
    time_t now = ::time(nullptr);
    struct tm* tm_loc = ::localtime(&now);
    tm_loc->tm_mday += steps;
    tm_loc->tm_hour = 0;
    tm_loc->tm_min = 0;
    tm_loc->tm_sec = 0;
    return ::mktime(tm_loc);
}

time_t today() 
{
    return floor_day(0);
}

time_t tomorrow()
{
    return floor_day(1);
}

time_t utc_tdy = today();
struct tm* tdy = ::gmtime(&utc_tdy);
cout << "tdy: " << asctime(tdy);

time_t utc_tmr = tomorrow();
struct tm* tmr = ::gmtime(&utc_tmr);
cout << "tmr: " << asctime(tdy);

运行结果:godbolt

tdy: Sat Jul  8 16:00:00 2023
tmr: Sun Jul  9 16:00:00 2023

至此 tomorrow() 函数的实现算是解决了。然而,新的需求来了,现在要求计算出特定时区的起始时间 time_t today(tz),而现有的标准库并没有提供可以指定时区的函数,那么该怎么办呢?

时间转换模型

+-------------------+     +-----------------+     +-----------------+
|                   |  +  |                 |  =  |                 |
|   Absolute Time   |     |    Time Zone    |     |    Civil Time   |
|                   |  =  |                 |  +  |                 |
+-------------------+     +-----------------+     +-----------------+
- time_t                                          - struct tm
- time_point
                        F(Absolute, TZ) -> Civil
                        F(Civil, TZ)    -> Absolute

如上图,可建立时间的转换关系:

然而传统的时间 API 隐藏了 Time Zone,使得无法获取特定时区的时间。好在各个系统带有 Time Zone 数据库,CCTZ 据此以及时间转换模型,实现了一套处理时间相关问题的 API。

+-------------------+     +-----------------+     +-----------------+
|                   |  +  |                 |  =  |                 |
|   Absolute Time   |     |    Time Zone    |     |    Civil Time   |
|                   |  =  |                 |  +  |                 |
+-------------------+     +-----------------+     +-----------------+
  cctz::time_point          cctz::time_zone       cctz::civil_time<T>

现在我们来实现 today(tz)tomorrow(tz) 函数:

time_t floor_day(const string& tz_name, int steps)
{
    time_zone tz;
    load_time_zone(tz_name, &tz);

    auto tp_now = system_clock::now();
    // UTC -> Civil
    auto civil_now = convert(tp_now, tz);
    // 当天的起始时间
    civil_day civil_floor_day{civil_now};
    if (steps != 0) {
        civil_floor_day += steps;
    }
    // Civil 转 UTC
    auto tp_floor_day = convert(civil_floor_day, tz);
    return system_clock::to_time_t(tp_floor_day);
}

time_t today(const string& tz_name) 
{
    return floor_day(tz_name, 0);
}

time_t tomorrow(const string& tz_name)
{
    return floor_day(tz_name, 1);
}

auto tzs = {"Asia/Chongqing", "America/Los_Angeles"};
for (auto tz : tzs) {
    std::cout << tz << std::endl;
    auto tp_tdy = system_clock::from_time_t(today(tz));
    std::cout << "  tdy: " << format("%FT%T%z", tp_tdy, utc_time_zone()) << std::endl;
    auto tp_tmr = system_clock::from_time_t(tomorrow(tz));
    std::cout << "  tmr: " << format("%FT%T%z", tp_tmr, utc_time_zone()) << std::endl;
}

运行结果:godbolt

Asia/Chongqing
  tdy: 2023-07-07T16:00:00+0000
  tmr: 2023-07-08T16:00:00+0000
America/Los_Angeles
  tdy: 2023-07-08T07:00:00+0000
  tmr: 2023-07-09T07:00:00+0000

C++20 Time Zones

+-------------------+     +-----------------+     +-----------------+
|                   |     |                 |     |                 |
|   Absolute/Civil  |  +  |    Time Zone    |  =  |   Zoned Time    |
|                   |     |                 |     |                 |
+-------------------+     +-----------------+     +-----------------+
- time_point<Clock>            time_zone              zoned_time
- time_point<local_t>

类似 CCTZ 的时间转换模型,有了一些改进。C++ 标准的实现,来自于 Howard Hinnant 的 date

time_point 不再只是表示 UTC 时间,通过添加 local_t 对其进行特化,也表示 Civil 时间。两者的统一,使得对于 Civil 时间的计算能够复用 time_point 的函数,而不像 cctz::civil_time 需要单独再实现一套计算函数。

除此之外,标准库提供了一个容器将 time_pointtime_zone 关联起来。从 zoned_time 既可以获取 Absolute Time (time_point<Clock>),也能获取 Civil Time (time_point<local_t>)。

time_t floor_day(string_view tz_name, int steps)
{
    auto tp_now = round<seconds>(system_clock::now());
    // UTC -> Civil
    zoned_seconds zoned_now{tz_name, tp_now};
    auto civil_now = zoned_now.get_local_time();
    // 当天的起始时间
    auto civil_floor_day = floor<days>(civil_now);
    if (steps != 0) {
        civil_floor_day += days{steps};
    }
    zoned_seconds zoned_floor_day{tz_name, round<seconds>(civil_floor_day)};
    // Civil -> UTC
    return system_clock::to_time_t(zoned_floor_day.get_sys_time());
}

time_t today(const string& tz_name) 
{
    return floor_day(tz_name, 0);
}

time_t tomorrow(const string& tz_name)
{
    return floor_day(tz_name, 1);
}

auto tzs = {"Asia/Chongqing", "America/Los_Angeles"};
for (auto tz : tzs) {
    std::cout << tz << std::endl;
    auto tp_tdy = round<seconds>(system_clock::from_time_t(today(tz)));
    std::cout << format("  tdy: {:%FT%T%z}", tp_tdy) << std::endl;
    auto tp_tmr = round<seconds>(system_clock::from_time_t(tomorrow(tz)));
    std::cout << format("  tmr: {:%FT%T%z}", tp_tmr) << std::endl;
}

运行结果:godbolt

Asia/Chongqing
  tdy: 2023-07-07T16:00:00+0000
  tmr: 2023-07-08T16:00:00+0000
America/Los_Angeles
  tdy: 2023-07-08T07:00:00+0000
  tmr: 2023-07-09T07:00:00+0000

需要注意的是 当计算真实的时间跨度时,应转到 UTC 时间再取差值。如下:

constexpr string_view tz{"America/Los_Angeles"};

local_days curr_date{2011y/3/13};
zoned_seconds curr_zt{tz, curr_date};

local_days next_date = curr_date + days{1};
zoned_seconds next_zt{tz, next_date};

cout << "local dur:\t" << (next_zt.get_local_time() - curr_zt.get_local_time()) << endl;
cout << "sys dur:\t" << (next_zt.get_sys_time() - curr_zt.get_sys_time()) << endl;

运行结果:godbolt

local dur:	86400s
sys dur:	82800s

对比

Lib macOS/iOS/Android Linux Windows <10 Windows ≥10
CCTZ
date ✓ (libcurl) ✓ (libcurl)
STL ✓ (≥g++13) ✓ (≥vs2019)