Qub1
Qub1

Reputation: 1244

localtime_s fails where gmtime_s succeeds with dates before 1-1-1970

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

Answers (1)

Howard Hinnant
Howard Hinnant

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

Related Questions