如何解析由 std::put_time("%x") 在 Windows 上创建的字符串?

How to parse a string created by std::put_time("%x") on Windows?

起点

让我们假设 windows 上的第三方组件使用以下函数创建一个表示日期的字符串:

std::string getDate() {
    std::tm t = {};
    std::time_t now = std::time(nullptr);
    localtime_s(&t, &now);
    std::stringstream s;
    s.imbue(std::locale(""));
    s << std::put_time(&t, "%x");
    return s.str();
}

根据您的系统语言环境和短日期格式的设置,您会得到类似 15.09.202009/15/202015. Sept. 2020 等的字符串

这是预期的,因为 %xcppreference 上被描述为 writes localized date representation (locale dependent)

问题

如何将 std::put_time("%x") 生成的字符串解析回 std::tm(假设相同的语言环境和短日期格式系统设置)?

什么不起作用

STL

std::tm parseDate1(const std::string& date) {
    std::tm t = {};
    std::istringstream ss(date);
    ss.exceptions(std::ifstream::failbit);
    ss.imbue(std::locale(""));
    ss >> std::get_time(&t, "%x");
    return t;
}

不起作用,因为 std::get_time 的实现需要硬编码格式 "%d / %m / %y for %x in xlocime.

提升

std::tm parseDate2(const std::string& date) {
    boost::gregorian::date d;
    auto* input_facet = new boost::gregorian::date_input_facet();
    input_facet->format("%x");
    std::istringstream ss(date);
    ss.exceptions(std::ifstream::failbit);
    ss.imbue(std::locale(std::locale(""), input_facet));    
    ss >> d;
    return boost::gregorian::to_tm(d);  
}

Boost always returns 1400-Jan-01 因为 %x 似乎根本没有实现解析。

strptime

windows 似乎不可用。有一个实现 here 但编译和集成似乎并不简单。

解决方法

到目前为止我想到的最好的解决方法是使用 Win32 函数 EnumDateFormats() 读取系统 DATE_SHORTDATE 格式并将此格式转换为 std::get_time() 语法,因为它不兼容(例如,对于 std::get_time()dd.MM.yyyy 需要转换为 %d.%m.%Y)。但这似乎很容易出错,而不是“正确”的方法...

似乎std::put_time()在内部使用strftimestd::get_time()是“自行实现的”。我原以为 std::put_time() 生成的所有内容都应该可以由 std::get_time() 使用相同的格式字符串进行解析。但这似乎并非如此,而且似乎也没有被记录在案。或者我错过了什么?

Plauger 有一个著名的 post,他不打算解析“米迦勒节后的第 14 天”,标准委员会不能让他这样做。你可以做的是 put_time 一个已知的日期,例如71/2/1,并尝试剖析结果以重新创建一个详细的模式,您稍后可以将其用于解析。

您可以准备所有可能格式的值,例如:

const char* formats[] = {
  "%Y.%M.%d",
  "%Y/%M/%d",
  "%Y %b %d",
  ...
};

然后你可以尝试依次使用格式解析get_time的第一个日期,直到解析成功。

之后您可以保存格式索引并将其用于其他日期解析。

但基本上您在这里遗漏了一些信息。例如,只需查看:

2020/02/03

如果您不知道生成日期的语言环境或格式,您无法判断是 3 月 2 日还是 2 月 3 日。

为了克服这个问题,您可以尝试对从集合中随机选择的几个日期(或所有日期)使用上述格式,让每个解析都成功,这样您就可以更加确定所选择的格式是正确的一.

但这是一个蛮力解决方案。

对于 simplest/most 正确的解决方案,有必要对 put_time 生成的日期进行一些限制 - 您必须了解其格式。

这是我迄今为止想到的最好的:

#include <iostream>
#include <sstream>
#include <locale>
#include <iomanip>
#include <windows.h>
#include <boost/algorithm/string.hpp> 

static std::string g_shortDateFormat;
BOOL CALLBACK EnumDateFormatsProc(_In_ LPSTR formatString) {
    if (g_shortDateFormat.empty())
        g_shortDateFormat = formatString;
    return TRUE;
}

std::string getShortDatePattern() {
    if (g_shortDateFormat.empty()) {
        EnumDateFormatsA(EnumDateFormatsProc, LOCALE_USER_DEFAULT, DATE_SHORTDATE);
        boost::algorithm::replace_all(g_shortDateFormat, "yyyy", "%Y");
        boost::algorithm::replace_all(g_shortDateFormat, "yy", "%y");
        boost::algorithm::replace_all(g_shortDateFormat, "MMMM", "%b");
        boost::algorithm::replace_all(g_shortDateFormat, "MMM", "%b");
        boost::algorithm::replace_all(g_shortDateFormat, "MM", "%m");
        boost::algorithm::replace_all(g_shortDateFormat, "M", "%m");
        boost::algorithm::replace_all(g_shortDateFormat, "dddd", "%a");
        boost::algorithm::replace_all(g_shortDateFormat, "ddd", "%a");
        boost::algorithm::replace_all(g_shortDateFormat, "dd", "d"); // intended to avoid %%d
        boost::algorithm::replace_all(g_shortDateFormat, "d", "%d");
    }
    return g_shortDateFormat;
}

std::string getLocalDate(const std::tm& t) {
    std::stringstream s;
    s.imbue(std::locale(""));
    s << std::put_time(&t, "%x");
    return s.str();
}

std::tm parseLocalDate(const std::string& localDate) {
    auto format = getShortDatePattern();
    std::istringstream is(localDate);
    is.imbue(std::locale(""));
    is.exceptions(std::istream::failbit);

    std::tm t = {};
    is >> std::get_time(&t, format.c_str());
    return t;
}

std::tm now() {
    auto now = std::time(nullptr);
    std::tm t = {};
    localtime_s(&t, &now);
    return t;
}

int main() {
    auto t = now();
    auto localDate = getLocalDate(t);
    auto parsedDate = parseLocalDate(localDate);
    std::cout << localDate << " - " << getLocalDate(parsedDate) << std::endl;
    return 0;
}

即使我在我的 windows 区域设置中输入相当奇怪的自定义短日期格式(例如 DD.MM.YYYY, DDDD,它会生成 ‎17.‎09.‎2020, ‎Thursday.

等日期,这仍然有效