Search code examples
c++datetimec++11c++-chrono

Does Howard Hinnant's date::parse() function work with floating-point durations?


I'm trying to parse 2020-03-25T08:27:12.828Z into std::chrono::time_point<system_clock, duration<double>> using Howard Hinnant's Date library.

It is expected that the following code outputs two identical strings:

#include "date.h"
#include <chrono>
#include <string>
#include <iostream>

using namespace std;
using namespace std::chrono;
using namespace date;

int main() {
  double d = 1585124832.828;

  time_point<system_clock, duration<double>> t{duration<double>{d}}, t1;

  string s {format("%FT%TZ", t) };
  cout << s << "\n";

  stringstream ss {s};
  ss >> parse("%FT%TZ", t1);
  cout << format("%FT%TZ", t1) << "\n";

}

But I get:

2020-03-25T08:27:12.828000Z
1970-01-01T00:00:00.000000Z

When I declare t and t1 as follows:

time_point<system_clock, milliseconds> t{duration_cast<milliseconds>(duration<double>{d})}, t1;

the code works as expected i.e. it outputs two identical lines


Solution

  • It can parse floating point, but you really don't want to. Your fix using milliseconds is the recommended way to go.

    Explanation:

    When you format the double-based seconds, it uses fixed formatting which defaults to 6 decimal places. This is why you see the 3 trailing zeroes after the .828.

    On parse, the expected precision is driven by the precision of the input type, even if the rep is floating point. So with duration<double> it only parses the integral part of the seconds. Then it starts looking for the trailing Z and finds . instead. This causes the parse to fail. If you didn't have the Z in the parse string, it wouldn't fail, but it also wouldn't parse the fractional part of the seconds.

    If you changed the time_point to be double-based microseconds, then it works again:

    time_point<system_clock, duration<double, micro>> t{duration<double>{d}}, t1;
    

    But I consider this way too cryptic and subtle, and it still has another problem you haven't hit yet: In C++17, round is supplied by the vendor as std::chrono::round, and this is used under the hood of parse. And the C++17 version of round does not permit the destination duration to have a floating point rep. So your code won't even compile in C++17 or later.

    Using integral-based milliseconds avoids all of this complication with parsing floating point values. And you can still convert the result back to duration<double> if you want to.

    One subtle suggestion though:

    time_point<system_clock, milliseconds> t{round<milliseconds>(duration<double>{d})}, t1;
    

    When converting from floating-point based to integral-based reps, I like to use round instead of duration_cast. This avoids off-by-one errors when the underlying double doesn't exactly represent the desired value and duration_cast truncates in the wrong direction (towards zero). round will round towards the nearest representable value.

    The above can be further simplified with the date::sys_time templated type alias:

    sys_time<milliseconds> t{round<milliseconds>(duration<double>{d})}, t1;
    

    The above is exactly equivalent. sys_time<duration> is a type alias for time_point<system_clock, duration>. And in C++20, it simplifies even further with new CTAD rules:

    sys_time t{round<milliseconds>(duration<double>{d})};
    

    The <milliseconds> is deduced from the type of the argument (C++20). Though you then have to declare t1 separately, or give t1 a milliseconds initial value (e.g. , t1{0ms};).