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
It can parse floating point, but you really don't want to. Your fix using milliseconds
is the recommended way to go.
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 rep
s, 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};
).