J. Jaksche
J. Jaksche

Reputation: 131

Using C++'s date library to read times

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

Answers (1)

Howard Hinnant
Howard Hinnant

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:

  1. A month_day
  2. A time of 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:

  1. Parse the fields, and then
  2. Deconstruct the time_point into fields to get the current year, and then
  3. Put the fields back together again into a time_point.

Upvotes: 6

Related Questions