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
如上图,可建立时间的转换关系:
- 一个 UTC 时间,利用 Time Zone 信息,可以转换到当地时间。
- 一个当地时间,利用 Time Zone 信息,可以转换到 UTC 时间。
然而传统的时间 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_point
与 time_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) |