【问题标题】:Convert UTC time_t to UTC tm将 UTC time_t 转换为 UTC tm
【发布时间】:2021-07-27 16:05:17
【问题描述】:

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

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

【问题讨论】:

  • 您是否按照localtime 参考页中的说明设置环境变量TZen.cppreference.com/w/cpp/chrono/c/localtime
  • 抱歉,刚刚进行了编辑。我正在寻找跨平台的东西,我不相信 Windows 有那个环境变量
  • "如果我使用 gmtime,它会得到错误的时间,会因时区差异而关闭。" --> 也许您的期望不正确,因为gmtime() 应该是正确的方法。使用的邮政编码,示例time_t 输入,完整的struct tm 输出,预期输出以获得好的答案。
  • 这是什么意思:except that tm_isdst may be set resulting in the time being off an hour?你能提供一个例子吗,minimal reproducible example 如果可能的话。
  • 值 0 表示自 1970 年 1 月 1 日 00:00:00 UTC 以来的零秒。因此,gmtime 将 UTC 时间转换为 UTC 时间,这是应该的。它不会也不应该调整任何东西。如果您有 UTC 格式的 time_tgmtime 应将其转换为 UTC 格式的 struct tm,正如您所要求的那样。如果您认为不是这种情况,请显示特定值 time_t 和特定 struct tm 它会被转换。

标签: c++ time data-conversion


【解决方案1】:

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

实际上,通过<chrono>time_t 转换为UTC tm 比使用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_clock 但具有 seconds 精度。

此函数所做的只是将类型从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 纪元之前的time_points)。这只不过是除以 86400。

year_month_day ymd = td;

上面的第二行计算自纪元以来的天数,并将其转换为{year, month, day} 数据结构。这是大部分计算发生的地方。

hh_mm_ss<seconds> tod{tp - td};  // <seconds> can be omitted in C++17

上面的第三行从秒精度time_point 中减去天精度time_point,得到自UTC 午夜以来的std::chrono::seconds 持续时间。然后将这个持续时间分解为{hours, minutes, seconds} 数据结构(hh_mm_ss 类型)。在 C++17 中,这一行可以选择简化为:

hh_mm_ss tod{tp - td};  // <seconds> can be omitted in C++17

现在to_tm简单的提取字段,根据C API填写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 初始化为零很重要,因为不同的平台有额外的 tm 数据成员作为扩展,最好给定值 0。

tm t{};

对于小时、分钟和秒,只需从tod 中提取适当的chrono::duration,然后使用.count() 成员函数提取整数值:

t.tm_sec  = tod.seconds().count();
t.tm_min  = tod.minutes().count();
t.tm_hour = tod.hours().count();

day 显式转换为 unsigned,这是 C API 不会给 tm 数据成员带来意外偏差的少数几个地方之一:

t.tm_mday = unsigned{ymd.day()};

tm_mon 被定义为“自一月以来的几个月”,因此必须考虑偏差。可以从month 中减去January,得到monthsduration。这是一个chrono::duration,可以用.count()成员函数提取整数值:

t.tm_mon  = (ymd.month() - January).count();

同样,tm_year 是自 1900 年以来的年份:

t.tm_year = (ymd.year() - 1900_y).count();

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

t.tm_wday = weekday{td}.c_encoding();

tm_yday 是自 1 月 1 日以来的天数 -- [0, 365]。这很容易通过从日期精度 time_point (td) 中减去一年中的第一天,创建一个 days chrono::duration 来计算:

t.tm_yday = (td - sys_days{ymd.year()/January/1}).count();

最后,tm_isdst 应设置为 0,表示夏令时无效。从技术上讲,这一步在零初始化 tm 时已经完成,但出于可读性目的在此重复:

t.tm_isdst = 0;

to_tm 可以通过以下方式移植到 C++20:

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

使用示例:

给定time_t,您可以使用这些函数将其转换为 UTC tm

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

这里是必要的标题:

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

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

#include <chrono>
#include <ctime>

【讨论】:

  • 谢谢,这是很好的信息。我一直犹豫要不要学习 chrono 的东西,因为 C++11 和 14 的跨平台支持还不是很好,但在某些时候我们需要强制它们升级。欣赏这个答案的深度
  • 如果有帮助,这里有一个关于 C++11/14 的视频教程 &lt;chrono&gt;: youtube.com/watch?v=P32hvk8b13M 这将花费你一个小时。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-22
  • 2015-05-06
  • 2012-09-13
  • 2014-02-02
  • 2012-09-03
相关资源
最近更新 更多