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:
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;
};
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.
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.
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.