【问题标题】:How to convert a UTC date & time to a time_t in C++?如何在 C++ 中将 UTC 日期和时间转换为 time_t?
【发布时间】:2012-09-03 09:32:44
【问题描述】:

我想将以数字形式给出的年、月、日等 UTC 日期和时间转换为 time_t。一些系统为此提供了诸如mkgmtimetimegm 之类的功能,但这不是标准的,并且在我的Solaris 系统上不存在。

到目前为止,我发现的唯一解决方案是使用 setenv 将本地时区设置为 UTC,然后调用 mktime。然而,这种方法不是线程安全的、速度慢、不可移植,甚至会在我的系统上产生内存泄漏。

我还看到了尝试使用gmtime 确定当前UTC 偏移然后将其添加到mktime 的结果的方法。但据我所见,所有这些方法都有差距。毕竟从当地时间到UTC的转换并不是唯一的。

您认为最好的解决方案是什么?

【问题讨论】:

  • @Fred Larson:这不是关于转换字符串,而是关于从单个数字计算 time_t。
  • 我明白了。我误解了这个问题。

标签: c++ time-t


【解决方案1】:

我决定实现我自己的 mkgmtime 版本,它比我想象的要容易。

const int SecondsPerMinute = 60;
const int SecondsPerHour = 3600;
const int SecondsPerDay = 86400;
const int DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool IsLeapYear(short year)
{
    if (year % 4 != 0) return false;
    if (year % 100 != 0) return true;
    return (year % 400) == 0;
}

time_t mkgmtime(short year, short month, short day, short hour, short minute, short second)
{
    time_t secs = 0;
    for (short y = 1970; y < year; ++y)
        secs += (IsLeapYear(y)? 366: 365) * SecondsPerDay;
    for (short m = 1; m < month; ++m) {
        secs += DaysOfMonth[m - 1] * SecondsPerDay;
        if (m == 2 && IsLeapYear(year)) secs += SecondsPerDay;
    }
    secs += (day - 1) * SecondsPerDay;
    secs += hour * SecondsPerHour;
    secs += minute * SecondsPerMinute;
    secs += second;
    return secs;
}

我主要担心mkgmtime 必须与gmtime 保持一致。这样gmtime(mktime(t)) 返回原始输入值。因此,我比较了 time_t 的 0 和 MAX_INT 之间所有 61 的倍数的结果,它们确实是相等的(至少在我的系统上)。所以上面的套路是正确的。

这个结果也意味着 C 库没有考虑闰秒,这本身是一件坏事,但对我的目的来说是件好事。这两个功能将长期保持一致。可以肯定的是,我使用此函数的 Timestamp 类总是对程序启动进行快速检查,并证明几个有意义的值的一致性。

【讨论】:

  • 我知道这已经有几个月了,但你的问题想要一些便携的东西,而这不是,因为你不能在 time_t 上代表自 1970 年以来的秒数,或者根本就是秒数,对于这个问题。 C 标准(以及作为参考的 C++ 标准)没有定义 如何 time_t 代表时间,只是它必须是能够这样做的真实类型。
  • 叹息。应该是“不能依赖 time_t 代表自1970 年以来的秒数”,并且“没有定义如何 time_t 代表时间”。早上太早了。
  • @PaulGriffiths:(又过了几个月;-))我明白你的意思了,但有没有更好的方法?至少这是我需要的“便携”,因为 time_t 是自 1970 年以来在 Linux、Windows 和 Solaris 上的秒数。
  • 这里有很多不同的方式来定义“更好”,但this answer of mine 提供了另一种方法,this code of mine 实现了该方法。
  • 回想起来,该答案仅给出了该方法的一部分,即在指定的秒数内计算 time_t 值的方法。另一部分是计算两个struct tms 之间的秒差,一个从localtime() 返回,另一个从gmtime() 返回。代码中的 cmets 应该有望使逻辑清晰。我认为该项目中有一些单元测试涵盖了一些边缘情况,以证明它有效。
【解决方案2】:

为了完整起见,这里有一个 mkgmtime() 版本,它以 struct tm* 作为参数:

static time_t mkgmtime(const struct tm *ptm) {
    time_t secs = 0;
    // tm_year is years since 1900
    int year = ptm->tm_year + 1900;
    for (int y = 1970; y < year; ++y) {
        secs += (IsLeapYear(y)? 366: 365) * SecondsPerDay;
    }
    // tm_mon is month from 0..11
    for (int m = 0; m < ptm->tm_mon; ++m) {
        secs += DaysOfMonth[m] * SecondsPerDay;
        if (m == 1 && IsLeapYear(year)) secs += SecondsPerDay;
    }
    secs += (ptm->tm_mday - 1) * SecondsPerDay;
    secs += ptm->tm_hour       * SecondsPerHour;
    secs += ptm->tm_min        * SecondsPerMinute;
    secs += ptm->tm_sec;
    return secs;
}

【讨论】:

    【解决方案3】:

    如上所述,虽然time_t 通常表示自 1970 年 1 月 1 日以来经过的秒数,但在任何地方都没有指定。使用不同内部表示的实现可能随时出现,任何对time_t 的内部工作进行假设的代码都将无法正常工作。

    经过一番思考,我想出了以下几点:

    time_t mkgmtime(struct tm * pt) {
        time_t ret;
    
        /* GMT and local time */
        struct tm * pgt, * plt;
    
        ret = mktime(pt);
    
        pgt = g_memdup(gmtime(ret), sizeof(struct tm));
        plt = g_memdup(localtime(ret), sizeof(struct tm));
    
        plt->tm_year -= pgt->tm_year - plt->tm_year;
        plt->tm_mon -= pgt->tm_mon - plt->tm_mon;
        plt->tm_mday -= pgt->tm_mday - plt->tm_mday;
        plt->tm_hour -= pgt->tm_hour - plt->tm_hour;
        plt->tm_min -= pgt->tm_min - plt->tm_min;
        plt->tm_sec -= pgt->tm_sec - plt->tm_sec;
    
        ret = mktime(plt);
    
        g_free(pgt);
        g_free(plt);
    
        return ret;
    }
    

    可以通过删除plt(使用pt 并省略localtime()g_free(plt) 调用)来进一步优化这一点。

    这应该适用于所有公开 mktime()gmtime()localtime() 的实现,包括跨 DST 切换日期。 (mktime() 将“规范化”超出范围的值,例如将 1 月 35 日变为 2 月 4 日;我还希望冬中夏令时 9:50 变为标准时间 8:50。)

    它确实存在一个潜在的错误:如果时区的 UTC 偏移量因 DST 标志中未反映的原因而更改,则切换时间周围的时间戳可能会被错误地解释:标准情况是立法更改其时区(例如立陶宛在独立后从苏联时代改为 CET,几年后改为 EET)。一些立法在仲夏有双倍 DST,每年循环 3 个不同的 UTC 偏移量,这是 DST 标志无法代表的。

    【讨论】:

      【解决方案4】:

      这是我在标准库中找不到任何可以为我执行此操作的内容后为自己提出的解决方案。此方法仅使用基本算法进行计算,使其比从 1970 年到提供的日期之间的每年循环要快得多。但是与之前的大多数答案一样,这取决于使用 Unix/Epoch 时间实现的 time_t,并且不适用于早于 1970 年的时间戳,这对我来说不是必需的。

      #include <ctime>
      #include <cassert>
      
      constexpr unsigned int count_leapyears(unsigned int year) {
          assert(year > 0);
          return year / 4 - year / 100 + year / 400;
      }
      
      time_t timeutc(tm utc) {
          assert(utc.tm_year >= 70);
          constexpr unsigned int const leaps_before_epoch = count_leapyears(1970);
          unsigned int leapdays = count_leapyears(utc.tm_year + 1899) - leaps_before_epoch;
          unsigned int unix_time;
      
          unix_time = ((utc.tm_year - 70) * 365 + leapdays) * 86400;
          unix_time += utc.tm_yday * 86400 + utc.tm_hour * 3600 + utc.tm_min * 60 + utc.tm_sec;
      
          return unix_time;
      }
      

      【讨论】:

      • 好答案,避免循环。如果你这样做static_cast&lt;std::time_t&gt; (utc.tm_year - 70),它似乎适用于从 1970 年到至少 2200 年的每个时间戳(秒)(我强制使用它),即不受 2038 年问题(签名 32 位溢出)和 2106 年问题的影响(无符号 32 位溢出),如果 std::time_t 是 64 位。
      • 你能添加一个函数time_t timeutc(int year, int month, int day, int hour, int second)
      猜你喜欢
      • 1970-01-01
      • 2016-11-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-21
      • 1970-01-01
      • 1970-01-01
      • 2022-06-21
      相关资源
      最近更新 更多