Search code examples
c++formatc++20c++-chrono

Convert time_point to string using std::format with format that includes date, time and subseconds


Let's assume we have a simple function that takes a std::chrono::time_point and returns a string using a given formatting string. Like so:

std::string DateTimeToString(std::chrono::sys_time, const char * szFormat /*= "%Y/%m/%d %H:%M:%S"*/)

According to the cppreference documentation of std::formatter we should be able to use the individual tags to create a string like 2021/03/24 15:36:32.123123. However, when the duration of the time_point uses microseconds (which is mandatory for our use) then this throws a format_error.

If we split this into to separate calls so that we use format for the date and time separately then it works. Like this:

auto tSysTime = std::chrono::system_clock::now();
std::string sTime = std::format("{:%H:%M:%S}", tSysTime.time_since_epoch());
std::string sDate = std::format("{:%Y/%m/%d}", std::chrono::sys_days(std::chrono::duration_cast<std::chrono::days>(tSysTime.time_since_epoch())));

Am I missunderstanding something or is there no way to have a single format string for my purpose? Keep in mind that the format string is an optional parameter so I cannot hardcode a split of individual parameters.

My expection would be to write

std::format("{:%Y/%m/%d %H:%M:%S}",tSysTime.time_since_epoch());

However, this crashes...

Thank you in advance for any help!

Edit: I'm using /std::c++latest flag in Visual Studio 2019


Solution

  • You need:

    std::format("{:%Y/%m/%d %H:%M:%S}",tSysTime);
    

    Or you can simplify it to:

    std::format("{:%Y/%m/%d %T}",tSysTime);
    

    If your system_clock::time_point::duration isn't microseconds, you can force it to microseconds precision with:

    std::format("{:%Y/%m/%d %T}",floor<microseconds>(tSysTime));
    

    The reason std::format("{:%Y/%m/%d %H:%M:%S}",tSysTime.time_since_epoch()); fails is that the .time_since_epoch() turns the time_point into a duration. And durations know nothing about dates. For example if you asked me what the current date and time is and I told you 1,616,600,192,123,123µs, you would look at me funny. A duration knows nothing about an epoch. A duration is just a measure of time between two time_points.

    A time_point on the other hand, knows about an epoch. It holds a duration and means: This much time duration beyond (or before) my epoch.

    std::format understands the distinction between time_point and duration. And so if the format string asks for dates (e.g. %Y/%m/%d) and you are formatting a duration, it throws an exception indicating insufficient information to do the requested job. But if you give it a time_point, then std::format knows how to get the date information out of that.

    This is all part of the type-safety design that <chrono> is built on, so that one can catch as many logic errors as possible, as early as possible.

    int64_t i = 1'616'600'192'123'123;
    microseconds d{i};
    sys_time<microseconds> t{d};
    cout << "i = " << i << '\n';
    cout << "d = " << d << '\n';
    cout << "t = " << t << '\n';
    

    i, d and t all hold the same value in memory: 1,616,600,192,123,123, but mean different things. And you can see these different meanings when you print them out:

    i = 1616600192123123
    d = 1616600192123123µs
    t = 2021-03-24 15:36:32.123123
    

    This is no different than:

    char x = 'A';
    int y{x};
    cout << "x = " << x << '\n';  // x = A
    cout << "y = " << y << '\n';  // y = 65