使用C++将时间戳转换为格式化日期时间

4
我只能使用C++标准库(C++14)来将时间戳转换为给定格式的日期时间。我刚接触C++,知道C++没有像Java那样的库可以支持我们很多。在中欧时区(CET)的2011-03-10 11:23:56时刻,将产生以下标准格式输出:"2011-03-10T11:23:56.123+0100"。
std::string format = "yyyy-MM-dd'T'HH:mm:ss'.'SSSZ"; //default format
auto duration = std::chrono::system_clock::now().time_since_epoch();
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();

我的格式字符串语法将会是

G : Era 
yy,yyyy : year (two digits/four digits) 
M,MM : month in number (without zero/ with zero) - (e.g.,1/01) 
MMM,MMMM : month in text (shortname/fullname)- (e.g.,jan/january) 
d,dd : day in month (without zero/with zero)- (e.g.,1/01) 
D : day in year
F : day of week of month
E, EEEE : day of week 
h,hh : hours(1-12) (without zero/with zero)- (e.g.,1/01) 
H,HH : hours(0-23) (without zero/with zero)- (e.g.,1/01) 
m,mm : minutes (without zero/with zero)- (e.g.,1/01) 
s,ss : seconds (without zero/with zero)- (e.g.,1/01) 
S,SS,SSS : milliseconds 
w,W : Week in year (without zero/with zero)- (e.g.,1/01) 
a : AM/PM 
z,zzzz : timezone name 

你尝试过什么?哪些方法不起作用?Stack Overflow不是一个作业写作服务。如果这不是作业,只需使用c++20日期库https://en.cppreference.com/w/cpp/chrono,也可在https://howardhinnant.github.io/date/date.html上找到。 - Alan Birtles
1
这里也许可以用到 std::put_time,您觉得呢? - Galik
3
“只使用C++标准库(C++14)”这一说法是什么让你觉得他可以使用一个甚至没有完全实现的C++标准呢? - eike
看起来你想以 ISO 8601 格式输出时间戳。如果是这样,这里应该有一些解决方案:https://dev59.com/rmkw5IYBdhLWcg3w9_Qi - Frodyne
没有理由不使用具有自由许可的库,尽量说服您的公司,如果您使用现有的库而不是自己实现所有内容,您将更加高效和代码更加可靠。 - Alan Birtles
显示剩余7条评论
2个回答

14
这是一个有些棘手的问题,因为:
  1. 未明确说明输入内容。但从示例代码中,我将假设为std::chrono::system_clock::time_point

  2. 重要的是要认识到,中欧时区(CET)被定义为一个具有固定UTC偏移量为1小时的时区。一些地理区域全年遵循这个时区规则,有些则不是。而且没有一直遵循它。在任何情况下,这部分问题允许我们硬编码涉及的UTC偏移量:1小时。没有夏令时调整。

在C ++14中,有两种方法可以在不涉及版权(甚至开源)第三方软件的情况下执行此操作:

  1. 使用C API。

  2. 自己写。

问题1的问题在于它容易出错。它不能直接处理毫秒精度。它不能直接处理特定的时区,如CET。 C API只知道UTC和计算机本地设置的时区。但这些问题是可以克服的。

问题2的问题是涉及非直观的算术来提取std::chrono::system_clock::time_point的年、月和日字段。

尽管2存在问题,但那是我偏好的解决方案,下面我将介绍它。我还会展示C++20如何使得这个过程变得更加容易。

在所有的解决方案中,我将通过实现这种形式的函数来规范输入和输出:

std::string format_CET(std::chrono::system_clock::time_point tp);

自己实现 (C++14)

这里有六个离散的步骤。需要包含以下头文件,不可添加其他头文件:

#include <chrono>
#include <string>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>

A. 将输入时间向前移动一个小时的UTC偏移量。

// shift time_point to CET
tp += 1h;

在函数内使用指令很方便,可以将UDL h引入作用域,以及在该函数中需要的所有其他来自<chrono>的内容:

using namespace std::chrono;

B. 获取两个版本的time_point tp:一个是毫秒精度,一个是天精度:

// Get time_points with both millisecond and day precision
auto tp_ms = time_point_cast<milliseconds>(tp);
auto tp_d = time_point_cast<days>(tp_ms);

重要的是要了解这两个转换{{向零取整}},并且对于负时间点会给出不正确的结果。 system_clock 在其1970-01-01 00:00:00 UTC之前提供负时间点。C++17引入了floor<millliseconds>(tp)来解决这个问题。
日精度time_point将用于提取年、月和日字段,毫秒精度time_point将用于提取小时、分钟、秒和毫秒字段。上面使用的duration days直到C++20才被添加,但您可以使用以下方法完成:
using days = std::chrono::duration<int, std::ratio<86400>>;

C. 要从tp_d中获取年、月和日字段,最方便的方法是使用公共领域日历操作算法之一。这不是第三方库,而是编写自己的日历库的算法(这就是我正在解释的内容)。我已经定制了civil_from_days算法,以完全满足format_CET的需求:

// Get {y, m, d} from tp_d
auto z = tp_d.time_since_epoch().count();
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");
z += 719468;
const int era = (z >= 0 ? z : z - 146096) / 146097;
const unsigned doe = static_cast<unsigned>(z - era * 146097);          // [0, 146096]
const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
int y = static_cast<int>(yoe) + era * 400;
const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
const unsigned mp = (5*doy + 2)/153;                                   // [0, 11]
const unsigned d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
const unsigned m = mp + (mp < 10 ? 3 : -9);                            // [1, 12]
y += (m <= 2);

对于那些想知道算法如何工作的人,可以在上面链接的网站上找到详细的推导过程。

此时,积分变量{y,m,d}包含年份、月份和日期三元组。

D. 获取自当地午夜以来的时间持续时间。这将用于提取当地的白天时间:

// Get milliseconds since the local midnight
auto ms = tp_ms - tp_d;

E. 获取小时、分钟、秒和毫秒字段:

// Get {h, M, s, ms} from milliseconds since midnight
auto h = duration_cast<hours>(ms);
ms -= h;
auto M = duration_cast<minutes>(ms);
ms -= M;
auto s = duration_cast<seconds>(ms);
ms -= s;

此时,chrono::duration 变量 {h, M, s, ms} 包含所需的值。

F. 现在我们准备格式化:

// Format {y, m, d, h, M, s, ms} as yyyy-MM-dd'T'HH:mm:ss'.'SSS+0100
std::ostringstream os;
os.fill('0');
os << std::setw(4) << y << '-' << std::setw(2) << m << '-' << std::setw(2)
   << d << 'T' << std::setw(2) << h.count() << ':'
   << std::setw(2) << M.count() << ':' << std::setw(2) << s.count()
   << '.' << std::setw(3) << ms.count() << "+0100";
return os.str();

使用操作符setw设置每个字段的宽度,并将填充字符设置为0,即可得到所需的前导零。

C++20解决方案

在C++20规范中,这变得更加容易

std::string
format_CET(std::chrono::system_clock::time_point tp)
{
    using namespace std::chrono;
    static auto const CET = locate_zone("Etc/GMT-1");
    return std::format("{:%FT%T%z}", zoned_time{CET, floor<milliseconds>(tp)});
}

"Etc/GMT-1"是IANA对中欧时间(CET)的等效表示。这个`time_zone const*`被定位并存储在变量`CET`中。`time_point tp`被截断到毫秒精度,并与`time_zone`配对,使用`zoned_time`。然后使用所示的格式字符串对该`zoned_time`进行格式化(精确到毫秒)。存在一个开源(MIT许可证)的C++20规范预览,语法差异非常小在此处
#include "date/tz.h"

std::string
format_CET(std::chrono::system_clock::time_point tp)
{
    using namespace date;
    using namespace std::chrono;
    static auto const CET = locate_zone("Etc/GMT-1");
    return format("%FT%T%z", zoned_time<milliseconds>{CET, floor<milliseconds>(tp)});
}

需要安装一些软件才能在Windows上使用。

此预览版与C++14兼容。在C++17及更高版本中,zoned_time<milliseconds>可以简化为只是zoned_time

自定义时区支持

还有一种方法可以使用预览库而无需安装。这将成为一个 header-only 库。通过创建一个仅模拟 CET 的自定义时区,然后将其安装在zoned_time中来实现。以下是自定义时区的示例:

#include "date/tz.h"

class CET
{
public:

    template <class Duration>
        auto
        to_local(date::sys_time<Duration> tp) const
        {
            using namespace date;
            using namespace std::chrono;
            return local_time<Duration>{(tp + 1h).time_since_epoch()};
        }

    template <class Duration>
        auto
        to_sys(date::local_time<Duration> tp) const
        {
            using namespace date;
            using namespace std::chrono;
            return sys_time<Duration>{(tp - 1h).time_since_epoch()};
        }

    template <class Duration>
        date::sys_info
        get_info(date::sys_time<Duration>) const
        {
            using namespace date;
            using namespace std::chrono;
            return {ceil<seconds>(sys_time<milliseconds>::min()),
                    floor<seconds>(sys_time<milliseconds>::max()),
                    1h, 0min, "CET"};
        }

    const CET* operator->() const {return this;}
};

CET现在满足足够的时区要求,可以在zoned_time中使用并像以前一样进行格式化。在C++14中,语法变得复杂,因为必须显式指定zoned_time模板参数:

std::string
format_CET(std::chrono::system_clock::time_point tp)
{
    using namespace date;
    using namespace std::chrono;
    using ZT = zoned_time<milliseconds, CET>;
    return format("%FT%T%z", ZT{CET{}, floor<milliseconds>(tp)});
}

这个选项也在C++20规范中,它的优点是时区缩写(在您的问题中未使用)将正确地报告“CET”,而不是“+01”。此处有关自定义时区的更多文档。
使用任何这些解决方案,函数现在可以像这样运行:
#include <iostream>

int
main()
{
    std::cout << format_CET(std::chrono::system_clock::now()) << '\n';
}

一个典型的输出如下所示:

2019-10-29T16:37:51.217+0100

非常清晰易懂,非常感谢您。接下来我需要格式化输出流。您有什么建议吗? - Nguyễn Đức Tâm
你认为在<ctime>头文件中定义的strftime()函数怎么样?因为我的输入格式语法与Java的SimpleDateformat相同,所以你认为这个函数能否实现我现在尝试的功能? - Nguyễn Đức Tâm
实际上,我想要做的就像Java中名为“SimpleDateFormat”的库。输入是时间戳字符串和格式字符串。输出将是格式化后的日期。我对C++也很新手。我已经编辑了我的帖子。 - Nguyễn Đức Tâm
是的,我知道 strftime 只支持到秒,但现在我可以做到毫秒。我接下来要做的是将 SimpleDateFormat 的语法格式化以适应 strftime - Nguyễn Đức Tâm
我已经编辑了我的帖子,你能帮我找到解决办法吗? - Nguyễn Đức Tâm
显示剩余2条评论

4
#include <ctime>
#include <iostream>
#include <iomanip>
int main()
{
    auto t = std::time(nullptr);
    auto tm = *std::localtime(&t);
    std::cout << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S.%z%Z") << "\n";
}
----
2019-10-29T05:05:14.-0700PDT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接