Search code examples
c++c++20c++-chronovisual-c++-2019

Parse time format "DD/MM/YYYY at hh:mm:ss" & others using std::chrono::from_stream()


I'm currently trying to parse some info about the start time of an experiment as listed in a log file. After reading in the file important info, e.g column titles, start time, time between measurements, is parsed using <regex>.

I'm trying to use the std::chrono::from_stream(...) function to parse a string with the format "DD/MM/YYYY at hh:mm:ss" into a std::chrono::time_point, example of a string:

08/03/2021 at 09:37:25

At the moment I'm attempting this using the following function which attempts to construct a duration from a provided string to parse & a string to parse it with, then converting that to a time_point so I have control over the clock used:

#include <chrono>
#include <string>
#include <sstream>
#include <iostream>

using nano = std::chrono::duration<std::uint64_t, std::nano>;

template <typename Duration>
Duration TimeFormat(const std::string& str,
                    const std::string& fmt,
                    const Duration& default_val)
{
    Duration dur;
    std::stringstream ss{ str };
    std::chrono::from_stream(ss, fmt.c_str(), dur);

    /*
    from_stream sets the failbit of the input stream if it fails to parse
    any part of the input format string or if it receives any contradictory
    information.
    */
    if (ss.good())
    {
        std::cout << "Successful parse!" << std::endl;
        std::cout << dur.count() << std::endl;
        return dur;
    }
    else
    {
        std::cout << "Failed parse!" << std::endl;
        std::cout << dur.count() << std::endl;
        return default_val;
    }
}

int main()
{
    /*
    The file is already read in, and regex matches the correct line from the log file and a
    format pattern from a related config file.
    */
    
    /*
    Two different lines in the log file give:
    - str1 = test start time.
    - str2 = time between each measurement.
    */
    std::string str1("08/03/2021 at 09:37:25"), str2("00:00:05");
    std::string fmt1("%d/%m/%Y at %H:%M:%S"), fmt2("%H:%M:%S");

    auto test1 = TimeFormat<nano>(str1, fmt1, nano::zero());
    /*
    --> "Failed parse!" & test1.count() = 14757395258967641292
    A little research indicates that this is what VS initializes variables to
    in debug mode. If run in release mode test1.count() = 0 in my tests.
    */

    auto test2 = TimeFormat<nano>(str2, fmt2, nano::zero());
    /* 
    --> "Failed parse!" & test2.count() = 5000000000 (5 billion nanoseconds)
    Chose nanoseconds because it also has to handle windows file times which are measured
    relative to 01/01/1601 in hundreds of nanoseconds. Might be worth pointing out.
    What's weird is that it fails even though the value it reads is correct.
    */

    /*
    ... Convert to a time_point after this,
    e.g auto t1 = std::chrono::time_point<std::chrono::high_resolution_clock, nano>(test1);
    */
}

The MS documentation for from_stream can be found here. With details about different format characters just after the from_stream docs.


Solution

  • ss.is_good() ?

    Is that a type-o in your question or an extension in the Visual Studio std::lib?

    I'm going to guess it is a type-o and that you meant ss.good()...

    The good() member function checks if all state flags are off:

    • failbit
    • badbit
    • eofbit

    eofbit in particular often does not mean "error". It simply means that the parsing reached the end of the stream. You are interpreting "end of stream" as a parsing error.

    Instead check failbit or badbit. This is most easily done with the fail() member function.

    if (!ss.fail())
        ...
    

    Update

    Any idea why it still won't pass the first string though?

    I'm not 100% positive if it is a bug in the VS implementation, or a bug in the C++ spec, or a bug in neither. Either way, it wouldn't return what you're expecting.

    For me (using the C++20 chrono preview library), the first parse is successful and returns

    34645000000000
    

    which if printed out in hh:mm:ss.fffffffff format is:

    09:37:25.000000000
    

    That is, only the time-part is contributing to the return value. This is clearly not what you intended. Your first test appears to intend to parse a time_point, not a duration.

    Here is a slightly rewritten program that I think will do what you want, parsing a time_point in the first test, and a duration in the second:

    #include <chrono>
    #include <string>
    #include <sstream>
    #include <iostream>
    
    using nano = std::chrono::duration<std::uint64_t, std::nano>;
    
    template <typename TimeType>
    TimeType TimeFormat(const std::string& str,
                        const std::string& fmt,
                        const TimeType& default_val)
    {
        TimeType dur;
        std::stringstream ss{ str };
        std::chrono::from_stream(ss, fmt.c_str(), dur);
    
        /*
        from_stream sets the failbit of the input stream if it fails to parse
        any part of the input format string or if it receives any contradictory
        information.
        */
        if (!ss.fail())
        {
            std::cout << "Successful parse!" << std::endl;
            std::cout << dur << std::endl;
            return dur;
        }
        else
        {
            std::cout << "Failed parse!" << std::endl;
            std::cout << dur << std::endl;
            return default_val;
        }
    }
    
    int main()
    {
    
        std::string str1("08/03/2021 at 09:37:25"), str2("00:00:05");
        std::string fmt1("%d/%m/%Y at %H:%M:%S"), fmt2("%H:%M:%S");
    
        auto test1 = TimeFormat(str1, fmt1, std::chrono::sys_time<nano>{});
        auto test2 = TimeFormat(str2, fmt2, nano::zero());
    }
    

    For me this outputs:

    Successful parse!
    2021-03-08 09:37:25.000000000
    Successful parse!
    5000000000ns
    

    If one wanted the output of the first test in terms of nanoseconds since epoch, then one could extract that from the dur time_point variable with dur.time_since_epoch(). This would then output:

    1615196245000000000ns