how do I parse an iso 8601 date (with optional milliseconds) to a struct tm in C++?

markt1964 picture markt1964 · Nov 12, 2014 · Viewed 21.7k times · Source

I have a string which should specify a date and time in ISO 8601 format, which may or may not have milliseconds in it, and I am wanting to get a struct tm from it as well as any millisecond value that may have been specified (which can be assumed to be zero if not present in the string).

What would be involved in detecting whether the string is in the correct format, as well as converting a user-specified string into the struct tm and millisecond values?

If it weren't for the millisconds issue, I could probably just use the C function strptime(), but I do not know what the defined behavior of that function is supposed to be when the seconds contain a decimal point.

As one final caveat, if it is at all possible, I would greatly prefer a solution that does not have any dependency on functions that are only found in Boost (but I'm happy to accept C++11 as a prerequisite).

The input is going to look something like:

2014-11-12T19:12:14.505Z

or

2014-11-12T12:12:14.505-5:00

Z, in this case, indicates UTC, but any time zone might be used, and will be expressed as a + or - hours/minutes offset from GMT. The decimal portion of the seconds field is optional, but the fact that it may be there at all is why I cannot simply use strptime() or std::get_time(), which do not describe any particular defined behavior if such a character is found in the seconds portion of the string.

Answer

Howard Hinnant picture Howard Hinnant · Aug 9, 2016

New answer for old question. Rationale: updated tools.

Using this free, open source library, one can parse into a std::chrono::time_point<system_clock, milliseconds>, which has the advantage over a tm of being able to hold millisecond precision. And if you really need to, you can continue on to the C API via system_clock::to_time_t (losing the milliseconds along the way).

#include "date.h"
#include <iostream>
#include <sstream>

date::sys_time<std::chrono::milliseconds>
parse8601(std::istream&& is)
{
    std::string save;
    is >> save;
    std::istringstream in{save};
    date::sys_time<std::chrono::milliseconds> tp;
    in >> date::parse("%FT%TZ", tp);
    if (in.fail())
    {
        in.clear();
        in.exceptions(std::ios::failbit);
        in.str(save);
        in >> date::parse("%FT%T%Ez", tp);
    }
    return tp;
}

int
main()
{
    using namespace date;
    using namespace std;
    cout << parse8601(istringstream{"2014-11-12T19:12:14.505Z"}) << '\n';
    cout << parse8601(istringstream{"2014-11-12T12:12:14.505-5:00"}) << '\n';
}

This outputs:

2014-11-12 19:12:14.505
2014-11-12 17:12:14.505

Note that both outputs are UTC. The parse converted the local time to UTC using the -5:00 offset. If you actually want local time, there is also a way to parse into a type called date::local_time<milliseconds> which would then parse but ignore the offset. One can even parse the offset into a chrono::minutes if desired (using a parse overload taking minutes&).

The precision of the parse is controlled by the precision of the chrono::time_point you pass in, instead of by flags in the format string. And the offset can either be of the style +/-hhmm with %z, or +/-[h]h:mm with %Ez.