Reputation: 131
I am attempting to use Howard Hinnant's date library (https://github.com/HowardHinnant/date) to read user input into a datetime object. I would like to use this library since it's modern, and is to be included in C++20.
The program should be able to accept datetimes in ISO 8601 format (YYYY-MM-DDTHH:MM:SS+HHMM
etc, eg 2020-07-07T18:30+0100
) as well as simple datetimes in the form DD-MM HH:MM
or HH:MM
. In the second case, it should assume that the missing information is to be filled in with the current date/year (the timezone is to be dealt with later).
Here is my attempt at doing this.
using namespace date;
int main(int argc, char ** argv)
{
std::istringstream ss { argv[1] };
std::chrono::system_clock::time_point dt
{ std::chrono::system_clock::now() };
if ( date::from_stream(ss, "%F T %T %z", dt) ) {
std::cout << "Cond 1 entered\n";
}
else if ( ss.clear(), ss.seekg(0); date::from_stream(ss, "%d-%m %R", dt)) {
std::cout << "Cond 2 entered\n";
}
std::cout << dt << "\n";
}
For the first format, this works as expected:
./a.out 2020-06-07T18:30:00+0200
Cond 1 entered
2020-06-07 16:30:00.000000000
However, the second method returns something strange, depending on the compiler used. When compiled with GCC and -std=c++17/-std=c++2a
:
./a.out "07-08 15:00"
Cond 2 entered
1754-04-06 13:43:41.128654848
EDIT 2: When compiled with LLVM and -std=c++2a
:
./a.out "07-08 15:00"
Cond 2 entered
0000-08-07 15:00:00.000000
which is a little closer to what I was expecting. I'd rather not have the behaviour dependent on the compiler used though!
I'm really stumped as to what's going on here, and I can't seem to make head or tail of the documentation.
How can I get date::from_stream
to simply overwrite the time and date and leave everything else?
EDIT 1:
For clarity, I was (incorrectly) expecting that when the second condition is entered the current year is preserved, since the time_point
object was initialised with the current year. E.g I hoped the second call to from_stream
would leave the time_point
object as 2020-08-07 15:00:33.803726000
in my second example.
See comments for further info.
EDIT 2:
Added results of trying with different compilers.
Upvotes: 5
Views: 1649
Reputation: 218750
Good question!!!
You're not doing it quite right, and you found a bug in date.h! :-)
First, I've fixed the bug you hit here. The problem was that I had the wrong value for not_a_year
in from_stream
, and that bug has been hiding in there for years! Thanks much for helping me find it! To update just pull the tip of the master branch.
When your program is run with the fixed date.h with the argument "07-08 15:00"
, it enters neither condition and prints out the current time.
Explanation:
The semantics of from_stream(stream, fmt, x)
is that if stream
doesn't contain enough information to fully specify x
using fmt
, then stream.failbit
gets set and x
is not modified. And "07-08 15:00"
doesn't fully specify a system_clock::time_point
.
The bug in date.h was that date.h was failing to recognize that there wasn't enough information to fully specify the system_clock::time_point
, and was writing deterministic garbage to it. And that garbage happened to produce two different values on LLVM/libc++ and gcc because of the different precisions of system_clock::time_point
(microseconds
vs nanoseconds
).
With the bug fix, the parse fails outright, and thus doesn't write the garbage.
I'm sure your next question will be:
How do I make the second parse work?
int main(int argc, char ** argv)
{
std::istringstream ss { argv[1] };
auto dt = std::chrono::system_clock::now();
ss >> date::parse("%FT%T%z", dt);
if (!ss.fail())
{
std::cout << "Cond 1 entered\n";
}
else
{
ss.clear();
ss.seekg(0);
date::month_day md;
std::chrono::minutes hm;
ss >> date::parse("%d-%m", md) >> date::parse(" %R", hm);
if (!ss.fail())
{
std::cout << "Cond 2 entered\n";
using namespace date;
auto y = year_month_day{floor<days>(dt)}.year();
dt = sys_days{y/md} + hm;
}
}
std::cout << dt << "\n";
}
The first parse is just as you had it, except that I switched the use of parse
for from_stream
which is a little higher level API. This doesn't really matter for the first parse, but makes the second parse neater.
For the second parse you need to parse two items:
month_day
And then combine those two elements with the current year
to produce the desired time_point
.
Now each parse fully specifies the variable it is parsing from the stream.
The mistake you made originally was in imagining that there is a "year field" under the hood of system_clock::time_point
. And actually this data structure is nothing but a count of microseconds or nanoseconds (or whatever) since 1970-01-01 00:00:00 UTC. So the second parse has to:
time_point
into fields to get the current year, and thentime_point
.Upvotes: 6