【问题标题】:c++, getting milliseconds since midnight in local timec ++,自当地时间午夜以来获得毫秒数
【发布时间】:2012-06-23 03:00:16
【问题描述】:

很难想象在 C++ 中完成上述任务是多么困难。我正在寻找一种在保持毫秒精度的同时尽可能高效地执行此操作的方法。

到目前为止,我的解决方案要么需要大量代码和函数调用,从而导致实施速度变慢,要么需要我每年更改两次代码以考虑夏令时。

将在其上运行的计算机使用 ntp 同步,并且应该可以直接访问为 DST 调整的本地时间。有这方面专业知识的人可以分享一些解决方案吗?

我的平台是 CentOS5,g++ 4.1.2,Boost 1.45,解决方案不需要是可移植的,可以是平台特定的。它只需要快速并且避免每年两次代码更改。

【问题讨论】:

  • 如果您能够获得这些信息,您将解决什么问题?
  • 为什么您必须每年更改两次代码才能考虑 DST?你依赖什么样的错误假设?
  • 你说,“我目前的解决方案”...你能列出它们吗?
  • 你试过浏览boost's libraries吗?该链接看起来非常相关且简短。
  • @user788171:你真的需要那种性能吗?如果这是为了保持时钟时间,您不需要速度非常快的东西。如果你需要一个快速的时基,你会使用处理器时钟时间,而不是挂钟时间。

标签: c++ time dst


【解决方案1】:

旧问题的新答案。

新答案的理由:我们现在有了更好的工具。

我假设期望的结果是自当地午夜以来的“实际”毫秒数(当自午夜以来 UTC 偏移发生变化时得到正确答案)。

基于<chrono> 并使用此free, open-source library 的现代答案非常容易。该库已移植到 VS-2013、VS-2015、clang/libc++、macOS 和 linux/gcc。

为了使代码可测试,我将启用 API 以从任何 std::chrono::system_clock::time_point 中的任何 IANA time zone 获取自午夜以来的时间(以毫秒为单位)。

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone);

然后在这个可测试的原语之上轻松编写自本地时区午夜以来的当前时间:

inline
std::chrono::milliseconds
since_local_midnight()
{
    return since_local_midnight(std::chrono::system_clock::now(), 
                                date::current_zone());
}

写出问题的实质相对简单:

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone)
{
    using namespace date;
    using namespace std::chrono;
    auto zt = make_zoned(zone, t);
    zt = floor<days>(zt.get_local_time());
    return floor<milliseconds>(t - zt.get_sys_time());
}

首先要做的是创建一个zoned_time,它实际上什么都不做,只是将zonet 配对。这种配对主要是为了使语法更好。它实际上不做任何计算。

下一步是获取与t 关联的本地时间。这就是zt.get_local_time() 所做的。这将具有 t 的任何精度,除非 t 比秒更粗,在这种情况下,本地时间的精度为秒。

floor&lt;days&gt; 的调用截断本地时间,精确到days。这有效地创建了一个等于当地午夜的local_time。通过将此local_time 分配回zt,我们根本不会更改zt 的时区,但我们将ztlocal_time 更改为午夜(因此也更改了它的sys_time )。

我们可以通过zt.get_sys_time()zt中得到对应的sys_time。这是对应于当地午夜的 UTC 时间。然后从输入t 中减去此值并将结果截断到所需的精度是一个简单的过程。

如果当地的午夜不存在或不明确(有两个),则此代码将引发源自std::exception 的异常,并带有非常丰富的what()

从当地时间午夜开始的当前时间可以简单地打印出来:

std::cout << since_local_midnight().count() << "ms\n";

为确保我们的函数正常工作,值得输出一些示例日期。这最容易通过指定时区(我将使用“America/New_York”)和一些我知道正确答案的本地日期/时间来完成。为了在测试中提供更好的语法,另一个since_local_midnight 有帮助:

inline
std::chrono::milliseconds
since_local_midnight(const date::zoned_seconds& zt)
{
    return since_local_midnight(zt.get_sys_time(), zt.get_time_zone());
}

这只是从zoned_time 中提取system_clock::time_point 和时区(精度为秒),并将其转发给我们的实现。

auto zt = make_zoned(locate_zone("America/New_York"), local_days{jan/15/2016} + 3h);
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

这是冬天中的凌晨 3 点,输出:

2016-01-15 03:00:00 EST is 10800000ms after midnight

并且是正确的 (10800000ms == 3h)。

只需将新的本地时间分配给zt,我就可以再次运行测试。以下是“春季前进”夏令时转换(3 月的第 2 个星期日)之后的凌晨 3 点:

zt = local_days{sun[2]/mar/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

这个输出:

2016-03-13 03:00:00 EDT is 7200000ms after midnight

由于跳过了从凌晨 2 点到凌晨 3 点的当地时间,因此正确输出了自午夜以来的 2 小时。

仲夏的一个例子让我们回到午夜后 3 小时:

zt = local_days{jul/15/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

2016-07-15 03:00:00 EDT is 10800000ms after midnight

最后一个例子是在秋季从夏令时转换回标准之后给我们 4 小时:

zt = local_days{sun[1]/nov/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

2016-11-06 03:00:00 EST is 14400000ms after midnight

如果需要,您可以避免在午夜不存在或不明确的情况下出现异常。在模棱两可的情况下,您必须事先决定:您要从第一个午夜开始测量还是从第二个午夜开始测量?

这是您从一开始就开始测量的方法:

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone)
{
    using namespace date;
    using namespace std::chrono;
    auto zt = make_zoned(zone, t);
    zt = make_zoned(zt.get_time_zone(), floor<days>(zt.get_local_time()),
                    choose::earliest);
    return floor<milliseconds>(t - zt.get_sys_time());
}

如果您想从第二个午夜开始测量,请改用choose::latest。如果午夜不存在,您可以使用choose,它将从与午夜所在的本地时间间隔接壤的单个 UTC 时间点开始测量。这一切都可能非常令人困惑,这就是为什么默认行为是只是抛出一个异常信息非常丰富的what()

zt = make_zoned(locate_zone("America/Asuncion"), local_days{sun[1]/oct/2016} + 3h);
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

what():
2016-10-02 00:00:00.000000 is in a gap between
2016-10-02 00:00:00 PYT and
2016-10-02 01:00:00 PYST which are both equivalent to
2016-10-02 04:00:00 UTC

如果您使用choose::earliest/latest 公式,而不是上述what() 的异常,您会得到:

2016-10-02 03:00:00 PYST is 7200000ms after midnight

如果你想做一些真正棘手的事情,比如在不存在的午夜使用choose,但在不存在的午夜抛出异常,这也是可能的:

auto zt = make_zoned(zone, t);
try
{
    zt = floor<days>(zt.get_local_time());
}
catch (const date::nonexistent_local_time&)
{
    zt = make_zoned(zt.get_time_zone(), floor<days>(zt.get_local_time()),
                    choose::latest);
}
return floor<milliseconds>(t - zt.get_sys_time());

因为遇到这种情况确实很少见(例外),所以使用try/catch 是合理的。但是,如果您想完全不抛出任何东西,那么这个库中有一个低级 API 可以实现这一点。

最后请注意,这个冗长的答案实际上是大约 3 行代码,其他一切都是关于测试和处理罕见的例外情况。

【讨论】:

    【解决方案2】:

    这实际上取决于您为什么需要“自午夜以来的毫秒数”以及您打算将其用于什么。

    话虽如此,但您需要考虑到凌晨 3 点并不真正意味着从午夜开始的 3 小时,当涉及 DST 时。如果您出于某种原因确实需要“自午夜以来的毫秒数”,您可以在午夜获得一个 Epoch 时间,在凌晨 3 点获得另一个,然后将两者相减。

    但是,在某些情况下,“午夜”的概念可能并不那么稳定。如果一个地区的规则是在 DST 结束时从凌晨 1 点回退到午夜,那么您一天中有两个午夜。

    所以我真的怀疑你对“午夜”的依赖。通常,这些细分时间仅用于显示和人类理解,所有内部计时均以纪元时间完成。

    【讨论】:

    • 股市开盘930,代码需要930开盘。
    • @user788171:为此,您只需要检查故障时间是否为09:30:00,不是吗?甚至不是午夜后市场开放 9 小时 30 分钟。
    • 传入的时间戳是午夜过后的毫秒数,同步代码所需的毫秒精度+精度。 gettimeofday 和 clock_gettime 是唯一足够精确的选项。
    【解决方案3】:

    如果您使用的是 Linux,gettimeofday 会给出自 Epoch 以来的秒数/微秒数,这可能会有所帮助。但这实际上与 DST 没有任何关系,因为 DST 仅与细分时间有关(即年、月、日、小时、分钟、秒)。

    要获得细分时间,请使用gmtimelocaltimegettimeofday 结果的“秒”部分:

    struct timeval tv;
    gettimeofday(&tv, 0);
    struct tm *t = localtime(&tv.tv_sec);  // t points to a statically allocated struct
    

    localtime 提供您当地时区的细分时间,但它可能容易受到 DST 的影响。 gmtime 以 UTC 格式给出故障时间,不受 DST 影响。

    【讨论】:

    • 所以这是问题的核心。 gettimeofday 的输出不是我的当地时间。要转换为我的本地时间,我需要知道 UTC 和 EST 之间的当前时差,但这每年更改两次。如果我通过查找本地时间并与 gettimeofday 中的 UTC 时间进行比较来提取这种差异,这是一个额外的调用,会大大降低函数的速度。
    • @user788171:你真的需要那种性能吗?如果这是为了保持时钟时间,您不需要速度非常快的东西。
    • 不仅如此。如果我按照我描述的方式进行操作(例如同时使用 gettimeofday 和 localtime),那么在两个小时之间,我会得到不同的结果,因为 UTC 和本地时间之间的差异是很少见的。
    • @user788171:很明显,您可能在同一天凌晨 1 点处于 UTC-0400 和凌晨 3 点处于 UTC-0300。但是很自然,也没什么不好。
    • 是不是您的函数调用已经有点慢,因此给出了一个有点旧的毫秒值,表明您毕竟不是那种精度?为什么不能精确到 1 秒?
    【解决方案4】:

    所提供的答案都没有真正满足我的需要。我想出了一些我认为应该可以独立工作的东西。如果有人发现任何错误或能想到更快的方法,请告诉我。当前代码需要 15 微秒才能运行。我向 SO 提出挑战,让他们更快地做出一些事情(我真的希望 SO 成功 =P)

    inline int ms_since_midnight()
    {
      //get high precision time
      timespec now;
      clock_gettime(CLOCK_REALTIME,&now);
    
      //get low precision local time
      time_t now_local = time(NULL);
      struct tm* lt = localtime(&now_local);
    
      //compute time shift utc->est
      int sec_local = lt->tm_hour*3600+lt->tm_min*60+lt->tm_sec;
      int sec_utc = static_cast<long>(now.tv_sec) % 86400;
      int diff_sec; //account for fact utc might be 1 day ahead
      if(sec_local<sec_utc) diff_sec = sec_utc-sec_local;
      else diff_sec = sec_utc+86400-sec_local;
      int diff_hour = (int)((double)diff_sec/3600.0+0.5); //round to nearest hour
    
      //adjust utc to est, round ns to ms, add
      return (sec_utc-(diff_hour*3600))*1000+(int)((static_cast<double>(now.tv_nsec)/1000000.0)+0.5);
    }
    

    【讨论】:

      【解决方案5】:

      您可以在调整localtime_r的结果后运行localtime_rmktime来计算“午夜”相对于Epoch的值。

      编辑:now 传递到例程中以避免对time 的不必要调用。

      time_t global_midnight;
      bool checked_2am;
      
      void update_global_midnight (time_t now, bool dst_check) {
          struct tm tmv;
          localtime_r(&now, &tmv);
          tmv.tm_sec = tmv.tm_min = tmv.tm_hour = 0;
          global_midnight = mktime(&tmv);
          checked_2am = dst_check || (now >= (global_midnight + 2*3600));
      }
      

      假设 global_midnight 最初是 0。然后,您将在凌晨 2 点和第二天调整它的值,使其与 DST 保持同步。当您调用clock_gettime 时,您可以计算与global_midnight 的差异。

      编辑:由于 OP 想要对例程进行基准测试,因此为假定 true 为快速路径的编译器调整代码,并四舍五入到最接近的毫秒。

      unsigned msecs_since_midnight () {
          struct timespec tsv;
          clock_gettime(CLOCK_REALTIME, &tsv);
          bool within_a_day = (tsv.tv_sec < (global_midnight + 24*3600));
          if (within_a_day)
              if (checked_2am || (tsv.tv_sec < (global_midnight + 2*3600))
                  return ((tsv.tv_sec - global_midnight)*1000
                          + (tsv.tv_nsec + 500000)/1000000);
          update_global_midnight(tsv.tv_sec, within_a_day);
          return ((tsv.tv_sec - global_midnight)*1000
                  + (tsv.tv_nsec + 500000)/1000000);
      }
      

      【讨论】:

      • @user788171: time 仅用于计算midnight in terms of EPOCH secondsclock_gettime 获取高分辨率时间和与midnight 的计算差异。
      • @user788171:我已经更新了答案。问候
      • 这可行,我最终可能会切换到类似的东西,但同时我发布了另一个我更喜欢的解决方案,因为它是一个功能且更易于使用。
      • @user788171:我认为无论如何您的代码都无法超越我的解决方案的快速路径。如果global_midnight 在此例程运行之前被初始化,则此例程仅包含对clock_gettime 的调用和一些简单的整数数学运算。您的例程也将始终调用 localtime,并在此基础上进行浮点数学运算。
      【解决方案6】:

      我参考了 [此处] 的帖子并进行了更改,以便以下函数可以返回自格林威治标准时间午夜以来的毫秒数。

      int GetMsSinceMidnightGmt(std::chrono::system_clock::time_point tpNow) {
          time_t tnow = std::chrono::system_clock::to_time_t(tpNow);
          tm * tmDate = std::localtime(&tnow);
          int gmtoff = tmDate->tm_gmtoff;
          std::chrono::duration<int> durTimezone(gmtoff); // 28800 for HKT
          // because mktime assumes local timezone, we shift the time now to GMT, then fid mid
          time_t tmid = std::chrono::system_clock::to_time_t(tpNow-durTimezone);
          tm * tmMid = std::localtime(&tmid);
          tmMid->tm_hour = 0;
          tmMid->tm_min = 0;
          tmMid->tm_sec = 0;
          auto tpMid = std::chrono::system_clock::from_time_t(std::mktime(tmMid));
          auto durSince = tpNow - durTimezone - tpMid;
          auto durMs = std::chrono::duration_cast<std::chrono::milliseconds>(durSince);
          return durMs.count();
      }
      

      如果你想知道当地时间,那就容易多了。

      【讨论】:

        猜你喜欢
        • 2010-11-07
        • 1970-01-01
        • 1970-01-01
        • 2018-03-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-08-12
        • 1970-01-01
        相关资源
        最近更新 更多