【问题标题】:std::mktime and timezone infostd::mktime 和时区信息
【发布时间】:2010-10-06 13:00:55
【问题描述】:

我正在尝试使用 C++ 中的 std::mktime 将我作为 UTC 字符串接收的时间信息转换为时间戳。我的问题是 <ctime> / <time.h> 没有转换为 UTC 的功能; mktime 只会将时间戳返回为本地时间。

所以我需要找出时区偏移并将其考虑在内,但我找不到不涉及将整个代码移植到boost::date_time 的独立于平台的方式。是否有一些我忽略的简单解决方案?

【问题讨论】:

  • "mktime 只会将时间戳返回为本地时间" - 需要明确的是,时间戳是一个 UNIX 时间戳(对它们来说时区完全不相关)。发生的情况是 mktime 将其 input 解释为本地时间。

标签: c++


【解决方案1】:
timestamp = mktime(&tm) - _timezone;

或平台无关的方式:

 timestamp = mktime(&tm) - timezone;

如果你看00117行的in the source of mktime(),时间会转换为当地时间:

seconds += _timezone;

【讨论】:

  • 这只是一些实现。您将从调用代码中的哪里获得_timezone
  • 我只是使用了“时区”(不是“_timezone”)。 “时区”在 . 中定义
  • @LightnessRacesinOrbit, _timezone 是一个全局变量。它只是存在。
  • 一些实现中。这不是可以依赖的东西。将其视为简单地存在。这就是它的名称以下划线开头的原因。
  • 这不考虑夏令时。如果时间戳是在夏令时创建的,那么您需要 3600 秒的结果。问题是,如何确定要转换的时间戳是否是在夏令时制作的?
【解决方案2】:

mktime() 使用 tzname 来检测时区。 tzset() 从 TZ 环境变量初始化 tzname 变量。如果 TZ 变量出现在环境中,但其值为空或无法正确解释其值,则使用 UTC。

根据timegm manpage 的可移植(非线程安全)版本

   #include <time.h>
   #include <stdlib.h>

   time_t
   my_timegm(struct tm *tm)
   {
       time_t ret;
       char *tz;

       tz = getenv("TZ");
       setenv("TZ", "", 1);
       tzset();
       ret = mktime(tm);
       if (tz)
           setenv("TZ", tz, 1);
       else
           unsetenv("TZ");
       tzset();
       return ret;
   }

Eric S Raymond 在他的文章Time, Clock, and Calendar Programming In C 中发布了线程安全版本

time_t my_timegm(register struct tm * t)
/* struct tm to seconds since Unix epoch */
{
    register long year;
    register time_t result;
#define MONTHSPERYEAR   12      /* months per calendar year */
    static const int cumdays[MONTHSPERYEAR] =
        { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };

    /*@ +matchanyintegral @*/
    year = 1900 + t->tm_year + t->tm_mon / MONTHSPERYEAR;
    result = (year - 1970) * 365 + cumdays[t->tm_mon % MONTHSPERYEAR];
    result += (year - 1968) / 4;
    result -= (year - 1900) / 100;
    result += (year - 1600) / 400;
    if ((year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0) &&
        (t->tm_mon % MONTHSPERYEAR) < 2)
        result--;
    result += t->tm_mday - 1;
    result *= 24;
    result += t->tm_hour;
    result *= 60;
    result += t->tm_min;
    result *= 60;
    result += t->tm_sec;
    if (t->tm_isdst == 1)
        result -= 3600;
    /*@ -matchanyintegral @*/
    return (result);
}

【讨论】:

  • 另见this answer to another question,它说要改用setenv("TZ", "UTC", 1);,这样它就可以在Windows上运行。
  • 请注意,这个解决方案是天啊,所以不是线程安全的。如果你想从两个不同的线程调用它,你必须把它包裹在一个互斥锁中。
  • 不可能使任何依赖于环境变量的东西成为线程安全的:-(
  • @Erroneous:那只会引入更多错误,getenv()setenv() 不是线程安全的。
  • year = 1900L + ... 中使用1900L,否则将year 设为long 有什么意义?也许year = 1900L + t-&gt;tm_year + t-&gt;tm_mon / MONTHSPERYEAR; int mon = t-&gt;tm_mon % MONTHSPERYEAR; if (mon &lt; 0) { mon += MONTHSPERYEAR; year--; 然后在其余代码中使用mon 而不是t-&gt;tm_mon
【解决方案3】:

我昨天也遇到了同样的问题,通过搜索文档“man mktime”:

函数 mktime() 和 timegm() 将分解的时间(在 *timeptr 指向的结构中)转换为具有相同编码的时间值 time(3) 函数返回的值的个数(即从 Epoch 到 UTC 的秒数)。 mktime() 函数根据以下方式解释输入结构 当前时区设置(参见 tzset(3))。 timegm() 函数将输入结构解释为表示通用协调时间 (UTC)。

短:

您应该使用 timegm,而不是使用 mktime。

问候,

【讨论】:

  • 适用于 Linux 和 BSD。在 Windows 上使用 _mkgmtime。
【解决方案4】:

mktime 假定日期值在本地时区。因此,您可以预先更改时区环境变量(setenv)并获取 UTC 时区。

Windows tzset

也可以试试看各种自制的utc-mktimes, mktime-utcs, etc.

【讨论】:

  • 谢谢!我在 google 搜索结果中看到了类似的东西,但它在 windows 上有效吗?
  • 不不不,永远不要在进程中更改环境变量。绝不。如果您传递给子进程,要么继承所有这些,要么在 fork 之前复制。没有线程安全的方法可以更改环境变量。
【解决方案5】:

如果您尝试在多线程程序中执行此操作并且不想处理锁定和解锁互斥锁(如果您使用环境变量方法,则必须这样做),有一个名为 timegm 的函数可以执行此操作.它不是便携式的,所以这里是来源: http://trac.rtmpd.com/browser/trunk/sources/common/src/platform/windows/timegm.cpp

int is_leap(unsigned y) {
    y += 1900;
    return (y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0);
}

time_t timegm (struct tm *tm)
{
    static const unsigned ndays[2][12] = {
        {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
        {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    };
    time_t res = 0;
    int i;

    for (i = 70; i < tm->tm_year; ++i)
        res += is_leap(i) ? 366 : 365;

    for (i = 0; i < tm->tm_mon; ++i)
        res += ndays[is_leap(tm->tm_year)][i];
    res += tm->tm_mday - 1;
    res *= 24;
    res += tm->tm_hour;
    res *= 60;
    res += tm->tm_min;
    res *= 60;
    res += tm->tm_sec;
    return res;
}

【讨论】:

  • 如果tm_mon 在闰年设置为12,这可能会触发越界异常,这是mktime 中明年一月的常见语义(如果没有,结果可能非常错误)。它进行了许多循环和函数调用(2015 年 12 月为 57 个),甚至更昂贵的模运算(近 100 个)。它假定time_t 以秒为单位,这不是刻在石头上的。
【解决方案6】:

使用_mkgmtime,它会处理一切。

【讨论】:

  • 这可以在哪里获得和记录?
  • 这仅适用于 Windows。在 linux 和 BSD 上,使用 timegm。
【解决方案7】:

这是一个简单的、经过测试的、希望可移植的代码,从可调整的 UTC 年份开始后将 struct tm 转换为秒数,无需临时更改时区。

// Conversion from UTC date to second, signed 64-bit adjustable epoch version.
// Written by François Grieu, 2015-07-21; public domain.

#include <time.h>                   // needed for struct tm
#include <stdint.h>                 // needed for int_least64_t
#define MY_EPOCH    1970            // epoch year, changeable
typedef int_least64_t my_time_t;    // type for seconds since MY_EPOCH

// my_mktime  converts from  struct tm  UTC to non-leap seconds since 
// 00:00:00 on the first UTC day of year MY_EPOCH (adjustable).
// It works since 1582 (start of Gregorian calendar), assuming an
// apocryphal extension of Coordinated Universal Time, until some
// event (like celestial impact) deeply messes with Earth.
// It strive to be strictly C99-conformant.
//
// input:   Pointer to a  struct tm  with field tm_year, tm_mon, tm_mday,
//          tm_hour, tm_min, tm_sec set per  mktime  convention; thus
//          - tm_year is year minus 1900;
//          - tm_mon is [0..11] for January to December, but [-2..14] 
//            works for November of previous year to February of next year;
//          - tm_mday, tm_hour, tm_min, tm_sec similarly can be offset to
//            the full range [-32767 to 32767].
// output:  Number of non-leap seconds since beginning of the first UTC
//          day of year MY_EPOCH, as a signed at-least-64-bit integer.
//          The input is not changed (in particular, fields tm_wday,
//          tm_yday, and tm_isdst are unchanged and ignored).
my_time_t my_mktime(const struct tm * ptm) {
    int m, y = ptm->tm_year+2000;
    if ((m = ptm->tm_mon)<2) { m += 12; --y; }
// compute number of days within constant, assuming appropriate origin
#define MY_MKTIME(Y,M,D) ((my_time_t)Y*365+Y/4-Y/100*3/4+(M+2)*153/5+D)
    return ((( MY_MKTIME( y           ,  m, ptm->tm_mday)
              -MY_MKTIME((MY_EPOCH+99), 12, 1           )
             )*24+ptm->tm_hour)*60+ptm->tm_min)*60+ptm->tm_sec;
#undef MY_MKTIME // this macro is private
    }

thisthat 中的代码相比,主要观察结果可以大大简化答案:

  • 从 3 月开始计算月份,除此之前的月份之外的所有月份以 5 个月的周期重复,总共 153 天,交替 31 天和 30 天,因此,对于 任何 个月,并且不考虑闰年,可以使用适当的常数相加、乘以 153 和整数除以 5 来计算(在常数范围内)自上一个 2 月以来的天数;
  • 可以计算(在常数内) 通过添加一个适当的常数,整数除以 100,乘以 3,整数除以 4;
  • 我们可以使用我们在主计算中使用的相同公式计算任何时期的校正,并且可以使用宏来执行此操作,以便在编译时计算此校正。

这是另一个不需要 64 位支持的版本,锁定到 1970 年。

// Conversion from UTC date to second, unsigned 32-bit Unix epoch version.
// Written by François Grieu, 2015-07-21; public domain.

#include <time.h>                   // needed for struct tm
#include <limits.h>                 // needed for UINT_MAX
#if UINT_MAX>=0xFFFFFFFF            // unsigned is at least 32-bit
typedef unsigned      my_time_t;    // type for seconds since 1970
#else
typedef unsigned long my_time_t;    // type for seconds since 1970
#endif

// my_mktime  converts from  struct tm  UTC to non-leap seconds since 
// 00:00:00 on the first UTC day of year 1970 (fixed).
// It works from 1970 to 2105 inclusive. It strives to be compatible
// with C compilers supporting // comments and claiming C89 conformance.
//
// input:   Pointer to a  struct tm  with field tm_year, tm_mon, tm_mday,
//          tm_hour, tm_min, tm_sec set per  mktime  convention; thus
//          - tm_year is year minus 1900
//          - tm_mon is [0..11] for January to December, but [-2..14] 
//            works for November of previous year to February of next year
//          - tm_mday, tm_hour, tm_min, tm_sec similarly can be offset to
//            the full range [-32767 to 32768], as long as the combination
//            with tm_year gives a result within years [1970..2105], and
//            tm_year>0.
// output:  Number of non-leap seconds since beginning of the first UTC
//          day of year 1970, as an unsigned at-least-32-bit integer.
//          The input is not changed (in particular, fields tm_wday,
//          tm_yday, and tm_isdst are unchanged and ignored).
my_time_t my_mktime(const struct tm * ptm) {
    int m, y = ptm->tm_year;
    if ((m = ptm->tm_mon)<2) { m += 12; --y; }
    return ((( (my_time_t)(y-69)*365u+y/4-y/100*3/4+(m+2)*153/5-446+
        ptm->tm_mday)*24u+ptm->tm_hour)*60u+ptm->tm_min)*60u+ptm->tm_sec;
    }

【讨论】:

    【解决方案8】:

    一个很少编码和可移植的解决方案,因为它只使用 mktime:

    解析的时间必须在 struct tm tm 中。 如果您使用 c++11,您可能希望使用 std::get_time 进行解析。它解析大多数时间字符串!

    在调用 mktime() 之前确保 tm.tm_isdst 设置为零,然后 mktime 不会针对夏令时进行调整,

    // find the time_t of epoch, it is 0 on UTC, but timezone elsewhere
    // If you newer change timezone while program is running, you only need to do this once
    // if your compiler(VS2013) rejects line below, zero out tm yourself (use memset or "=0" on all members)
    struct std::tm epoch = {};
    epoch.tm_mday = 2; // to workaround new handling in VC, add a day
    epoch.tm_year = 70;
    time_t offset = mktime(&epoch) - 60*60*24; // and subtract it again
    
    // Now we are ready to convert tm to time_t in UTC.
    // as mktime adds timezone, subtracting offset(=timezone) gives us the right result
    result = mktime(&tm)-offset
    

    根据@Tom 的评论进行编辑

    【讨论】:

    • 这在 Windows 上运行良好,但在 macOS、Linux、BSD 或其他使用 IANA 时区数据库的平台上效果不佳。不同之处在于,Windows 时区存储的历史数据非常少,而 IANA 时区数据库有非常准确的历史数据可以追溯到 1970 年。当 1970 年的“标准偏移量”(实际偏移量 - 夏令时调整)不同于您的目标时间点的“标准偏移量”,这种技术默默地失败了。它将在许多时区运行良好。但至少有 89 个时区可能会失败,包括欧洲/伦敦。
    • 每次在 Win 上都不能正常工作 - 返回 -1,这应该是一个错误??尝试 - 当增加小时直到它高于 -1 时,当小时是时区偏移时,我会得到 0,当加倍时,我得到 3600* 原始小时,这听起来更好一些(? - 在我的区域中)
    • @HowardHinnant 我认为您的夏令时 cmets 是错误的。 tm_dst 设置为零以告诉 mktiime 忽略它。该解决方案仅使用 mktime 来获取时区的偏移量。自 1970 年以来,某些区域可能已经改变了 TZ 偏移量,但不是 89,当然也不是欧洲/伦敦——它们很少改变测量值:-)
    • 从 1968-10-26 23:00:00Z 到 1971-10-31 02:00:00Z,欧洲/伦敦的 UTC 偏移量为 1 小时,被认为是“标准”时间 (@987654323 @)。自 1971-10-31 02:00:00Z 起,该时区的标准 UTC 偏移量为 0h。使用最新的 IANA TZ 数据库 (2021a),我计算了 96 个时区,其中 1970-01-02 和 2021-01-02 的“标准偏移”是 不同的 。另一个是 Pacific/Kwajalein,它从 -12h 切换到 12h(两者都被认为是“标准”)。此计数不包括 IANA 链接(其他时区的别名)。我会在这里列出它们全部,但这不适合评论。
    【解决方案9】:

    正如其他答案所指出的,mktime()(令人发指)假设tm 结构位于本地时区(即使在tm 具有tm_gmtoff 字段的平台上),并且没有标准,跨平台方式将您的 tm 解释为 GMT。

    不过,以下是相当跨平台的——它适用于 macOS、Windows(至少在 MSVC 下)、Linux、iOS 和 Android。

    tm some_time{};
    ... // Fill my_time
    
    const time_t utc_timestamp =
        #if defined(_WIN32)
            _mkgmtime(&some_time)
        #else // Assume POSIX
            timegm(&some_time)
        #endif
    ;
    

    【讨论】:

      【解决方案10】:

      mktime 使用的 tm 结构有一个 timezone 字段。
      如果将“UTC”放入时区字段会发生什么?

      http://www.delorie.com/gnu/docs/glibc/libc_435.html

      【讨论】:

      • 是的,这对于 linux 来说看起来很容易,但微软并不同意 - 他们提供了一个 _mkgmtime 函数......
      • 根据mktime() manpagestruct tm 没有时区字段。根据您提到的 glibc 文档,“与 tm_gmtoff 一样,此字段是 BSD 和 GNU 扩展,在严格的 ISO C 环境中不可见。”
      • 如果将“UTC”放入时区字段会怎样?至少在 Linux 上,我的实验表明它被忽略了。
      【解决方案11】:

      我一直在想办法做到这一点。我不相信这个解决方案是完美的(它取决于运行时库计算夏令时的准确程度),但它对我的问题非常有效。

      最初我认为我可以计算gmtimelocaltime 之间的差异,并将其添加到我转换的时间戳中,但这不起作用,因为差异会根据代码的一年中的时间而改变已运行,如果您的源时间在一年的另一半,您将提前一个小时。

      因此,诀窍是让运行时库为您尝试转换的时间计算 UTC 与本地时间之间的差异。

      所以我正在做的是计算我的输入时间,然后通过将其插入 localtimegmtime 并添加这两个函数的差异来修改计算的时间:

      std::tm     tm;
      
      // Fill out tm with your input time.
      
      std::time_t basetime = std::mktime( &tm );
      std::time_t diff;
      
      tm = *std::localtime( &basetime );
      tm.tm_isdst = -1;
      diff = std::mktime( &tm );
      
      tm = *std::gmtime( &basetime );
      tm.tm_isdst = -1;
      diff -= std::mktime( &tm );
      
      std::time_t finaltime = basetime + diff;
      

      计算这个方法有点迂回,但如果不求助于帮助库或编写我自己的转换函数,我找不到任何其他方法。

      【讨论】:

        【解决方案12】:

        将 UTC 时间从字符串转换为时间戳的简单平台无关方法是使用您自己的 timegm

        使用mktime 和操作时区环境变量取决于正确安装和配置的 TZ 数据库。在一种情况下,某些时区链接配置不正确(可能是尝试不同时间服务器软件包的副作用),这会导致基于 mktime 的算法在该计算机上失败,具体取决于所选时区时间。

        尝试在不更改时区的情况下使用mktime 解决此问题是死路一条,因为在本地时钟回退一小时以关闭 DST 时,字符串时间(被视为本地时间)无法正确解决 -相同的字符串将匹配两个时间点。

        // Algorithm: http://howardhinnant.github.io/date_algorithms.html
        inline int days_from_civil(int y, int m, int d) noexcept
        {
            y -= m <= 2;
            int era = y / 400;
            int yoe = y - era * 400;                                   // [0, 399]
            int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;  // [0, 365]
            int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;           // [0, 146096]
        
            return era * 146097 + doe - 719468;
        }
        
        // Converts a broken-down time structure with UTC time to a simple time representation.
        // It does not modify broken-down time structure as BSD timegm() does.
        time_t timegm_const(std::tm const* t)
        {
            int year = t->tm_year + 1900;
            int month = t->tm_mon;          // 0-11
            if (month > 11)
            {
                year += month / 12;
                month %= 12;
            }
            else if (month < 0)
            {
                int years_diff = (11 - month) / 12;
                year -= years_diff;
                month += 12 * years_diff;
            }
            int days_since_epoch = days_from_civil(year, month + 1, t->tm_mday);
        
            return 60 * (60 * (24L * days_since_1970 + t->tm_hour) + t->tm_min) + t->tm_sec;
        }
        

        此解决方案不受外部依赖、线程安全、可移植且快速。如果您发现代码有任何问题,请告诉我。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-09-15
          • 1970-01-01
          • 2023-03-26
          • 1970-01-01
          • 1970-01-01
          • 2014-09-04
          相关资源
          最近更新 更多