user24560
user24560

Reputation: 928

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

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.

Upvotes: 35

Views: 57221

Answers (13)

Loki Astari
Loki Astari

Reputation: 264729

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).

Upvotes: 36

Howard Hinnant
Howard Hinnant

Reputation: 219488

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.

Upvotes: 5

Sunding Wei
Sunding Wei

Reputation: 2234

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;
}

Upvotes: 0

user11844224
user11844224

Reputation:

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.)

Upvotes: 0

Christoph Lipka
Christoph Lipka

Reputation: 668

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.

Upvotes: 1

Arran Cudbard-Bell
Arran Cudbard-Bell

Reputation: 6075

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.

Upvotes: 2

apapa
apapa

Reputation: 708

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

Upvotes: 11

Bo Tian
Bo Tian

Reputation: 317

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.

Upvotes: 1

liberforce
liberforce

Reputation: 11454

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;
}

Upvotes: 8

DTiedy
DTiedy

Reputation: 91

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;
}

Upvotes: 9

Dana
Dana

Reputation: 364

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);
}

Upvotes: 0

Leo Accend
Leo Accend

Reputation: 321

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;
}

Upvotes: 5

Tom
Tom

Reputation: 19302

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;

}

Upvotes: 5

Related Questions