C++:将 Unix 时间转换为非本地时区

C++: converting Unix time to non-local timezone

我正在尝试将 time_t 变量中保存的时间转换为某个非本地时区的 struct tm* 时间。

建立在 this post 的基础上,它讨论了从 struct tm*time_t 的逆运算,我编写了以下函数:

struct tm* localtime_tz(time_t* t, std::string timezone) {

  struct tm* ret;
  char* tz;

  tz = std::getenv("TZ"); // Store currently set time zone                                                                                                                           

  // Set time zone                                                                                                                                                                   
  setenv("TZ", timezone.c_str(), 1);
  tzset();

  std::cout << "Time zone set to " << std::getenv("TZ") << std::endl;

  ret = std::localtime(t); // Convert given Unix time to local time in time zone                                                                                                     
  std::cout << "Local time is: " << std::asctime(ret);
  std::cout << "UTC time is: " << std::asctime(std::gmtime(t));

  // Reset time zone to stored value                                                                                                                                                 
  if (tz)
    setenv("TZ", tz, 1);
  else
    unsetenv("TZ");
  tzset();

  return ret;

}

然而,转换失败,我得到

Time zone set to CEST
Local time is: Wed Aug  9 16:39:38 2017
UTC time is: Wed Aug  9 16:39:38 2017

即本地时间设置为 UTC 时间,而不是 CEST 的 UTC+2。

date等工具显示的时区缩写,并非时区名称。 IANA 时区数据库使用相关区域内的主要城市。原因是缩写不是唯一的,它们没有传达足够的信息来转换过去的日期,因为地方会切换时区。

此外,POSIX 指定必须以特定方式解析 TZ 变量,它几乎建议像 UTC 一样处理纯时区缩写(但使用不同的缩写)。

相反,您必须使用实际的时区名称,例如 Europe/Berlin,或 POSIX 时区说明符,例如 CET-1CEST.

如果有人想以线程安全的方式执行此操作(无需设置全局变量,例如环境变量),我将提供一个额外的答案。这个答案需要 C++11(或更好)和 Howard Hinnant's free, open-source timezone library,它已移植到 linux、macOS 和 Windows.

这个库建立在 <chrono> 的基础上,将其扩展到日历和时区。可以使用此库与 C 时序 API(例如 struct tm)进行交互,但库中并未内置此类功能。该库使 C API 变得不必要,除非您必须与使用 C API.

的其他代码交互

例如,有一个名为 date::zoned_seconds 的类型,它将 system_clock::time_point(精度为 seconds 的除外)与 date::time_zone 相结合,以在任意时间创建本地时间区。作为 described in more detail here,以下是如何将 zoned_seconds 转换为 std::tm

std::tm
to_tm(date::zoned_seconds tp)
{
    using namespace date;
    using namespace std;
    using namespace std::chrono;
    auto lt = tp.get_local_time();
    auto ld = floor<days>(lt);
    time_of_day<seconds> tod{lt - ld};  // <seconds> can be omitted in C++17
    year_month_day ymd{ld};
    tm t{};
    t.tm_sec  = tod.seconds().count();
    t.tm_min  = tod.minutes().count();
    t.tm_hour = tod.hours().count();
    t.tm_mday = unsigned{ymd.day()};
    t.tm_mon  = unsigned{ymd.month()} - 1;
    t.tm_year = int{ymd.year()} - 1900;
    t.tm_wday = unsigned{weekday{ld}};
    t.tm_yday = (ld - local_days{ymd.year()/jan/1}).count();
    t.tm_isdst = tp.get_info().save != minutes{0};
    return t;
}

使用这个构建块,在任意时区(无需设置全局)将 time_t 转换为 tm 变得几乎微不足道:

std::tm
localtime_tz(time_t t, const std::string& timezone)
{
    using namespace date;
    using namespace std::chrono;
    return to_tm({timezone, floor<seconds>(system_clock::from_time_t(t))});
}

如果您使用的是 C++17,您可以删除 using namespace date,因为 floor<seconds> 将在 namespace std::chrono.

中完全提供

上面的代码从 string timezone 创建一个 zoned_seconds 和从 time_t t 派生的 system_clock::time_pointzoned_seconds 在概念上是 pair<time_zone, system_clock::time_point> 但具有任意精度。它也可以被认为是 pair<time_zone, local_time> 因为 time_zone 知道如何在 UTC 和本地时间之间映射。所以上面的大部分用户编写的代码只是从 zoned_seconds 中提取本地时间并将其放入 struct tm.

上面的代码可以这样练习:

auto tm = localtime_tz(time(nullptr), "America/Anchorage");

如果您不需要 tm 中的结果,而是可以留在 <chrono> 系统中,则 timezone library 中有非常好的功能用于解析和格式化zoned_seconds,例如:

cout << zoned_seconds{"America/Anchorage", floor<seconds>(system_clock::now())} << '\n';

示例输出:

2017-08-10 16:50:28 AKDT