c++std

std::get_time doesn't always give the same result


What I want to do

I am working on a small discord bot to handle friendly bets on ESport matches with my friends. Lately I have been trying to add the date and hours of the matches to:

  1. Display only the coming matches
  2. Don't allow the addition of new matches with past dates
  3. Display as a string the date and hours of the matches

What I did

So I looked on google how I could do that and I ended up with that very simple class:

class DateAndTime
{
public:
    explicit DateAndTime(const std::string& timeAsString)
    {
        std::istringstream timeAsStream{ timeAsString };
        timeAsStream >> std::get_time(&m_Time, std::string{ DATE_TIME_FORMAT }.c_str());
        if (timeAsStream.fail())
        {
            throw InvalidDateFormat(timeAsString);
        }
    }
    
    [[nodiscard]] std::string ToString() const noexcept
    {
        std::stringstream resultAsStream;
        resultAsStream << std::put_time(&m_Time, std::string{DATE_TIME_FORMAT}.c_str());

        return resultAsStream.str();
    }
    
    [[nodiscard]] bool IsInFuture() noexcept
    {
        const std::time_t dateInSeconds = std::mktime(&m_Time);

        const std::chrono::time_point now = std::chrono::system_clock::now();
        const std::time_t nowAsSeconds = std::chrono::system_clock::to_time_t(now);

        return dateInSeconds > nowAsSeconds;
    }

private:
    static constexpr std::string_view DATE_TIME_FORMAT = "%d-%m-%Y %H:%M";

    std::tm m_Time;
};

The problem

For some reason, with the same string as input, I don't have the same result depending on if I am executing my unit tests or the bot itself.

The unit tests:

TEST(DateAndTime_Tests, ToString)
{
    const DateAndTime test{ "10-01-1995 18:00" };
    EXPECT_EQ(test.ToString(), "10-01-1995 18:00");

    const DateAndTime test2{ "10-01-1995 18:00:00" };
    EXPECT_EQ(test2.ToString(), "10-01-1995 18:00");

    const DateAndTime test3{ "01-01-2028 18:00" };
    EXPECT_EQ(test3.ToString(), "01-01-2028 18:00");
}

All results are green so I works fine.

But if I execute my Discord Command with the date "01-01-2028 18:00" I have this error message (my error when giving a past date):

User error: The given Date [12-10-2000 16:42] is in the past.

As you can see, it is not the date I gave in input.

Debugging

At first I though that I had a problem in between the command and the DateTime's contructor like something altering the input string. But when I check add a breakpoint in the constructor, the input string is correct. So I must admit that I don't really understand what is happening.


Solution

  • The C timing API can be error prone to use.

    In this function you need to carefully initialize the std::tm before passing it into get_time:

    explicit DateAndTime(const std::string& timeAsString)
    {
        std::istringstream timeAsStream{ timeAsString };
        m_Time = {};           // Add this
        m_Time.tm_isdst = -1;  // Add this
        timeAsStream >> std::get_time(&m_Time, std::string{ DATE_TIME_FORMAT }.c_str());
        if (timeAsStream.fail())
        {
            throw InvalidDateFormat(timeAsString);
        }
    }
    

    You need to zero the whole thing, and then set the tm_isdst member to something negative to indicate that the daylight saving is unknown and the library should figure it out for itself.

    You could also opt to abandon the C API and use <chrono> exclusively (if you have C++20). That would look like this:

    class DateAndTime
    {
    public:
        explicit DateAndTime(const std::string& timeAsString)
        {
            std::istringstream timeAsStream{ timeAsString };
            timeAsStream >> std::chrono::parse(DATE_TIME_FORMAT, m_Time);
            if (timeAsStream.fail())
            {
                throw InvalidDateFormat(timeAsString);
            }
        }
        
        [[nodiscard]] std::string ToString() const noexcept
        {
            return std::vformat("{:" + DATE_TIME_FORMAT + '}', std::make_format_args(m_Time));
        }
        
        [[nodiscard]] bool IsInFuture() noexcept
        {
            const auto dateInSeconds = std::chrono::current_zone()->to_sys(m_Time);
    
            const auto now = std::chrono::system_clock::now();
            const auto nowAsSeconds = std::chrono::floor<std::chrono::seconds>(now);
    
            return dateInSeconds > nowAsSeconds;
        }
    
    private:
        static constexpr std::string DATE_TIME_FORMAT = "%d-%m-%Y %H:%M";
    
        std::chrono::local_seconds m_Time;
    };
    
    

    The <chrono> version is more explicit in the fact that m_Time is a local time in the computer's current local time zone. And if instead m_Time is UTC, or in some other time zone, it is trivial to change the code to reflect that.