Reputation: 1244
I'm trying to get the current year stored in a date from before 1970 using an std::chrono::time_point<std::chrono::system_clock>
, however I've run into an issue regarding the reading from its contents into a std::tm
struct.
I convert the time_point
to a time_t
first, after which I read its values to get the tm_year
value. However, when trying to do so, the code fails when using localtime_s
, however it succeeds when I'm using gmtime_s
. This is only for dates before 1-1-1970, dates after that work fine using both functions.
The code below reproduces the error. If terstGmTimeVsLocalTime
is called with utc=true
it works, if it is called with utc=false
it doesn't produce the correct output.
#include <iomanip>
#include <time.h>
#include <iostream>
void testGmTimeVsLocaltime(const bool& utc) {
// Create time
std::tm timeInfoWrite = std::tm();
timeInfoWrite.tm_year = 1969 - 1900; // Year to parse, here it is 1969
timeInfoWrite.tm_mon = 0;
timeInfoWrite.tm_mday = 1;
timeInfoWrite.tm_hour = 1;
timeInfoWrite.tm_min = 0;
timeInfoWrite.tm_sec = 0;
timeInfoWrite.tm_isdst = -1;
std::chrono::time_point<std::chrono::system_clock> timePoint = std::chrono::system_clock::from_time_t(utc ? _mkgmtime(&timeInfoWrite) : std::mktime(&timeInfoWrite));
// Convert to time_t
std::time_t timeT = std::chrono::system_clock::to_time_t(timePoint);
// Read values
std::tm timeInfoRead;
if (utc) {
gmtime_s(&timeInfoRead, &timeT);
} else {
localtime_s(&timeInfoRead, &timeT);
}
// Output result
std::cout << (timeInfoRead.tm_year + 1900) << '\n';
// Wait for input
std::getchar();
}
int main() {
testGmTimeVsLocaltime(true); // Set to false to show bug
return 0;
}
utc=true
outputs 1969, as would be expected. However, utc=false
outputs 1899 (presumably since an error occurs and tm_year
gets set to -1).
Is there anything I'm missing? The documentation doesn't specifically specify that localtime_s
should fail for dates before 1-1-1970.
I'm on Windows 10 x64 if it makes a difference.
Upvotes: 1
Views: 1198
Reputation: 218750
Using Howard Hinnant's free, open-source date lib, you can completely side-step the clumsy, error and bug-prone C api, and work directly with a modern <chrono>
-based system:
#include "chrono_io.h"
#include "date.h"
#include <iostream>
void
testGmTimeVsLocaltime()
{
using namespace date;
// Create time
auto timeInfoWrite = 1969_y/jan/1;
sys_days timePoint = timeInfoWrite; // this is a chrono::time_point
std::cout << timePoint.time_since_epoch() << '\n'; // -365 days
// Convert to time_t
// no need
// Read values
year_month_day timeInfoRead = timePoint;
// Output result
std::cout << timeInfoRead.year() << '\n';
}
int
main()
{
testGmTimeVsLocaltime();
}
Output:
-365[86400]s
1969
There are literals to make it easy to fill in a year_month_day
struct which is the analog to the year, month and day parts of a tm
. You can easily convert this to a std::chrono::time_point<system_clock, days>
(sys_days
). This is the same as a system_clock::time_point
, but with a precision of days instead. It itself will implicitly convert to a seconds-precision time_point
(typedef'd to sys_seconds
), or to a system_clock::time_point
.
Above I just output its time_since_epoch()
which shows that it is -365 days prior to the epoch.
There's never really any need to convert to C API data structures, but it is easy if you want to. For example, assuming time_t
is seconds since 1970-01-01:
std::time_t timeT = sys_seconds{timePoint}.time_since_epoch().count();
std::cout << timeT << '\n';
which outputs:
-31536000
The reverse conversion (back to year_month_day
) is just as easy. If you want to convert from timeT
it is just slightly more involved:
year_month_day timeInfoRead = floor<days>(sys_seconds{seconds{timeT}});
This first converts time_t
to chrono::seconds
, and then to a seconds
-precsion time_point
, and then to a days
-precsion time_point
, finally to the year_month_day
field type (tm
-like).
Finally year_month_day
has a year()
getter member function which is streamable. You can explicitly convert year
to int
if desired:
int{timeInfoRead.year()}
But I think it best to keep things like years, months and days as distinct types so that the compiler can help you catch when you accidentally mix them up.
Finally, if you really meant that you wanted 1969-01-01 00:00:00 in your computer's local timezone, there's a library to do that as well. And it is just a minor modification of the simple program above.
#include "tz.h"
#include <iostream>
void
testGmTimeVsLocaltime()
{
using namespace date;
using namespace std::chrono;
// Create time
auto timeInfoWrite = 1969_y/jan/1;
auto timePoint = make_zoned(current_zone(), local_days{timeInfoWrite});
// Convert to time_t
std::time_t timeT = timePoint.get_sys_time().time_since_epoch().count();
std::cout << timeT << '\n';
// Read values
timePoint = sys_seconds{seconds{timeT}};
year_month_day timeInfoRead{floor<days>(timePoint.get_local_time())};
// Output result
std::cout << timeInfoRead.year() << '\n';
}
int
main()
{
testGmTimeVsLocaltime();
}
Output:
-31518000
1969
Now you create a zoned_seconds
using the computer's current_zone()
timezone, and converting your timeInfoWrite
to local_days
instead of to sys_days
.
You can get the local time or the system time out of timePoint
. For converting to time_t
, system time makes the most sense:
std::time_t timeT = timePoint.get_sys_time().time_since_epoch().count();
And now the output (for me) is 5h later (18000s).
-31518000
You can get back either the local year, or the system (UTC) year, by either using .get_local_time()
or .get_sys_time()
. For me it makes no difference ("America/New_York"
). But if you're in "Australia/Sydney"
, you'll get 1968 if you request the UTC year instead of 1969. And that's all very easy to simulate by simply substituting "Australia/Sydney"
or "America/New_York"
for current_zone()
in the program above.
Yes, it works on Windows, VS-2013 and later. There is some installation required for the timezone lib: https://howardhinnant.github.io/date/tz.html#Installation
Upvotes: 2