使用 std::chrono 提取自本地时间午夜以来经过的秒数

Use std::chrono to extract number of seconds passed since midnight in local time

提问人:glades 提问时间:5/1/2023 最后编辑:glades 更新时间:5/2/2023 访问量:537

问:

我想计算自开始以来的秒数。问题是这给了我UTC的日间起始时间,而不是我当地时区的起始时间。比较一下:std::chrono::floor

演示

#include <time.h>
#include <cstdio>
#include <iostream>
#include <cstdint>
#include <chrono>
#include <ctime>

int main() {
    setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
    tzset();

    const auto tp_daystart = std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now());
    const auto tp_now = std::chrono::system_clock::now();
    const auto daysec = std::chrono::duration_cast<std::chrono::seconds>(tp_now - tp_daystart);
    std::time_t ttp = std::chrono::system_clock::to_time_t(tp_now);
    std::time_t ttp_s = std::chrono::system_clock::to_time_t(tp_daystart);
    std::cout << "time start: " << std::ctime(&ttp_s) << " / time now: " << std::ctime(&ttp) << "seconds since start of day = " << daysec.count() << "\n";
}

这会产生:

time start: Mon May  1 02:00:00 2023
 / time now: Mon May  1 16:26:04 2023
seconds since start of day = 51964

从一天开始的秒数当然是错误的,因为它从凌晨 2 点到下午 4 点计算,而实际上它应该从 .,因为我确实想在这个时区计算自这一天开始以来的秒数!我该如何实现?00:00:00

C ++-Chrono 地板 C++20

评论


答:

5赞 Howard Hinnant 5/1/2023 #1

这个问题实际上有一些棘手的细节,当夏令时等奇怪的事情使事情复杂化时,必须决定你到底想要发生什么。但无论你想发生什么,它都可以在时间内完成。

我假设你想要自一天开始以来的物理秒数。这意味着,如果在午夜和现在之间有 UTC 偏移量变化(例如,减去一小时),则将考虑此减法。

这意味着我们需要:

  1. 立即获取 UTC 时间。
  2. 获取当地午夜的 UTC 时间。
  3. 减去两个 UTC 时间。

由于您对秒精度感兴趣,我们将从一开始就截断它,然后不再担心它:

#include <chrono>
#include <format>
#include <iostream>

int
main()
{
    using namespace std;
    using namespace chrono;

    auto utc_now = floor<seconds>(system_clock::now());

出于可读性目的,我使用 local 来减少冗长。using directives

接下来获取对应的本地时间:utc_now

    auto local = zoned_time{current_zone(), utc_now};

current_zone()查找当前设置的时区。并且只是一个方便的“对”,它将 a 和 a 结合在一起。可以使用此结构来提取本地时间。或者你可以直接格式化本地时间:zoned_timetime_zone const*sys_time

    cout << "time now  : " << format("{:%a %b %e %T %Y}", local) << '\n';

对我来说,这只是输出我的本地时间和日期:

time now  : Mon May  1 10:56:07 2023

现在,要获取本地午夜,截断必须以本地时间进行,而不是以系统时间进行:days

    local = floor<days>(local.get_local_time());

这将提取本地时间,将其截断为,然后将该本地时间分配回 .这将更改基础 UTC 时间点 (sys_time),但不会更改时区。dayszoned_time

现在您可以再次格式化:local

    cout << "time start: " << format("{:%a %b %e %T %Y}", local) << '\n';

输出示例:

time start: Mon May  1 00:00:00 2023

最后,从本地午夜(存储在 中)中提取 UTC 时间,并从起始 UTC 时间中减去它:local

    auto delta = utc_now - local.get_sys_time();
    cout << "seconds since start of day = " << delta << '\n';

输出示例:

seconds since start of day = 39367s

还有一个复杂情况:如果存在导致本地时间完全跳过本地午夜的 UTC 偏移更改,该怎么办?或者,如果当地有两个午夜呢?

你想如何处理这个问题?

有几种可能性:

  1. 您可以忽略这种可能性,因为您确定您的本地时区永远不会这样做。在这种情况下,上面的代码可以按原样进行。如果确实发生,则会在代码行上抛出异常,该代码行将本地午夜分配回 。local

  2. 如果有两个午夜,你要选择第一个,如果午夜为零,你要从午夜之后的第一个本地时间开始计算。例如,如果本地时间从 23:30 跳到 00:30,则从本地时间 00:30 开始计数。

在这种情况下,更改:

    local = floor<days>(local.get_local_time());

自:

    local = zoned_time{local.get_time_zone(),
                       floor<days>(local.get_local_time()),
                       choose::earliest};

这:

  • 提取本地时间,并将其截断为精确度。days
  • 形成与 相同的time_zone的新 。如果您使用的是移动移动设备,您不想再次拨打电话。zoned_timelocalcurrent_zone()
  • 用于选择第一个本地时间,以防存在从本地到 UTC 的两个映射。这也将映射到与本地时间间隔(午夜零点)相关的 UTC 时间点。choose::earliest
  • 将此新名称重新分配到 .zoned_timelocal

现在永远不会抛出异常,你总是会从今天的第一个时刻开始计算秒数,即使也有昨天的片段。

如果您想要第二个午夜,以便永远不会计算昨天的点数,则更改为 .choose::earliestchoose::latest


如果您不想实际计算物理秒数,而是“日历秒数”,那么请用当地时间而不是UTC进行算术运算。矛盾的是,这不会涉及在白天更改 UTC 偏移量的复杂性,因此更简单。在此计算中,凌晨 4 点将始终是午夜后的 4 小时,即使凌晨 2 点的 UTC 偏移量更改将本地时间设置回凌晨 1 点也是如此。

auto utc_now = floor<seconds>(system_clock::now());
auto local_now = zoned_time{current_zone(), utc_now}.get_local_time();
auto local_midnight = floor<days>(local_now);
auto delta = local_now - local_midnight;
cout << "time now  : " << format("{:%a %b %e %T %Y}", local_now) << '\n';
cout << "time start: " << format("{:%a %b %e %T %Y}", local_midnight) << '\n';
cout << "seconds since start of day = " << delta << '\n';

在此版本中,本地时间永远不会映射回 UTC,因此没有机会发现所述映射不是唯一的,随后引发异常。

需要明确的是,此代码的所有三个不同版本几乎总是给出相同的结果。只有当在“现在”和上一个本地午夜之间进行 UTC 偏移量更改时,这些不同的版本才会给出不同的结果。

评论

0赞 Yakk - Adam Nevraumont 5/2/2023
以防万一由于 2271 年的计时大战而使本地时钟变得完全非线性,您可以从 UTC 现在以 1 秒的间隔向后迭代,计算本地时间变为非今天之前有多少。;)(又名,时间很烂)
0赞 glades 5/2/2023
感谢您的详细回答。在这种情况下,您错了,我实际上是在寻找自午夜以来经过的“表观秒数”,但您的回答为我提供了足够的洞察力来实现这一目标。如果我是对的,我可以轻松地使用带有 UTC 午夜的 zoned_time{} 提取当地时间的午夜,然后提取当地午夜。
0赞 Howard Hinnant 5/2/2023
啊,那么我答案中代码的最终版本可以满足您的需求。是的,你正确地描述了这个过程。在处理时间的问题中,问题陈述比问题解决方案更难的情况并不少见。:-)
1赞 Yakk - Adam Nevraumont 5/1/2023 #2

您正在寻找 .std::chrono::time_zone

setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
tzset();
std::chrono::time_zone const* time_zone = std::chrono::current_zone();
if (!time_zone)
  return -1;

一旦你有一个有效的,这很容易time_zone

const auto local = tz.to_local(sys);
const auto local_daystart = std::chrono::floor<std::chrono::days>(local);
const auto sys_daystart = tz.to_sys(local_daystart);

time_zone 中添加。您可以直接在系统数据库中搜索特定时区,也可以使用进程的当前时区(如上所述)。

你必须接受失败,因为有时系统没有时区。