I am implementing json serialization and deserialization of timestamps with nlohmann::json
. After transforming std::chrono::time_point
to mm/dd/yy hh:mm:ss
style date-time (by our requirements, it must be human-readable) naturally I am losing some information, so my criterion for the time points equality after deserialization is "different by no more than a second".
All was working fine on my local machine with gcc 11.4.0, but it fails on production server with gcc 8.5.0.
Minimal code to reproduce the issue live example on godbolt:
#include <iostream>
#include <iomanip>
#include <chrono>
#include <string>
int main() {
// serialization
auto ts = std::chrono::system_clock::now();
auto timeS_t = std::chrono::system_clock::to_time_t(ts);
std::ostringstream outStream;
outStream << std::put_time(std::localtime(&timeS_t), "%x %X");
std::string tsStr = outStream.str();
// deserialization
std::istringstream ss(tsStr);
std::tm t = {};
ss >> std::get_time(&t, "%x %X");
auto parsedTs = std::chrono::system_clock::from_time_t(std::mktime(&t));
// difference in seconds
int secDiff = std::chrono::duration_cast<std::chrono::seconds>(ts - parsedTs).count();
// just debug prints
std::cout << secDiff << std::endl;
std::time_t ttp1 = std::chrono::system_clock::to_time_t(ts);
std::time_t ttp2 = std::chrono::system_clock::to_time_t(parsedTs);
std::cout << "ts1: " << std::ctime(&ttp1);
std::cout << "ts2: " << std::ctime(&ttp2);
// my equality criterion
if (std::abs(secDiff) > 1) {
std::cout << "FAIL\n";
}
}
With newer compiler I get the equal results, e.g.
ts1: Thu Feb 22 16:06:25 2024
ts2: Thu Feb 22 16:06:25 2024
but with the old one I see the difference in time points by exactly 100 years:
ts1: Thu Feb 22 16:06:17 2024
ts2: Fri Feb 22 16:06:17 1924
I guess it might have something to do with std::localtime(&timeS_t)
in serialization routine, but I didn't find the "reverse" function for deserializer. What can I change to have a consistent behaviour?
You do not want to serialize a date/time to a locale-dependent format. Instead, serialize something that is independent of locale, like milliseconds since epoch:
// Serialize a time_point to a string
std::string serialize(const std::chrono::system_clock::time_point& time_point) {
auto duration_since_epoch = time_point.time_since_epoch();
auto milliseconds_since_epoch = std::chrono::duration_cast<std::chrono::milliseconds>(duration_since_epoch).count();
return std::to_string(milliseconds_since_epoch);
}
// Deserialize a string to a time_point
std::chrono::system_clock::time_point deserialize(const std::string& str) {
std::istringstream iss(str);
long long milliseconds_since_epoch;
iss >> milliseconds_since_epoch;
auto duration_since_epoch = std::chrono::milliseconds(milliseconds_since_epoch);
return std::chrono::system_clock::time_point(duration_since_epoch);
}
int main() {
// Get the current time
auto now = std::chrono::system_clock::now();
// Serialize the time to a string
std::string serialized_time = serialize(now);
std::cout << "Serialized time: " << serialized_time << std::endl;
// Deserialize the string back to a time_point
auto deserialized_time = deserialize(serialized_time);
std::cout << "Deserialized time: " << std::chrono::duration_cast<std::chrono::milliseconds>(deserialized_time.time_since_epoch()).count() << " milliseconds since epoch" << std::endl;
return 0;
}
Live Demo
Problem is how to interpret two digit year.
When human sees '80
he assumes year 1980. And when he sees '22
he assumes 2022.
Now this human bias was not reflected in older version library so std::get_time
just assumes that two digit year refers to 1900. New version of library tries to balance this and depeding on two digit value it assumes 1900 or 2000.
Here is demo of the problem including possible workaround for gcc 8.5: https://godbolt.org/z/MThhac943
struct Param
{
std::string fullTime;
std::string shortTime;
};
class MagicTimeStringTest : public testing::TestWithParam<Param>
{
public:
using clock = std::chrono::system_clock;
static clock::time_point parse(std::string in, std::string format)
{
std::istringstream stream(in);
std::tm t = {};
EXPECT_TRUE(stream >> std::get_time(&t, format.c_str()));
#ifdef WORAROUND
if (t.tm_year < 69) {
t.tm_year += 100;
}
#endif
return clock::from_time_t(std::mktime(&t));
}
static std::string toString(clock::time_point p)
{
std::ostringstream stream;
auto t = clock::to_time_t(p);
stream << std::put_time(std::localtime(&t), "%a %b %d %T %Y");
return stream.str();
}
};
TEST_P(MagicTimeStringTest, BarReturnsExpectedValue)
{
auto fromShort = parse(GetParam().shortTime, "%x %X");
auto fromFull = parse(GetParam().fullTime, "%b %d %T %Y");
EXPECT_EQ(fromShort, fromFull) << toString(fromShort) << '\n' << toString(fromFull);
auto diffHours = std::chrono::duration_cast<std::chrono::hours>(fromFull - fromShort).count();
EXPECT_EQ(diffHours, 0);
}
const Param data[] {
{"Feb 22 16:51:30 1994", "02/22/94 16:51:30"},
{"Feb 22 16:51:30 1974", "02/22/74 16:51:30"},
{"Feb 22 16:51:30 1970", "02/22/70 16:51:30"},
{"Feb 22 16:51:30 1969", "02/22/69 16:51:30"},
{"Feb 22 16:51:30 2068", "02/22/68 16:51:30"}, // note here change in test data it is 2068 not 1968
{"Feb 22 16:51:30 2024", "02/22/24 16:51:30"},
};
INSTANTIATE_TEST_SUITE_P(StackOverflow, MagicTimeStringTest, testing::ValuesIn(data));
It would be nice to find explanation of this behavior in documentation. I would not be surprised, if border between switching centuries moves as is moving with current time, so future 2100 is addressed to.
Edit:
With help of perplexity.ai I was able to find some documentation which explains this behavior (google and bing failed on this search):
date
If century is not specified, then values in the range [69,99] shall refer to years 1969 to 1999 inclusive, and values in the range [00,68] shall refer to years 2000 to 2068 inclusive. The current year is the default if yy is omitted.
Note:
It is expected that in a future version of IEEE Std 1003.1-2001 the default century inferred from a 2-digit year will change. (This would apply to all commands accepting a 2-digit year as input.)
So this behavior is standardized by IEEE Std 1003.1-2001
and wit is expected to be updated in a future. So far I was unable to find publicly available document with this standard.
Howard Hinnant has provide nice link to C++20 standard as comment:
[time.parse]
%y
The last two decimal digits of the year.
If the century is not otherwise specified (e.g., with %C), values in the range [69, 99] are presumed to refer to the years 1969 to 1999, and values in the range [00, 68] are presumed to refer to the years 2000 to 2068.
The modified command %Ny specifies the maximum number of characters to read.
If N is not specified, the default is 2.
Leading zeroes are permitted but not required.
The modified commands %Ey and %Oy interpret the locale's alternative representation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With