1

Given a textual date and time, for example:

Sat, 13 Jan 2018 07:54:39 -0500 (EST)

How can I convert it into ATL / MFC CTime?

The function should return (in case of my example):

CTime(2018,1,13,7,54,39) 

Either in GMT / UTF or plus the time zone

Update:

I tried writing the following function but it seems that ParseDateTime() always fail.

CTime DateTimeString2CTime(CString DateTimeStr)
{
    COleDateTime t;
    if (t.ParseDateTime(DateTimeStr))
    {
        CTime result(t);
        return result;
    }
    return (CTime)NULL;
}
Michael Haephrati
  • 3,660
  • 1
  • 33
  • 56
  • Are you sure you want to ignore the timezone information? This seems to be a very unusual thing to do, essentially trashing the information. – IInspectable Jan 14 '18 at 20:20
  • I don't want to ignore (or to trash) any information. – Michael Haephrati Jan 14 '18 at 21:39
  • So then, update your question to reflect that. As currently written you are asking for a `CTime`-based representation of a datetime string, ignoring the timezone information. – IInspectable Jan 14 '18 at 21:45
  • CTime doesn't ignore time zone (See https://msdn.microsoft.com/en-us/library/78zb0ese.aspx#ctime__formatgmt) but I updated my question to make it clear that nothing should be thrown away (also Day Light Saving). – Michael Haephrati Jan 14 '18 at 21:50
  • The particular c'tor you are invoking uses the *local* time, to make the appropriate adjustments to UTC. If you run that code in a timezone different from (EST), you get the wrong value, i.e. a chance of 23 (or more) to 1 to get the wrong result. Regardless, a `CTime` object cannot store timezone information. It stores time information that is implied to be UTC. – IInspectable Jan 14 '18 at 22:02
  • I received 2 very good answers. The first stage is to interpret the textual date into its indigents (including the time zone and day light saving). Then, we import this data to a CTime object. If timezone information cannot be stored in a CTime, how FormatGMT works? It uses the local time zone. I guess we need to use GetTimeZoneInformation()... – Michael Haephrati Jan 15 '18 at 13:09
  • 1
    You can only meaningfully construct a `CTime` object from a timestamp in local time. You have to decompose the source string into its constituents, convert it to local time by performing the necessary adjustments, and then feeding that into the `CTime` c'tor. Alternatively, calculate the offset from source to local time, construct a `CTime` from the source time, and add a respective [CTimeSpan](https://learn.microsoft.com/en-us/cpp/atl-mfc-shared/reference/ctimespan-class) to fix up the difference between source and local time. None of the answers extract the timezone information. – IInspectable Jan 15 '18 at 13:27
  • @zett42 's answers seems to be the most effective. I commented him to add the time zone and day light saving. I completely agree with your both suggestions as for implementing it. – Michael Haephrati Jan 15 '18 at 13:37

2 Answers2

3

You have to parse the string into the individual time components, convert these to integers and pass them to the appropriate CTime constructor.

There are many ways for parsing, one of the most straightforward and easy-to-maintain ways is to use regular expressions (once you get used to the syntax):

#include <iostream>
#include <regex>

void test( std::wstring const& s, std::wregex const& r );

int main()
{
    std::wregex const r{ 
        LR"(.*?)"            // any characters (none or more) 
        LR"((\d+))"          // match[1] = day
        LR"(\s*)"            // whitespace (none or more)
        LR"((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))"  // match[2] = month 
        LR"(\s*)"            // whitespace (none or more)
        LR"((\d+))"          // match[3] = year
        LR"(\s+)"            // whitespace (1 or more)
        LR"((\d+))"          // match[4] = hour
        LR"(\s*:\s*)"        // whitespace (none ore more), colon (1), whitespace (none ore more)
        LR"((\d+))"          // match[5] = minute
        LR"((?:\s*:\s*(\d+))?)" // match[6] = second (none or more) 
        LR"(.*)"             // any characters (none or more)
        , std::regex_constants::icase };

    test( L"Sat, 13 Jan 2018 07:54:39 -0500 (EST)", r );
    test( L"Wed, 10 jan2018 18:30 +0100", r );
    test( L"10Jan 2018 18 :30 : 00 + 0100", r );
}

void test( std::wstring const& s, std::wregex const& r )
{
    std::wsmatch m;
    if( regex_match( s, m, r ) )
    {
        std::wcout 
            << L"Day    : " << m[ 1 ] << L'\n'
            << L"Month  : " << m[ 2 ] << L'\n'
            << L"Year   : " << m[ 3 ] << L'\n'
            << L"Hour   : " << m[ 4 ] << L'\n'
            << L"Minute : " << m[ 5 ] << L'\n'
            << L"Second : " << m[ 6 ] << L'\n';
    }
    else
    {
        std::wcout << "no match" << '\n';    
    }
    std::wcout << std::endl;
}

Live demo.

You specify a pattern (the r variable) that encloses each component in parenthesis. After the call to regex_match, the result is stored in the variable m where you can access each component (aka sub match) through the subscript operator. These are std::wstrings aswell.

If necessary, catch exceptions that can be thrown by regex library aswell as std::stoi. I've omitted this code for brevity.

Edit:

After OP commented that a more robust parsing is required, I modified the regex accordingly. As can be seen in the calls to the test() function, the whitespace requirements are more relaxed now. Also the seconds part of the timestamp is now optional. This is implemented using a non-capturing group that is introduced with (?: and ends with ). By putting a ? after that group, the whole group (including whitespace, : and digits) can occur none or one time, but only the digits are captured.

Note: LR"()" designates a raw string literal to make the regex more readable (it avoids escaping the backslash). So the outer parenthesis are not part of the actual regex!


For manual parsing one could employ std::wstringstream. In my opinion, the only advantage over regular expressions is propably better performance. Otherwise this solution is just harder to maintain, for instance if the time format must be changed in the future.

#include <iostream>
#include <sstream>
#include <array>
#include <string>

int month_to_int( std::wstring const& m )
{
    std::array<wchar_t const*, 12> names{ L"Jan", L"Feb", L"Mar", L"Apr", L"May", L"Jun", L"Jul", L"Aug", L"Sep", L"Oct", L"Nov", L"Dec" };
    for( std::size_t i = 0; i < names.size(); ++i )
    {
        if( names[ i ] == m )
            return i + 1;
    }
    return 0;
}

int main()
{
    std::wstringstream s{ L"Sat, 13 Jan 2018 07:54:39 -0500 (EST)" };
    std::wstring temp;
    int day, month, year, hour, minute, second;
    // operator >> reads until whitespace delimiter
    s >> temp;
    s >> day;
    s >> temp; month = month_to_int( temp );
    s >> year;
    // use getline to explicitly specify the delimiter
    std::getline( s, temp, L':' ); hour = std::stoi( temp );
    std::getline( s, temp, L':' ); minute = std::stoi( temp );
    // last token separated by whitespace again
    s >> second;

    std::cout 
        << "Day    : " << day << '\n'
        << "Month  : " << month << '\n'
        << "Year   : " << year << '\n'
        << "Hour   : " << hour << '\n'
        << "Minute : " << minute << '\n'
        << "Second : " << second << '\n';
}

Live demo.

Again, no error handling here for brevity. You should check stream state after each input operation or call std::wstringstream::exceptions() after construction to enable exceptions and handle them.

zett42
  • 25,437
  • 3
  • 35
  • 72
  • I tried you live demo on "Wed, 10 Jan 2018 18:30 +0100" or "Wed, 10 Jan 2018 18:30 : 00 + 0100" and it failed. Any suggestions? – Michael Haephrati Jan 14 '18 at 12:31
  • @MichaelHaephrati I have tried to make the [regex more robust](https://wandbox.org/permlink/zZJ0qN0YCE5IcZXM). – zett42 Jan 14 '18 at 13:14
  • How do I take into consideration the time zone and day light saving? – Michael Haephrati Jan 14 '18 at 21:52
  • @MichaelHaephrati Well you could extract the timezone by adding more capturing group(s) to the regex, which I leave as an excercise. How to deal with that information then is propably better asked as a new question. – zett42 Jan 15 '18 at 20:34
  • Your answer is probably the best. Adding the timezone and day light saving would be great, if and when you have the time. – Michael Haephrati Jan 15 '18 at 20:42
3

As an alternative to manual parsing, you could use the COleDateTime Class, and it's member COleDateTime::ParseDateTime:

bool ParseDateTime(  
 LPCTSTR lpszDate,
 DWORD dwFlags = 0,
 LCID lcid = LANG_USER_DEFAULT) throw();

From the docs:

The lpszDate parameter can take a variety of formats.
For example, the following strings contain acceptable date/time formats:

"25 January 1996"
"8:30:00"
"20:30:00"
"January 25, 1996 8:30:00"
"8:30:00 Jan. 25, 1996"
"1/25/1996 8:30:00" // always specify the full year,
// even in a 'short date' format

From there you could convert to CTime if needed.

Danny_ds
  • 11,201
  • 1
  • 24
  • 46
  • I tried the following CTime DateTimeString2CTime(CString DateTimeStr) { COleDateTime t; if (t.ParseDateTime(DateTimeStr)) { CTime result(t); return result; } return (CTime)NULL; } but it always fails – Michael Haephrati Jan 14 '18 at 12:08
  • @MichaelHaephrati I did some testing (couldn't test this yesterday). It looks like the parser doesn't like the day (Sat) and the timezone. You could start parsing after the day (we don't need the name of the day) up to the time zone and then add/subtract the time difference from `t`. – Danny_ds Jan 14 '18 at 13:29
  • How about "Thu, 15 Mar 2018 21:15:11 GMT"? Just encounter that format, (for example when you fetch date/time from the WMI database) – Michael Haephrati Apr 19 '18 at 16:25