Search code examples
c++windowsdatebooststl

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


Starting Point

Let's assume a third party component on windows creates a string representing a date using the following function:

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();
}

Depending on your system locale and settings for the short date format you get strings like 15.09.2020 or 09/15/2020 or 15. Sept. 2020 etc.

This is expected as %x is described as writes localized date representation (locale dependent) on cppreference.

Question

How to parse a string generated by std::put_time("%x") back to a std::tm (assuming identical locale and short date format system settings)?

What doesn't work

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;
}

Doesn't work because the implementation of std::get_time expects hard-coded format "%d / %m / %y for %x in xlocime.

Boost

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 because %x doesn't seem to be implemented for parsing at all.

strptime

Doesn't seem to be available on windows. There exists an implementation here but it doesn't seem to be straightforward to compile and integrate.

Workaround

The best workaround I have came up with so far is using Win32 function EnumDateFormats() to read the system DATE_SHORTDATE format and convert this format to std::get_time() syntax because it is not compatible (e.g. dd.MM.yyyy needs to be converted to %d.%m.%Y for std::get_time()). But this seems to be error prone and not the "right" way to do it...

It also seems that std::put_time() uses strftime internally and std::get_time() is "self-implemented". I would have expected that everything produced by std::put_time() should be parsable by std::get_time() using the same format string. But this doesn't seem to be the case and it also doesn't seem to be documented somehwere. Or am I missing something?


Solution

  • Here is the best I came up with so far:

    #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;
    }
    

    This works even if I enter rather strange custom short dateformats like DD.MM.YYYY, DDDD in my windows regional settings which generates dates like ‎17.‎09.‎2020, ‎Thursday.