std::mktime and timezone info
Asked Answered
A

13

46

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?

Athalla answered 9/2, 2009 at 23:22 Comment(1)
"mktime will only return the timestamp as local time" - To be clear, the timestamp is a UNIX timestamp (for whom timezones are utterly irrelevant). What happens is that mktime interprets its input as local time.Fillin
F
4

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.

Fireweed answered 9/2, 2009 at 23:32 Comment(2)
Thanks! I saw something like this in a google result, but does it work on windows?Athalla
No no no, never change environment variables in a process. Never. If you pass to a child process either inherit all of them or copy before fork. There is no threadsafe way to change environment variables.If
L
37
timestamp = mktime(&tm) - _timezone;

or platform independent way:

 timestamp = mktime(&tm) - timezone;

If you look in the source of mktime() on line 00117, the time is converted to local time:

seconds += _timezone;
Lilley answered 1/3, 2011 at 16:15 Comment(7)
That's just some implementation. Where will you get _timezone from in the calling code?Fillin
I simply used the "timezone" (not the "_timezone"). The "timezone" is defined in <time.h>.Lilley
@LightnessRacesinOrbit, _timezone is a global variable. It simply exists.Lilley
In some implementation. It's not something to rely on. Consider it as not simply existing. That's why its name starts with an underscore.Fillin
@LightnessRacesinOrbit, I agree, it's not a scientific solution. I had to find the fastest solution in terms of working on it, and processing time, and this worked on both Windows and Linux. I guess that if _timezone or timezone do not exist, the code would not compile.Lilley
This doesn't account for daylight savings time. If the timestamp was created during daylight savings time, you'd want to 3600 seconds to the result. The problem is, how do you determine if the timestamp you are converting was made during daylight savings time?Retrorse
@Brian You don't have to worry about DST if you set tm.tm_isdst to 0 before calling mktime. I'm about to edit the answer now…Blunk
R
13

I have this same problem yesterday and searching man mktime:

The functions mktime() and timegm() convert the broken-out time (in the structure pointed to by *timeptr) into a time value with the same encoding as that of the values returned by the time(3) function (that is, seconds from the Epoch, UTC). The mktime() function interprets the input structure according to the current timezone setting (see tzset(3)). The timegm() function interprets the input structure as representing Universal Coordinated Time (UTC).

In short:

You should use timegm(), instead of using mktime().

Rodd answered 23/4, 2013 at 9:47 Comment(1)
Works on Linux and BSD. Use _mkgmtime on windows.Erect
L
10

mktime() uses tzname for detecting timezone. tzset() initializes the tzname variable from the TZ enviroment variable. If the TZ variable appears in the enviroment but its value is empty or its value cannot be correctly interpreted, UTC is used.

A portable (not threadsafe) version according to the timegm manpage

   #include <time.h>
   #include <stdlib.h>

   time_t
   my_timegm(struct tm *tm)
   {
       time_t ret;
       char *tz;

       tz = getenv("TZ");
       setenv("TZ", "", 1);
       tzset();
       ret = mktime(tm);
       if (tz)
           setenv("TZ", tz, 1);
       else
           unsetenv("TZ");
       tzset();
       return ret;
   }

Eric S Raymond has a threadsafe version published in his article Time, Clock, and Calendar Programming In C

time_t my_timegm(register struct tm * t)
/* struct tm to seconds since Unix epoch */
{
    register long year;
    register time_t result;
#define MONTHSPERYEAR   12      /* months per calendar year */
    static const int cumdays[MONTHSPERYEAR] =
        { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };

    /*@ +matchanyintegral @*/
    year = 1900 + t->tm_year + t->tm_mon / MONTHSPERYEAR;
    result = (year - 1970) * 365 + cumdays[t->tm_mon % MONTHSPERYEAR];
    result += (year - 1968) / 4;
    result -= (year - 1900) / 100;
    result += (year - 1600) / 400;
    if ((year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0) &&
        (t->tm_mon % MONTHSPERYEAR) < 2)
        result--;
    result += t->tm_mday - 1;
    result *= 24;
    result += t->tm_hour;
    result *= 60;
    result += t->tm_min;
    result *= 60;
    result += t->tm_sec;
    if (t->tm_isdst == 1)
        result -= 3600;
    /*@ -matchanyintegral @*/
    return (result);
}
Lynden answered 4/7, 2012 at 7:25 Comment(12)
See also this answer to another question which says to use setenv("TZ", "UTC", 1); instead, so that it will work on Windows.Canonry
Note that this solution is OH GOD, SO NOT THREADSAFE. If you want to call this from two different threads, you must wrap it in a mutex lock.Purdum
It's not possible to make anything that depends on environment variables thread safe :-(If
While it may not be 100% possible to make anything dependent on environment variables totally threadsafe, if you are the in complete control of the system you can make a psuedo mutex by setting your own environment variable (e.g. TZ_lock) and waiting until it is null or "0".Overture
@Erroneous: That would just introduce more bugs, getenv() and setenv() are not thread-safe.Analyze
cumdays[t->tm_mon % MONTHSPERYEAR] is a problem (UB) should t->tm_mon < 0. Should not be too hard to fix.Coco
Use 1900L in year = 1900L + ..., else what was the point of making year a long? Perhaps year = 1900L + t->tm_year + t->tm_mon / MONTHSPERYEAR; int mon = t->tm_mon % MONTHSPERYEAR; if (mon < 0) { mon += MONTHSPERYEAR; year--; and then use mon instead of t->tm_mon in the rest of code.Coco
result = (year - 1970) * 365 also suffers from range issues as the product can exceed int range, but not time_t. Suggest result = (year - (time_t)1970) * 365 or the like if one cares about years like 6,000,000.Coco
I can't see that second piece of code on the linked page.Amesace
@Overture Nobody is ever in total control of the system, there are always libraries you use sooner or later.If
@Purdum "NOT THREADSAFE."? Which one, the former or the latter function? WHy not thread safe? Could you please shed some light on this matter?Secretarial
@John: My 8-year-old comment referred to the state of this answer 8 years ago. You can view that state in the history, here: stackoverflow.com/revisions/11324281/1Purdum
P
5

Use _mkgmtime, it takes care of everything.

Pastry answered 15/11, 2013 at 15:40 Comment(2)
And where might this be available and documented?Rip
This is only on windows. On linux and BSD, use timegm.Erect
F
4

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.

Fireweed answered 9/2, 2009 at 23:32 Comment(2)
Thanks! I saw something like this in a google result, but does it work on windows?Athalla
No no no, never change environment variables in a process. Never. If you pass to a child process either inherit all of them or copy before fork. There is no threadsafe way to change environment variables.If
R
4

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;
}
Retrorse answered 8/11, 2013 at 17:41 Comment(1)
This could trigger an out-of-bounds exception if tm_mon is set to 12 on a leap year, a common semantic for January of next year in mktime (and if it does not, result is likely very wrong). It makes many loops and function calls (57 on December 2015), and even more expensive modulo operations (nearly 100). It assumes time_t is in seconds, which is not carved in stone.Backstretch
B
4

Here is a simple, tested, hopefully portable piece of code converting from struct tm to seconds since the beginning of an adjustable UTC year, without temporary change of time zone.

// Conversion from UTC date to second, signed 64-bit adjustable epoch version.
// Written by François Grieu, 2015-07-21; public domain.

#include <time.h>                   // needed for struct tm
#include <stdint.h>                 // needed for int_least64_t
#define MY_EPOCH    1970            // epoch year, changeable
typedef int_least64_t my_time_t;    // type for seconds since MY_EPOCH

// my_mktime  converts from  struct tm  UTC to non-leap seconds since 
// 00:00:00 on the first UTC day of year MY_EPOCH (adjustable).
// It works since 1582 (start of Gregorian calendar), assuming an
// apocryphal extension of Coordinated Universal Time, until some
// event (like celestial impact) deeply messes with Earth.
// It strive to be strictly C99-conformant.
//
// input:   Pointer to a  struct tm  with field tm_year, tm_mon, tm_mday,
//          tm_hour, tm_min, tm_sec set per  mktime  convention; thus
//          - tm_year is year minus 1900;
//          - tm_mon is [0..11] for January to December, but [-2..14] 
//            works for November of previous year to February of next year;
//          - tm_mday, tm_hour, tm_min, tm_sec similarly can be offset to
//            the full range [-32767 to 32767].
// output:  Number of non-leap seconds since beginning of the first UTC
//          day of year MY_EPOCH, as a signed at-least-64-bit integer.
//          The input is not changed (in particular, fields tm_wday,
//          tm_yday, and tm_isdst are unchanged and ignored).
my_time_t my_mktime(const struct tm * ptm) {
    int m, y = ptm->tm_year+2000;
    if ((m = ptm->tm_mon)<2) { m += 12; --y; }
// compute number of days within constant, assuming appropriate origin
#define MY_MKTIME(Y,M,D) ((my_time_t)Y*365+Y/4-Y/100*3/4+(M+2)*153/5+D)
    return ((( MY_MKTIME( y           ,  m, ptm->tm_mday)
              -MY_MKTIME((MY_EPOCH+99), 12, 1           )
             )*24+ptm->tm_hour)*60+ptm->tm_min)*60+ptm->tm_sec;
#undef MY_MKTIME // this macro is private
    }

Key observations allowing great simplification compared to the code in this and that answers:

  • numbering months from March, all months except the one before that origin repeat with a cycle of 5 months totaling 153 days alternating 31 and 30 days, so that, for any month, and without consideration for leap years, the number of days since the previous February can be computed (within a constant) using addition of an appropriate constant, multiplication by 153 and integer division by 5;
  • the correction in days accounting for the rule for leap year on years multiple-of-100 (which by exception to the multiple-of-4 rules are non-leap except if multiple of 400) can be computed (within a constant) by addition of an appropriate constant, integer division by 100, multiplication by 3, and integer division by 4;
  • we can compute correction for any epoch using the same formula we use in the main computation, and can do this with a macro so that this correction is computed at compilation time.

Here is another version not requiring 64-bit support, locked to 1970 origin.

// Conversion from UTC date to second, unsigned 32-bit Unix epoch version.
// Written by François Grieu, 2015-07-21; public domain.

#include <time.h>                   // needed for struct tm
#include <limits.h>                 // needed for UINT_MAX
#if UINT_MAX>=0xFFFFFFFF            // unsigned is at least 32-bit
typedef unsigned      my_time_t;    // type for seconds since 1970
#else
typedef unsigned long my_time_t;    // type for seconds since 1970
#endif

// my_mktime  converts from  struct tm  UTC to non-leap seconds since 
// 00:00:00 on the first UTC day of year 1970 (fixed).
// It works from 1970 to 2105 inclusive. It strives to be compatible
// with C compilers supporting // comments and claiming C89 conformance.
//
// input:   Pointer to a  struct tm  with field tm_year, tm_mon, tm_mday,
//          tm_hour, tm_min, tm_sec set per  mktime  convention; thus
//          - tm_year is year minus 1900
//          - tm_mon is [0..11] for January to December, but [-2..14] 
//            works for November of previous year to February of next year
//          - tm_mday, tm_hour, tm_min, tm_sec similarly can be offset to
//            the full range [-32767 to 32768], as long as the combination
//            with tm_year gives a result within years [1970..2105], and
//            tm_year>0.
// output:  Number of non-leap seconds since beginning of the first UTC
//          day of year 1970, as an unsigned at-least-32-bit integer.
//          The input is not changed (in particular, fields tm_wday,
//          tm_yday, and tm_isdst are unchanged and ignored).
my_time_t my_mktime(const struct tm * ptm) {
    int m, y = ptm->tm_year;
    if ((m = ptm->tm_mon)<2) { m += 12; --y; }
    return ((( (my_time_t)(y-69)*365u+y/4-y/100*3/4+(m+2)*153/5-446+
        ptm->tm_mday)*24u+ptm->tm_hour)*60u+ptm->tm_min)*60u+ptm->tm_sec;
    }
Backstretch answered 21/7, 2015 at 7:45 Comment(0)
M
4

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

Madame answered 31/3, 2020 at 15:25 Comment(4)
This works pretty well on Windows, but not so much on macOS, Linux, BSD, or other platforms that use the IANA timezone database. The difference is that the Windows time zone stores very little if any historical data, and the IANA time zone database has very accurate historical data back to 1970. When the "standard offset" (actual offset - daylight saving adjustment) of 1970 is different than the "standard offset" of your target time point, this technique silently fails. It will run great in many time zones. But there's at least 89 time zones where it can fail, including Europe/London.Perkoff
Does not work properly each time on Win - returns -1, that should be an error ?? Trying - when increasing hour till it is higher than -1, I will get 0 when hour is timezone shift, when doubled then I got 3600* original hour that sounds a bit better(? - in my zone)Amaliaamalie
@HowardHinnant I think your comments on daylight saving is wrong. tm_dst is set to zero to tell mktiime to ignore this. The solution only use mktime to get the timezone's offset. Some zones may have changed their TZ offset since 1970, but not 89 and certainly not Europe/London - they seldom change their measurements:-)Madame
From 1968-10-26 23:00:00Z until 1971-10-31 02:00:00Z, Europe/London had a UTC offset of 1h and was considered "standard" time (tm_isdst == 0). Since 1971-10-31 02:00:00Z, this time zone has a standard UTC offset of 0h. Using the latest IANA TZ database (2021a), I'm counting 96 time zones for which the "standard offset" at 1970-01-02 and 2021-01-02 are different . Another one is Pacific/Kwajalein which switched from -12h to 12h (both considered "standard"). This count does not include IANA links, (aliases to other time zones). I would list them all here, but that doesn't fit in a comment.Perkoff
S
3

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
;
Surbeck answered 6/9, 2020 at 15:22 Comment(0)
E
2

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.

Excrescency answered 30/12, 2018 at 21:41 Comment(0)
D
1

The tm structure used by mktime has a timezone field.
What happens if you put 'UTC' into the timzone field?

http://www.delorie.com/gnu/docs/glibc/libc_435.html

Disproportionate answered 9/2, 2009 at 23:44 Comment(3)
Yes, that looks easy for linux, but Microsoft doesn't seam to agree - they provide a _mkgmtime function instead ...Athalla
According to the mktime() manpage, the struct tm doesn't have a timezone field. According to the glibc document you referred to, "Like tm_gmtoff, this field is a BSD and GNU extension, and is not visible in a strict ISO C environment."Canonry
What happens if you put 'UTC' into the timezone field? On Linux at least, my experiments indicate that it is ignored.Thirtieth
X
1

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.

Xavierxaviera answered 18/1, 2020 at 2:33 Comment(0)
B
1

Surprisingly, there's no standard function for such conversion, but implementation-specific functions exist. They are timegm in g++ and _mkgmtime in VC++. You can use one of them directly or define your own portable function which calls one of them:

#include <ctime>
#include <cstdio>

/**
    A wrapper function for compiler-specific functions for conversion from UTC in 'struct tm' to time_t.
    Inverse of gmtime function.
    Similar to mktime function but the input time is UTC.
    On error, the function may or may not set errno, depending on implementation.
    \param[in,out] tm Pointer to 'struct tm' having time in UTC.
        On input, only values of fields tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec matter.
        On success, fields tm_wday and tm_yday are calculated and set by the function.
        Other fields may also be changed.
    \returns On success, the number of seconds (not counting leap seconds) since 00:00, Jan 1 1970 UTC.
        On error, -1 cast to time_t.
*/
inline time_t mkgmtime(struct tm *tm) {
    #if defined(_DEFAULT_SOURCE) // Feature test for glibc
        return timegm(tm);
    #elif defined(_MSC_VER) // Test for Microsoft C/C++
        return _mkgmtime(tm);
    #else
        #error Function for conversion from UTC in 'struct tm' to time_t is not detected.
    #endif
}

int main() {
    struct tm utc;
    utc.tm_year = 2024 - 1900;
    utc.tm_mon = 9 - 1; // September
    utc.tm_mday = 12;
    utc.tm_hour = 16;
    utc.tm_min = 4;
    utc.tm_sec = 0;
    // The field tm_isdst can be ignored.

    time_t t = mkgmtime(&utc);

    // time_t type is implementation-specific, so casting it to long long. It should be OK.
    printf("%d-%02d-%02d %02d:%02d:%02d, week day: %d, year day: %d, time_t: %lld\n",
        utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec,
        utc.tm_wday, utc.tm_yday + 1, (long long)t);

    return 0;
}

Output:

2024-09-12 16:04:00, week day: 4, year day: 256, time_t: 1726157040

The function is inline for zero-overhead efficiency. So, if you wish to declare it in a header file, put the whole definition there.

Alternatively, you may define a macro:

// Define a unified name for a function converting from UTC in 'struct tm' to time_t.
#if defined(_DEFAULT_SOURCE) // Feature test for glibc
    #define MKGMTIME timegm
#elif defined(_MSC_VER) // Test for Microsoft C/C++
    #define MKGMTIME _mkgmtime
#else
    #error Function for conversion from UTC in 'struct tm' to time_t is not detected.
#endif

// ...

time_t t = MKGMTIME(&utc);

Tested in g++ 11.4.0, g++ 6.3.0, VS 2022 and VS 2013.

Blunk answered 18/3 at 16:37 Comment(1)
I didn't actually test whether functions set or don't set errno. Linux manual claims to do that, MS documentation for VS 2022 says nothing.Blunk

© 2022 - 2024 — McMap. All rights reserved.