将 UTC time_t 转换为 UTC tm

时间:2021-07-27 16:05:17

标签: c++ time data-conversion

我所有的内部时间都是 UTC 存储在 time_t 中。我需要将它们转换为 struct tm。如果我使用 localtime 时间是正确的,除了 tm_isdst 可能设置导致时间偏离一小时。如果我使用 gmtime,它会得到错误的时间,因为时区差异。

编辑我正在寻找适用于 Windows 和 Linux 的跨平台解决方案

1 个答案:

答案 0 :(得分:2)

这是一个需要 C++11 或更高版本的跨平台解决方案,以及一个 free, open-source, header-only date library。当您的供应商为您提供 C++20 时,您可以放弃 date library,因为它已合并到 C++20 <chrono> 中。

通过 time_ttm 转换为 UTC <chrono> 实际上比使用 C API 更容易。在每个平台上确实存在各种扩展来执行此操作,但扩展具有不同的语法。此解决方案具有跨所有平台的统一语法。

在 C++11 中,虽然没有指定,但 time_tstd::chrono::system_clock 跟踪 Unix Time 是事实上的标准,尽管精度不同。在 C++20 中,这被指定为 std::chrono::system_clock。对于 time_t,事实上的精度是 seconds。可以利用这一知识在 C API 和 C++ <chrono> API 之间创建极其高效的转换。

第 1 步:将 time_t 转换为 chrono::time_point

这非常简单高效:

date::sys_seconds
to_chrono(std::time_t t)
{
    using namespace date;
    using namespace std::chrono;

    return sys_seconds{seconds{t}};
}

date::sys_seconds 只是一个类型别名:

std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>

即基于 time_point 但精度为 system_clockseconds

此函数所做的只是将类型从 time_t 更改为 seconds,然后再更改为 time_point。没有进行实际计算。这是 to_chrono 的优化 clang 编译:

    .globl  __Z9to_chronol          ## -- Begin function _Z9to_chronol
    .p2align    4, 0x90
__Z9to_chronol:                         ## @_Z9to_chronol
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, %rax
    popq    %rbp
    retq
    .cfi_endproc

所有这些都是用于函数调用的样板。如果你内联这个,即使那个也会消失。

此外,只需删除 using namespace date 并将 date::sys_seconds 更改为 std::chrono::sys_seconds,此函数将移植到 C++20。

第 2 步:将 sys_seconds 转换为 tm

这是计算发生的地方:

std::tm
to_tm(date::sys_seconds tp)
{
    using namespace date;
    using namespace std::chrono;

    auto td = floor<days>(tp);
    year_month_day ymd = td;
    hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17
    tm t{};
    t.tm_sec  = tod.seconds().count();
    t.tm_min  = tod.minutes().count();
    t.tm_hour = tod.hours().count();
    t.tm_mday = unsigned{ymd.day()};
    t.tm_mon  = (ymd.month() - January).count();
    t.tm_year = (ymd.year() - 1900_y).count();
    t.tm_wday = weekday{td}.c_encoding();
    t.tm_yday = (td - sys_days{ymd.year()/January/1}).count();
    t.tm_isdst = 0;
    return t;
}

所有计算都发生在前三行:

auto td = floor<days>(tp);
year_month_day ymd = td;
hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17

然后函数的其余部分只是提取字段以填写 tm 成员。

auto td = floor<days>(tp);

上面的第一行简单地将 time_point 的精度从 seconds 截断到 days,向下舍入到负无穷大(即使对于 1970- 01-01 时代)。这只不过是除以 86400。

time_point

上面的第二行获取自纪元以来的天数并将其转换为 year_month_day ymd = td; 数据结构。这是大部分计算发生的地方。

{year, month, day}

上面的第三行从秒精度 hh_mm_ss<seconds> tod{tp - td}; // <seconds> can be omitted in C++17 中减去天精度 time_point,从而得出自 UTC 午夜起 time_point 的持续时间。然后将此持续时间分解为 std::chrono::seconds 数据结构(类型 {hours, minutes, seconds})。在 C++17 中,这一行可以选择性地简化为:

hh_mm_ss

现在 hh_mm_ss tod{tp - td}; // <seconds> can be omitted in C++17 只需提取字段以根据 C API 填写 to_tm

tm

首先对 int tm_sec; // seconds after the minute -- [0, 60] int tm_min; // minutes after the hour -- [0, 59] int tm_hour; // hours since midnight -- [0, 23] int tm_mday; // day of the month -- [1, 31] int tm_mon; // months since January -- [0, 11] int tm_year; // years since 1900 int tm_wday; // days since Sunday -- [0, 6] int tm_yday; // days since January 1 -- [0, 365] int tm_isdst; // Daylight Saving Time flag 进行零初始化很重要,因为不同的平台有额外的 tm 数据成员作为扩展,最好将其设为 0。

tm

对于小时、分钟和秒,只需从 tm t{}; 中提取适当的 chrono::duration,然后使用 tod 成员函数提取整数值:

.count()

t.tm_sec = tod.seconds().count(); t.tm_min = tod.minutes().count(); t.tm_hour = tod.hours().count(); 显式转换为 day,这是 C API 不给 unsigned 数据成员意外偏差的少数几个地方之一:

tm

t.tm_mday = unsigned{ymd.day()}; 被定义为“自一月以来的月份”,因此必须考虑偏差。可以从 tm_mon 中减去 January,得到 month duration。这是一个months,可以用chrono::duration成员函数提取整数值:

.count()

同样,t.tm_mon = (ymd.month() - January).count(); 是 1900 年以来的年份:

tm_year

可以使用转换语法将天精度t.tm_year = (ymd.year() - 1900_y).count(); (time_point) 转换为td,然后weekday 具有成员函数weekday 以提取与 C API 匹配的整数值:自星期日起的天数 -- [0, 6]。或者,如果需要 ISO 编码 [Mon, Sun] -> [1, 7],还有一个 .c_encoding() 成员函数。

.iso_encoding()

t.tm_wday = weekday{td}.c_encoding(); 是自 1 月 1 日以来的天数 -- [0, 365]。这很容易通过从天数精度 tm_yday (time_point) 中减去年份的第一个来计算,创建一个 td days

chrono::duration

最后 t.tm_yday = (td - sys_days{ymd.year()/January/1}).count(); 应设置为 0 以表示夏令时无效。从技术上讲,这一步在零初始化 tm_isdst 时已经完成,但为了便于阅读,这里重复了一遍:

tm

t.tm_isdst = 0; 可以通过以下方式移植到 C++20:

  • 删除to_tm
  • using namespace date; 更改为 date::sys_seconds
  • std::chrono::sys_seconds 更改为 1900_y

示例使用:

给定一个 1900y,下面是如何使用这些函数将其转换为 UTC time_t

tm

以下是必要的标题:

std::time_t t = std::time(nullptr);
std::tm tm = to_tm(to_chrono(t));

或者在 C++20 中,只需:

#include "date/date.h"
#include <chrono>
#include <ctime>