4

令人难以置信的是,在 C++ 中完成上述任务是多么困难。我正在寻找一种在保持毫秒精度的同时尽可能有效地做到这一点的方法。

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

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

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

4

6 回答 6

5

老问题的新答案。

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

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

基于<chrono>和使用这个免费的开源库的现代答案非常简单。该库已移植到 VS-2013、VS-2015、clang/libc++、macOS 和 linux/gcc。

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

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());
}

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

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

调用将本地时间floor<days> 截断days为. 这有效地创建了一个local_time等于当地午夜的时间。通过将其分配local_timezt,我们根本不会更改时区zt,但我们将local_timeof更改zt为午夜(因此sys_time也更改了它)。

我们可以从 with 中得到对应sys_time的。这是对应于当地午夜的 UTC 时间。然后从输入中减去它并将结果截断到所需的精度是一个简单的过程。ztzt.get_sys_time()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());
}

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

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 行代码,其他一切都是关于测试和处理罕见的例外情况。

于 2016-07-17T01:31:58.870 回答
1

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

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

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

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

于 2012-06-20T22:37:15.750 回答
0

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

要获得分解时间,请使用gmtimelocaltime与结果的“秒”部分一起使用gettimeofday

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 影响。

于 2012-06-20T21:58:24.193 回答
0

您可以运行localtime_r, 并mktime在调整结果后localtime_r计算“午夜”相对于 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);
}
于 2012-06-20T22:19:34.323 回答
0

提供的答案都没有真正满足我的需要。我想出了一些我认为应该可以独立工作的东西。如果有人发现任何错误或能想到更快的方法,请告诉我。当前代码需要 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);
}
于 2012-06-21T00:34:58.313 回答
0

我参考了帖子 [here] 并进行了更改,以便下面的函数可以返回自 GMT 时间午夜以来的毫秒数。

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();
}

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

于 2016-07-14T04:02:24.333 回答