Easy way to convert a struct tm (expressed in UTC) to time_t type
Asked Answered
P

13

34

How do I do the above? There is mktime function but that treats the input as expressed in local time but how do i perform the conversion if my input tm variable happens to be in UTC.

Polyvalent answered 12/11, 2008 at 6:25 Comment(0)
N
35

Use timegm() instead of mktime()

Worth noting as pointed out by @chux - Reinstate Monica below is that time_t timegm(struct tm *timeptr) is considered adding to the C23 standard (and thus by inclusion into the C++ standard).

Naresh answered 12/11, 2008 at 6:30 Comment(10)
Good answer - only demerit is that it is a non-standard (as in, not in POSIX or C standard) function.Wiskind
I've seen other answers on the net that talk about getting the timezone offset using something like (note this is pseudo code - the parameters aren't right): difftime( mktime( gmtime( time())), mktime( localtime( time()))). But no one ever says how you apply this offset to your time_t variable.Cramped
Look at my answer for a more portable version.Mongolic
@Dana: What is your source for the deprication (I can find no reference to it in the man pages of any of my systems)? I will give you that it is a non standard GNU function. But it is commonly found on BSD Linux systems. But the fact that the OP accepted the answer means it was useful to them.Naresh
Not meaning to say it is not useful. But it is a feature test macro, not a standard function. See linux.die.net/man/3/timegm - says it is to be avoided. If you include that in your answer I can remove my downvote.Exterminate
Down vote because you don't explain why one should use timegm() over mktime(); without the explanation I'm included to favour Dana's argument.Killoran
I just wrote an article on this subject: blog.reverberate.org/2020/05/12/optimizing-date-algorithms.htmlApplause
Martin York, as C23 is adding time_t timegm(struct tm *timeptr); to the standard library, consider adding that info to your answer.Ogrady
@chux-ReinstateMonica I don't see it in N4950Naresh
@MartinYork post is tagged C and C++. My comment was about C23. N4950 is about C++. Such is the woes of dual language tagging.Ogrady
T
11

for those on windows, the below function is available:

_mkgmtime

link for more info: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/mkgmtime-mkgmtime32-mkgmtime64

Threshold answered 1/4, 2015 at 10:25 Comment(1)
"for those on windows," --> is more like "for those using visual studio. It is a function available via the compiler, not the OS. Other compilers on widows may not have _mkgmtime().Ogrady
C
9

Here is a solution I use (Can't recall where I found it) when it isn't a windows platform

time_t _mkgmtime(const struct tm *tm) 
{
    // Month-to-day offset for non-leap-years.
    static const int month_day[12] =
    {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};

    // Most of the calculation is easy; leap years are the main difficulty.
    int month = tm->tm_mon % 12;
    int year = tm->tm_year + tm->tm_mon / 12;
    if (month < 0) {   // Negative values % 12 are still negative.
        month += 12;
        --year;
    }

    // This is the number of Februaries since 1900.
    const int year_for_leap = (month > 1) ? year + 1 : year;

    time_t rt = tm->tm_sec                             // Seconds
        + 60 * (tm->tm_min                          // Minute = 60 seconds
        + 60 * (tm->tm_hour                         // Hour = 60 minutes
        + 24 * (month_day[month] + tm->tm_mday - 1  // Day = 24 hours
        + 365 * (year - 70)                         // Year = 365 days
        + (year_for_leap - 69) / 4                  // Every 4 years is     leap...
        - (year_for_leap - 1) / 100                 // Except centuries...
        + (year_for_leap + 299) / 400)));           // Except 400s.
    return rt < 0 ? -1 : rt;
}
Chatwin answered 6/11, 2015 at 18:2 Comment(3)
This solution can have integer overflows, thus security issues. You may need to bound the year and use larger types (see Mutt bug 3880 as an example).Coaptation
I used this method and for some reason 31-01-2018 23:59:58 had more seconds than 01-02-2018 00:00:01. I ended up writing my variant without taking leap years into account.Foreskin
This can be made even simpler if you just use the value of tm->tm_yday instead of messing around with the tm_mday and the number of ydays prior to tm_mon.Jim
M
8

The answer of Loki Astari was a good start, timegm is one of the possible solutions. However, the man page of timegm gives a portable version of it, as timegm is not POSIX-compliant. Here it is:

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

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

    tz = getenv("TZ");
    if (tz)
        tz = strdup(tz);
    setenv("TZ", "", 1);
    tzset();
    ret = mktime(tm);
    if (tz) {
        setenv("TZ", tz, 1);
        free(tz);
    } else
        unsetenv("TZ");
    tzset();
    return ret;
}
Mongolic answered 8/3, 2013 at 19:6 Comment(8)
It also isn't thread-safe, but then again neither is mktime() nessecarily.Rutherford
if it's a c function, it's better to have a return typeForay
Nice catch! Yeah, that was a copy/paste error. This is fixed now.Mongolic
@T.E.D This is guaranteed to be thread-unsafe whereas mktime() usually is.Genital
@Mongolic i noticed your copy/paste is missing some code from the man pages, can you comment on why you skipped it? if (tz) tz = strdup(tz);Fdic
That was 4 years ago and now I have no Idea why :). But you're right, so I'm changing. Thanks for pointing this out.Mongolic
I think at that time I failed to understand that tz could be a statically allocated buffer, and didn't see the point in copying it... That was a really really bad idea, I can't think of a good reason why one would change an example from a man page.Mongolic
"portable version"? --> unsetenv() is not part of the standard C library. Still a good effort.Ogrady
D
5

timegm() works, but is not present on all systems.

Here's a version that only uses ANSI C. (EDIT: not strictly ANSI C! I'm doing math on time_t, assuming that the units are in seconds since the epoch. AFAIK, the standard does not define the units of time_t.) Note, it makes use of a hack, so-to-speak, to determine the machine's time zone and then adjusts the result from mktime accordingly.


/*
  returns the utc timezone offset
  (e.g. -8 hours for PST)
*/
int get_utc_offset() {

  time_t zero = 24*60*60L;
  struct tm * timeptr;
  int gmtime_hours;

  /* get the local time for Jan 2, 1900 00:00 UTC */
  timeptr = localtime( &zero );
  gmtime_hours = timeptr->tm_hour;

  /* if the local time is the "day before" the UTC, subtract 24 hours
    from the hours to get the UTC offset */
  if( timeptr->tm_mday < 2 )
    gmtime_hours -= 24;

  return gmtime_hours;

}

/*
  the utc analogue of mktime,
  (much like timegm on some systems)
*/
time_t tm_to_time_t_utc( struct tm * timeptr ) {

  /* gets the epoch time relative to the local time zone,
  and then adds the appropriate number of seconds to make it UTC */
  return mktime( timeptr ) + get_utc_offset() * 3600;

}

Declass answered 31/1, 2012 at 23:20 Comment(4)
Won't this fail if the time offset for the timeptr is different than the time offset at zero? In other words, I don't see how it properly accounts for a daylight savings time changeover.Rutherford
Anyway, no portable and correct implementation of UTC to time_t conversion existsMaurita
@T.E.D the question states that the timeptr in this case is UTC, not sure what your point is.Genital
This code won't work unless your time zone UTC offset is in whole hours. There are 12 time zones where this is not true. Ref. en.wikipedia.org/wiki/Time_zone#List_of_UTC_offsetsNamaqualand
S
5

The following implementation of timegm(1) works swimmingly on Android, and probably works on other Unix variants as well:

time_t timegm( struct tm *tm ) {
  time_t t = mktime( tm );
  return t + localtime( &t )->tm_gmtoff;
}
Settler answered 29/1, 2014 at 19:3 Comment(6)
Wish I could give this more than one upvote. This allows you to pull the DST delta from a UTC.Exterminate
@Dana: both mktime() and localtime() may introduce an error because a wrong UTC offset is used.Maurita
be careful with daylight saving time though... tm->tm_isdst must be 0 for this to work!Whitehead
As Leo says, this is not portable as tm_gmtoff isn't POSIX. It is available on BSDs and in the GNU C library however.Genital
Let the correct return value be time_t y. This answer fails in corner cases as the tm_gmtoff value used is based on t and not on y. So if tm_gmtoff differs from t and y, this code generates the wrong answer.Ogrady
This function will fail when called around the time when DST is turned of and failure will depend on the selected timezone of the machine making a call.Grunion
U
3

New answer for old question because C++20 chrono makes this operation very nearly trivial, and very efficient.

  • Threadsafe.
  • Does not involve the local UTC offset.
  • No iteration, not even within the chrono implementation.

#include <chrono>
#include <ctime>

std::time_t
my_timegm(std::tm const& t)
{
    using namespace std::chrono;
    return system_clock::to_time_t(
        sys_days{year{t.tm_year+1900}/(t.tm_mon+1)/t.tm_mday} +
        hours{t.tm_hour} + minutes{t.tm_min} + seconds{t.tm_sec});
}

<chrono> is designed so that you never have to deal with the C timing API again. But even when you do have to deal with it, <chrono> can make that easier too.

Update:

In response to the first comment below:

The subexpression year{t.tm_year+1900}/(t.tm_mon+1)/t.tm_mday creates a {year, month, day} structure called year_month_day. I.e. no computation is done to construct the year_month_day, it simply stores the three fields.

Then the year_month_day is converted to an equivalent date class called sys_days. This is a time_point based on system_clock with a precision of days. This holds a count of days since the Unix Time epoch of 1970-01-01. This conversion uses the algorithm days_from_civil described in detail at the link. Note that the algorithm contains no loops, and a good optimizer can get rid of the branches too (it does using clang at -O3).

Finally the time-of-day is added to the date, with chrono supplying all of the necessary conversion factors (multiply the day count by 86400, the hour count by 3600, etc.).

The result is a time_point based on system_clock with a precision of seconds. For all implementations of chrono I'm aware of, the system_clock::to_time_t function will simply unwrap the count of seconds so it can be stored in a time_t.

Uncomfortable answered 12/3, 2023 at 15:2 Comment(3)
Could you explain that a bit more? In particular, the sys_days construction intuits differently than what is actually happening. It looks like a string-like expression year{2023}/3/16 but it's actually arithmetic?Exploitation
Answer updated with more details.Uncomfortable
@Exploitation The trick is that std::chrono has overloads for operator/. So an expression like year{2023}/3/16 looks like two divisions, but those divisions are actually used to create a date. Specifically, the first one takes a year and an int, and it returns a year_month (it's listed as overload #2 at that link), and the second one takes the resulting year_month and an int and returns a year_month_day (overload #21).Togoland
G
2

POSIX page for tzset, describes global variable extern long timezone which contains the local timezone as an offset of seconds from UTC. This will be present on all POSIX compliant systems.

In order for timezone to contain the correct value, you will likely need to call tzset() during your program's initialization.

You can then just subtract timezone from the output of mktime to get the output in UTC.

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

time_t utc_mktime(struct tm *t)
{

    return (mktime(t) - timezone) - ((t->tm_isdst > 0) * 3600);
} 

int main(int argc, char **argv)
{
    struct tm t = { 0 };

    tzset();
    utc_mktime(&t);
}

Note: Technically tzset() and mktime() aren't guaranteed to be threadsafe.

If a thread accesses tzname, [XSI] [Option Start] daylight, or timezone [Option End] directly while another thread is in a call to tzset(), or to any function that is required or allowed to set timezone information as if by calling tzset(), the behavior is undefined.

...but the majority of implementations are. GNU C uses mutexes in tzset() to avoid concurrent modifications to the global variables it sets, and mktime() sees very wide use in threaded programs without synchronization. I suspect if one were to encounter side effects, it would be from using setenv() to alter the value of TZ as done in the answer from @liberforce.

Genital answered 8/3, 2017 at 18:32 Comment(3)
This was helpful to me, but I have found I need to add an additional check that t.tm_isdst > 0 before adding timezone.Jim
It should be mktime(t) - timezone. Also, mktime converts with respect to DST at t which is not account for at all.Kosher
One caveat is that this approach presumes that time zones are a fixed thing. When in a time zone that has seen a change in its offset between the date to convert and today, the result will be off.Orchid
F
1

I was troubled by the issue of mktime() as well. My solution is the following

time_t myTimegm(std::tm * utcTime)
{
    static std::tm tmv0 = {0, 0, 0, 1, 0, 80, 0, 0, 0};    //1 Jan 1980
    static time_t utcDiff =  std::mktime(&tmv0) - 315532801;

    return std::mktime(utcTime) - utcDiff;
}

The idea is to get the time difference by calling std::mktime() with a known time (in this case 1980/01/01) and subtract its timestamp (315532801). Hope it helps.

Fiftieth answered 29/12, 2017 at 13:27 Comment(3)
This assumes that time_t represents seconds, which isn't necessarily the case (though it usually is)Olecranon
Isn't the time_t value here off by 1? Seems like it should be 315532800.Yates
This is not reliable, as Howard Hinnant pointed out in a comment here: https://mcmap.net/q/364400/-std-mktime-and-timezone-info As a practical example, Russia experimented with "permanent DST" between 2011 and 2014. This means a date like 2012-01-01 in MSK (Moskau Standard Time) would be converted according to UTC+04, but the correction for 1.1.1980 would be according to UTC+03 assuming a historically accurate implementation.Crosscountry
O
1

Here's my take, which is based exclusively on time_t/tm conversion functions, and the only presumption it makes about time_t is that it is linear:

  1. Pretending against better knowledge the tm structure holds local time (non-DST if anyone asks; it doesn't matter, but must be consistent with step 3), convert it to time_t.
  2. Convert the date back into a tm structure, but this time in UTC representation.
  3. Pretending against better knowledge that tm structure to also hold local (non-DST if anyone asks, but more importantly consistent with step 1), and convert it to time_t once more.
  4. From the two time_t results I can now compute the difference between local time (non-DST if anyone asks) and UTC in time_t units.
  5. Adding that difference to the first time_t result gives me the proper time in UTC.

Note that computation of the difference can conceivably be done once, and then applied later to as many dates as desired; this might be a way to solve issues arising from the lack of thread-safety in gmtime.

(Edit: Then again, this might cause issues if the time zone is changed between the date used to compute the offset, and the date to be converted.)

tm tt;
// populate tt here
tt.tm_isdst = 0;
time_t tLoc = mktime(&tt);
tt = *gmtime(&tLoc);
tt.tm_isdst = 0;
time_t tRev = mktime(&tt);
time_t tDiff = tLoc - tRev;
time_t tUTC = tLoc + tDiff;

Caveat: If the system uses a TAI-based time_t (or anything else that does respect leap seconds), the resulting time may be off by 1 second if applied to a point in time close to a leap second insertion.

Orchid answered 12/8, 2021 at 16:27 Comment(1)
"this might cause issues if the time zone is changed between the date used to compute the offset, and the date to be converted" --> Not only that, but even if the TZ setting is constant, the non-daylight time UTC offset may changed in those few hours. Example Moscow in one day in 2014. Many other modern examples exist and may happen the future rendering this approach brittle.Ogrady
E
0

This is really a comment with code to address the answer by Leo Accend: Try the following:

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

/*
 *  A bit of a hack that lets you pull DST from your Linux box
 */

time_t timegm( struct tm *tm ) {           // From Leo's post, above
  time_t t = mktime( tm );
  return t + localtime( &t )->tm_gmtoff;
}
main()
{
    struct timespec tspec = {0};
    struct tm tm_struct   = {0};

    if (gettimeofday(&tspec, NULL) == 0) // clock_gettime() is better but not always avail
    {
        tzset();    // Not guaranteed to be called during gmtime_r; acquire timezone info
        if (gmtime_r(&(tspec.tv_sec), &tm_struct) == &tm_struct)
        {
            printf("time represented by original utc time_t: %s\n", asctime(&tm_struct));
            // Go backwards from the tm_struct to a time, to pull DST offset. 
            time_t newtime = timegm (&tm_struct);
            if (newtime != tspec.tv_sec)        // DST offset detected
            {
                printf("time represented by new time_t: %s\n", asctime(&tm_struct));

                double diff = difftime(newtime, tspec.tv_sec);  
                printf("DST offset is %g (%f hours)\n", diff, diff / 3600);
                time_t intdiff = (time_t) diff;
                printf("This amounts to %s\n", asctime(gmtime(&intdiff)));
            }
        }
    }
    exit(0);
}
Exterminate answered 26/6, 2014 at 20:56 Comment(0)
A
0

For all timezones and at all times would be exceedingly difficult if not impossible. You would need an accurate record of all the various arbitrary timezone and daylight savings time (DST) decrees. Sometimes, it is not clear who the local authority is, never mind what was decreed and when. Most systems, for example, are off by one second for uptime (time system has been up) or boottime (timestamp system booted), if a leap second was spanned. A good test would be a date that was once in DST but now is not (or vis versa). (It was not too long ago in the US that it changed.)

Antony answered 7/9, 2021 at 16:39 Comment(0)
O
0

Souce code copied from timegm():

https://sources.debian.org/src/tdb/1.2.1-2/libreplace/timegm.c/

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

time_t rep_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;
    unsigned i;

    if (tm->tm_mon > 12 ||
        tm->tm_mon < 0 ||
        tm->tm_mday > 31 ||
        tm->tm_min > 60 ||
        tm->tm_sec > 60 ||
        tm->tm_hour > 24) {
        /* invalid tm structure */
        return 0;
    }
    
    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;
}

Test by switching the timezone

int main()
{
    struct tm utc = {};
    utc.tm_year = 1972 - 1900;
    utc.tm_mon = 1 - 1;
    utc.tm_mday = 1;

    time_t calendar = rep_timegm(&utc);
    printf("is_leap: %d\n", is_leap(utc.tm_year));
    printf("timegm: %ld\n", calendar);
    assert(calendar == 63072000);

  return 0;
}
Oloroso answered 2/3, 2023 at 6:43 Comment(6)
timezone offsets never change with time? I have to change my local clocks twice a year. I wish I lived in your timezone...Uncomfortable
mktime(...) depends on the system local timezone, it will change with your timezone, just test it, no need to live in my era. mktime("1970-1-1") will give you an opposite timezone offset, it depends on your system.Oloroso
I tested it & learned something new, thanks. Your code implies .tm_idst = 0 which means Daylight Saving Time is not in effect. Your code is correct for every time zone for which the standard UTC offset for 1970-01-01 is the same as the standard offset for the utc argument. For my time zone "America/New_York", this is correct: UTC standard offset = -5h always. This negates the Daylight Saving effect I anticipated. However your code still fails for time zones where the standard UTC offset changed since 1970. In "Pacific/Apia" after 2011 your code is off by 1 day. Haven't checked other zones.Uncomfortable
Interesting discussion for a 14 years old question. Seconds since "January 1, 1970" is the unix time definition, it maybe not precise to UTC due to leap seconds, I hate the leap seconds. So the mktime("January 1, 1970") will always give you a system correct opposite timezone offset in Unix time, if not exploring the universe, I am confident to use the Unix/Linux mktime().Oloroso
When I test your code with my machine set to Europe/London for the UTC date 1972-01-01 00:00:00 it gives 63075600. Is that what it gives for you? The correct answer is 63072000 == 365*2*86400.Uncomfortable
OK, it seems the leap seconds issue, thanks for pointing out, now I am not confident with the system mktime() anymore, let's just copy the source code from timegm(0), it calculates absolutely, I updated the answer.Oloroso

© 2022 - 2024 — McMap. All rights reserved.