2

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?

florian
  • 63
  • 2
  • 4
  • What is localtime_s and what platform do you use to compile that? I can't seem to make it work. Given https://en.cppreference.com/w/c/chrono/localtime it looked like I should be able to replace by `localtime_r` but no dice on any compiler I have access to – sehe Sep 15 '20 at 15:02
  • So, I figure that the arguments were swapped. – sehe Sep 15 '20 at 15:06
  • @sehe: The code has been compiled using MSVC 2019. It is defined in `time.h`. – florian Sep 16 '20 at 14:38

3 Answers3

1

There is that famous post by Plauger that he is not going to parse "14th day after Michaelmas" and the standards committee cannot make him do it. What you could to is put_time for a known date, e.g. 71/2/1, and try to dissect the result to recreate a detailed pattern you can later use for parsing.

1

You could prepare values of all possible formats, something like:

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

Then you could try to parse the first date with get_time using formats in turn until the parsing succeeds.

After that you can save the format index and use it for other date parsing.

But basically you are missing some information here. For example by just looking at:

2020/02/03

you can't tell if its the 2nd march or 3rd february, if you don't know with wich locale or format the date was produced.

To overcome this, you could try to use above formats for several dates choosen randomly from the set (or for all dates), having each parsing succeeding, so you can be more sure that the picked up format is the correct one.

But this is a brutte force solution.

For the simplest/most correct solution it would be necessary to have some constraints for the dates produces by put_time - you have to know something about it's formatting.

StPiere
  • 4,113
  • 15
  • 24
  • For code that does just that: https://stackoverflow.com/questions/44083239/using-boost-parse-datetime-string-with-single-digit-hour-format/44091087#44091087 - however it is based on strptime – sehe Sep 15 '20 at 15:04
0

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.

florian
  • 63
  • 2
  • 4