std::mktime and timezone info

Viewed 65034

I'm trying to convert a time info I reveive as a UTC string to a timestamp using std::mktime in C++. My problem is that in <ctime> / <time.h> there is no function to convert to UTC; mktime will only return the timestamp as local time.

So I need to figure out the timezone offset and take it into account, but I can't find a platform-independent way that doesn't involve porting the whole code to boost::date_time. Is there some easy solution which I have overlooked?

12 Answers

mktime assumes that the date value is in the local time zone. Thus you can change the timezone environment variable beforehand (setenv) and get the UTC timezone.

Windows tzset

Can also try looking at various home-made utc-mktimes, mktime-utcs, etc.

If you are trying to do this in a multithreaded program and don't want to deal with locking and unlocking mutexes (if you use the environment variable method you'd have to), there is a function called timegm that does this. It isn't portable, so here is the source: http://trac.rtmpd.com/browser/trunk/sources/common/src/platform/windows/timegm.cpp

int is_leap(unsigned y) {
    y += 1900;
    return (y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0);
}

time_t timegm (struct tm *tm)
{
    static const unsigned ndays[2][12] = {
        {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
        {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    };
    time_t res = 0;
    int i;

    for (i = 70; i < tm->tm_year; ++i)
        res += is_leap(i) ? 366 : 365;

    for (i = 0; i < tm->tm_mon; ++i)
        res += ndays[is_leap(tm->tm_year)][i];
    res += tm->tm_mday - 1;
    res *= 24;
    res += tm->tm_hour;
    res *= 60;
    res += tm->tm_min;
    res *= 60;
    res += tm->tm_sec;
    return res;
}

A solution with little coding and portable, as it only uses mktime:

The parsed time has to be in struct tm tm. if you use c++11, you might want to use std::get_time for parsing. It parses most time strings!

Before calling mktime() be sure tm.tm_isdst is set to zero, then mktime does not adjust for daylight savings,

// find the time_t of epoch, it is 0 on UTC, but timezone elsewhere
// If you newer change timezone while program is running, you only need to do this once
// if your compiler(VS2013) rejects line below, zero out tm yourself (use memset or "=0" on all members)
struct std::tm epoch = {};
epoch.tm_mday = 2; // to workaround new handling in VC, add a day
epoch.tm_year = 70;
time_t offset = mktime(&epoch) - 60*60*24; // and subtract it again

// Now we are ready to convert tm to time_t in UTC.
// as mktime adds timezone, subtracting offset(=timezone) gives us the right result
result = mktime(&tm)-offset

Edit based on comment from @Tom

As other answers note, mktime() (infuriatingly) assumes the tm struct is in the local timezone (even on platforms where tm has a tm_gmtoff field), and there is no standard, cross platform way to interpret your tm as GMT.

The following, though, is reasonably cross platform—it works on macOS, Windows (at least under MSVC), Linux, iOS, and Android.

tm some_time{};
... // Fill my_time

const time_t utc_timestamp =
    #if defined(_WIN32)
        _mkgmtime(&some_time)
    #else // Assume POSIX
        timegm(&some_time)
    #endif
;

I've just been trying to figure out how to do this. I'm not convinced this solution is perfect (it depends on how accurately the runtime library calculates Daylight Savings), but it's working pretty well for my problem.

Initially I thought I could just calculate the difference between gmtime and localtime, and add that on to my converted timestamp, but that doesn't work because the difference will change according to the time of year that the code is run, and if your source time is in the other half of the year you'll be out by an hour.

So, the trick is to get the runtime library to calculate the difference between UTC and local time for the time you're trying to convert.

So what I'm doing is calculating my input time and then modifying that calculated time by plugging it back into localtime and gmtime and adding the difference of those two functions:

std::tm     tm;

// Fill out tm with your input time.

std::time_t basetime = std::mktime( &tm );
std::time_t diff;

tm = *std::localtime( &basetime );
tm.tm_isdst = -1;
diff = std::mktime( &tm );

tm = *std::gmtime( &basetime );
tm.tm_isdst = -1;
diff -= std::mktime( &tm );

std::time_t finaltime = basetime + diff;

It's a bit of a roundabout way to calculate this, but I couldn't find any other way without resorting to helper libraries or writing my own conversion function.

The easy platform-independent way to convert UTC time from string to a timestamp is to use your own timegm.

Using mktime and manipulating timezone environment variables depends on correctly installed and configured TZ database. In one case some timezone links were incorrectly configured (likely side effect of trying different time server packages) which caused mktime-based algorithm to fail on that machine depending on the selected timezone and the time.

Trying to solve this problem with mktime without changing timezone is a dead end because string time (treated as local time) cannot be correctly resolved around the time when your local clock is set back one hour to turn off DST - the same string will match two points in time.

// Algorithm: http://howardhinnant.github.io/date_algorithms.html
inline int days_from_civil(int y, int m, int d) noexcept
{
    y -= m <= 2;
    int era = y / 400;
    int yoe = y - era * 400;                                   // [0, 399]
    int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;  // [0, 365]
    int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;           // [0, 146096]

    return era * 146097 + doe - 719468;
}

// Converts a broken-down time structure with UTC time to a simple time representation.
// It does not modify broken-down time structure as BSD timegm() does.
time_t timegm_const(std::tm const* t)
{
    int year = t->tm_year + 1900;
    int month = t->tm_mon;          // 0-11
    if (month > 11)
    {
        year += month / 12;
        month %= 12;
    }
    else if (month < 0)
    {
        int years_diff = (11 - month) / 12;
        year -= years_diff;
        month += 12 * years_diff;
    }
    int days_since_epoch = days_from_civil(year, month + 1, t->tm_mday);

    return 60 * (60 * (24L * days_since_1970 + t->tm_hour) + t->tm_min) + t->tm_sec;
}

This solution is free from external dependencies, threadsafe, portable and fast. Let me know if you can find any issues with the code.

Related