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

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

本文关键字:时区 转换 Unix 时间 C++      更新时间:2023-10-16

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

在这篇文章的基础上,讨论了从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的免费开源时区库,该库已移植到Linux,macOS和Windows。

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

例如,有一个名为date::zoned_seconds的类型将system_clock::time_point(精度seconds除外(与date::time_zone耦合,以在任意时区中创建本地时间。 如此处更详细地描述,以下是将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内完全提供。

上面的代码从stringtimezone创建一个zoned_seconds,并从time_tt创建一个派生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>系统中,则时区库中存在非常好的功能,用于解析和格式化zoned_seconds,例如:

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

示例输出:

2017-08-10 16:50:28 AKDT