日期字符串到纪元秒 (UTC)

Posted

技术标签:

【中文标题】日期字符串到纪元秒 (UTC)【英文标题】:Date string to epoch seconds (UTC) 【发布时间】:2019-12-09 18:31:01 【问题描述】:

问题

我想将作为字符串 (UTC) 给出的日期时间解析为自 epoch 以来的秒数。示例(见EpochConverter):

2019-01-15 10:00:00 -> 1547546400

问题

直接的解决方案,在一个非常相关的问题C++ Converting a time string to seconds from the epoch 中也被接受,使用std::string -> std::tm -> std::time_t 使用std::get_time 然后std::mktime

std::tm tm;
std::stringstream ss("2019-01-15 10:00:00");
ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
std::time_t epoch = std::mktime(&tm);
// 1547546400 (expected)
// 1547539200 (actual, 2 hours too early)

std::mktime 似乎由于时区的原因而弄乱了时间。我正在执行来自UTC+01:00 的代码,但我们当时也有夏令时,所以这里是+2

tmstd::get_time 之后为hour 字段显示15。一进入std::mktime就会乱七八糟。

同样,字符串将被解释为 UTC 时间戳,不应涉及任何时区。但我想出的所有解决方案似乎都将其解释为本地时间戳并为其添加偏移量。


限制

对此我有一些限制:

C++17 独立于平台/编译器 没有环境变量破解 没有外部库(如 boost)

尽管为了问答而发布涉及这些问题的答案,但我不会接受。


研究

我找到了各种尝试来解决这个问题,但没有一个符合我的要求:

std::mktime(如上所述),因为它假定为当地时间,所以弄乱了时间 strptime,在我的平台上不可用,不属于标准 timegm(这正是我所需要的),与平台无关 _mkgmtime,与平台无关 boost::posix_time::from_iso_string,是一个外部库 std::chrono::date::parse,不适用于 C++17 用tzset清除并重置时区变量,使用环境变量hacking 使用 mktime(localtime(&timestamp)) - mktime(gmtime(&timestamp)) 手动抵消偏移量,计算出错误的偏移量,因为它不考虑 DST(在我的平台上为 1 小时,但需要 2 小时)

【问题讨论】:

From this std::mktime reference: "如果std::tm对象是从std::get_time...获取的,tm_isdst的值是不确定的,需要在调用mktime之前显式设置。”所以从那开始。 @Someprogrammerdude 但这需要我知道 DST 是否处于活动状态。对于我想随时随地在世界各地运行的程序来说,情况并非如此。对我来说这听起来没有必要,因为我根本不想涉及任何时区,日期字符串是 UTC,也应该用 UTC 解释,不应该涉及偏移量。这就是为什么我认为std::mktime 是一种不好的方法,因为它假定为当地时间。感谢您的意见:) 然后您需要将时区显式设置为“UTC”,这在 C++20 之前将依赖于平台。从 C++20 开始,有 calendar 和 time zone 函数。 【参考方案1】:

C++20 之前的解决方案:自己动手。

有了正确的文档,它确实比听起来容易得多,如果您不需要太多错误检测,甚至可以闪电般快速。

第一个问题是在不操纵任何数字的情况下解析数字。您只需要读取长度为 2 和 4 位的无符号值,因此只需做到这一点:

int
read2(std::string const& str, int pos)

    return (str[pos] - '0')*10 + (str[pos+1]  - '0');


int
read4(std::string const& str, int pos)

    return (str[pos] - '0')*1000 + (str[pos+1] - '0')*100 +
           (str[pos+2] - '0')*10 + (str[pos+3]  - '0');

现在给定一个字符串,很容易解析出你需要的不同值:

// yyyy-mm-dd hh:MM:ss -> count of non-leap seconds since 1970-01-01 00:00:00 UTC
// 0123456789012345678
long long
EpochConverter(std::string const& str)

    auto y = read4(str, 0);
    auto m = read2(str, 5);
    auto d = read2(str, 8);
    ...

通常使人们绊倒的部分是如何将三元组y, m, d 转换为自 1970 年 1 月 1 日以来/之前的天数。这是一个collection of public domain calendrical algorithms,可以帮助您做到这一点。这不是第 3 方日期/时间库。这是一个关于您将需要编写您自己的日期/时间库的算法的教程。而且这些算法高效。没有迭代。没有大桌子。这使得它们对管道和缓存非常友好。它们经过了 +/- 一百万年的单元测试。因此,您不必担心会遇到任何正确性界限。如果您对它们的工作原理感兴趣,这些算法也有非常深入的推导。

所以只需转到collection of public domain calendrical algorithms,选择您需要的算法(并根据需要自定义它们),然后推出您自己的转换器。

例如:

#include <cstdint>
#include <limits>
#include <string>

int
days_from_civil(int y, unsigned m, unsigned d) noexcept

    static_assert(std::numeric_limits<unsigned>::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits<int>::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    y -= m <= 2;
    const int era = (y >= 0 ? y : y-399) / 400;
    const unsigned yoe = static_cast<unsigned>(y - era * 400);      // [0, 399]
    const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d-1;  // [0, 365]
    const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;         // [0, 146096]
    return era * 146097 + static_cast<int>(doe) - 719468;


int
read2(std::string const& str, int pos)

    return (str[pos] - '0')*10 + (str[pos+1]  - '0');


int
read4(std::string const& str, int pos)

    return (str[pos] - '0')*1000 + (str[pos+1] - '0')*100 +
           (str[pos+2] - '0')*10 + (str[pos+3]  - '0');


// yyyy-mm-dd hh:MM:ss -> count of non-leap seconds since 1970-01-01 00:00:00 UTC
// 0123456789012345678
long long
EpochConverter(std::string const& str)

    auto y = read4(str, 0);
    auto m = read2(str, 5);
    auto d = read2(str, 8);
    auto h = read2(str, 11);
    auto M = read2(str, 14);
    auto s = read2(str, 17);
    return days_from_civil(y, m, d)*86400LL + h*3600 + M*60 + s;


#include <iostream>

int
main()

    std::cout << EpochConverter("2019-01-15 10:00:00") << '\n';

这只是为我输出:

1547546400

添加适合您的应用程序的任何错误检测。

【讨论】:

我看到的上述算法的问题是它们不能解释时间和时区的所有奇怪现象。有些国家/地区切换了时区,有些国家/地区具有非常特殊的偏移情况。尽管该算法在大多数标准情况下都可以正常工作,但对于全球范围内的奇怪边缘情况却失败了。不过我会接受它,因为我确实知道那里没有令人满意的解决方案(在我的要求范围内),而且这已经足够接近并且经过了很好的研究。谢谢。 您问题中的措辞使我相信您不希望涉及时区。我的解决方案仅限于 UTC。 “纪元”以 UTC(1970-01-01 00:00:00 UTC)测量。并且您声明输入字符串是 UTC。所以没有需要处理的时区。 哦,你是对的。由于我们直接解析字符串,没有任何内置解析器,因此我们不会得到 C++ 自动添加的任何奇怪的偏移量。在这种情况下,您的解决方案可以解决我的问题。非常感谢。【参考方案2】:

我最近也有同样的要求。我很失望地发现在编写时间戳和解析它们之间似乎对 DST 和时区的处理不一致。

我想出的代码是这样的:

void time_point_from_stream(std::istream &is, system_clock::time_point &tp)

    std::tm tm ;
    is >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");

    // unhappily, mktime thinks it's reading local time with DST adjustments
    auto my_time_t = std::mktime(&tm);
    my_time_t += tm.tm_gmtoff;

    if (tm.tm_isdst == 1)
        my_time_t -= 3600;

    tp = system_clock::from_time_t(my_time_t);

    if (not is)
        return;
    auto ch = is.peek();

    if (std::isspace(ch))
        return;

    if (ch == '.')
    
        double zz;
        is >> zz;
        auto zseconds = std::chrono::duration< double >(zz);
        tp += chrono::duration_cast< system_clock::duration >(zseconds);
        if (not is)
            return;
        ch = is.peek();
    

    if (ch == 'Z')
        is.get();
    else if (not isspace(ch))
    
        is.setstate(std::ios::failbit);
    

基本上,步骤是:

    使用std::get_time 填写tm 使用std::mktime 将其转换为time_t 撤销时区和 DST 调整 转换为std::chrono::system_clock::time_point 解析小数秒并调整结果。

我相信 c++20 会改善这种情况。

Howard Hinnant 还编写了改进的日期/时间库。还有boost::posix_time,我一直觉得它比std 更容易使用。

【讨论】:

正如我在对 OP 的评论中提到的,tm_isdst 的值在get_time 之后是不确定的,因此不能依赖它。 @Someprogrammerdude 这可以解释为什么我找不到关于它的文档,并且不得不通过经验分析来推断行为。 boost posix_time 是一个更好的库

以上是关于日期字符串到纪元秒 (UTC)的主要内容,如果未能解决你的问题,请参考以下文章

如何从 gmtime() 的时间 + 日期输出中获取纪元以来的秒数?

如何使用 Perl 将纪元时间转换为 UTC 时间?

是否有 perl 5.005 核心模块可以将纪元秒数转换为日期时间字符串?

将纪元时间转换为人类日期时间字符串,无需夏令时转换

在纪元中转换日期格式

C ++自定义时间日期结构到utc纪元