c ++,从当地时间午夜开始获得毫秒数

时间:2012-06-20 21:41:59

标签: c++ time dst

令人难以置信的是,在C ++中完成上述工作有多困难。我正在寻找一种尽可能高效地完成这项工作的方法,同时仍保持毫秒精度。

到目前为止,我所拥有的解决方案要么需要大量的代码和函数调用才能使实现变慢,或者它们要求我每年更改一次代码以解决夏令时问题。

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

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

6 个答案:

答案 0 :(得分:4)

旧问题的新答案。

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

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

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

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

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<days> 的调用会将本地时间截断,精度为days。这有效地创建了一个等于当地午夜的local_time。通过将此local_time分配回zt,我们根本不会更改zt的时区,但我们会将local_time的{​​{1}}更改为午夜(因此也改变了它的zt。)

我们可以使用sys_timesys_time中获取相应的zt。这是与当地午夜相对应的UTC时间。然后,从输入zt.get_sys_time()中减去此值并将结果截断为所需的精度是一个简单的过程。

如果本地午夜不存在或模糊(有两个),则此代码将抛出一个从t派生的异常,其中包含非常丰富的std::exception

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

what()

为了确保我们的功能正常工作,输出一些示例日期是值得的。通过指定时区(我将使用“America / New_York”)以及我知道正确答案的一些本地日期/时间,可以轻松完成此操作。为了在测试中提供良好的语法,另一个std::cout << since_local_midnight().count() << "ms\n"; 有助于:

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和时区(精确到秒),并将其转发给我们的实现。

zoned_time

这是在冬季中午凌晨3点输出:

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";

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

我可以通过为2016-01-15 03:00:00 EST is 10800000ms after midnight 分配新的本地时间来再次运行测试。以下是在“春季前进”夏令时转换(3月第2个星期日)之后的凌晨3点:

zt

输出:

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

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

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

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

最后一个例子就是在从夏令时回归到标准之后,我们花了4个小时:

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

如果您愿意,可以避免在午夜不存在或含糊不清的情况下发生异常。你必须事先决定这个含糊不清的案例:你想从第一个午夜或第二个开始衡量吗?

以下是从第一个开始测量的方法:

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,它将从午夜所在的当地时间间隔边界的单个UTC时间点开始测量。这可能非常混乱,这就是默认的原因行为只是抛出一个非常有用的choose

的异常
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()); 是合理的。但是,如果您想在不丢弃的情况下执行此操作,则此库中存在一个低级API来实现此目的。

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

答案 1 :(得分:1)

调整localtime_r的结果后,您可以运行mktimelocaltime_r以计算相对于纪元的“午夜”值。

修改:将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为快速路径,并舍入到最接近的msec。

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

答案 2 :(得分:1)

这实际上取决于你为什么需要“从午夜开始的毫秒”以及你打算用它做什么。

话虽如此,你需要考虑到这样一个事实,即凌晨3点并不意味着午夜,当DST参与时的3小时。如果由于某种原因你真的需要“午夜以来的毫秒”,你可以在午夜获得一个大纪元时间,在凌晨3点获得另一个大纪元时间,并减去这两个。

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

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

答案 3 :(得分:0)

如果你在Linux上,gettimeofday给出了自纪元以来的秒/微秒数,这可能会有所帮助。但这与DST没有任何关系,因为DST只与故障时间(即年,月,日,时,分,秒)有关。

要获得分解时间,请将gmtimelocaltimegettimeofday的结果中的“秒”部分一起使用:

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

localtime给出了您当地时区的细分时间,但可能会受到夏令时的影响。 gmtime给出了UTC中的分解时间,这对DST免疫。

答案 4 :(得分: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);
}

答案 5 :(得分: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();
}

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