Convert date and time numbers to time_t AND specify the timezone
Asked Answered
Y

7

10

I have the following integers:

int y, mon, d, h, min, s;

Their values are: 2012, 06, 27, 12, 47, 53 respectively. I want to represent the date time of "2012/06/27 12:47:53 UTC" if I have selected 'UTC' somewhere else in my application, or "2012/06/27 12:47:53 AEST" if I have selected 'AEST' somewhere else in my application.

I want to convert this into a time_t, and here's the code that I am current using to do so:

struct tm timeinfo;
timeinfo.tm_year = year - 1900;
timeinfo.tm_mon = mon - 1;
timeinfo.tm_mday = day;
timeinfo.tm_hour = hour;
timeinfo.tm_min = min;
timeinfo.tm_sec = sec;
//timeinfo.tm_isdst = 0; //TODO should this be set?

//TODO find POSIX or C standard way to do covert tm to time_t without in UTC instead of local time
#ifdef UNIX
return timegm(&timeinfo);
#else
return mktime(&timeinfo); //FIXME Still incorrect
#endif

So I am using a tm struct and mktime, however this is not working well, because it is always assuming my local time-zone.

What is the correct way of doing this?

So below is the solution that I have come up with so far. It basically does one of three things:

  1. If UNIX, simply use timegm
  2. If not UNIX
    1. Either, do math using the difference between UTC epoch and local epoch as an offset
      • Reservation: Math may be incorrect
    2. Or, set the "TZ" environment variable to UTC temporarily
      • Reservation: will trip up if/ when this code needs to be multithreaded
namespace tmUtil
{
    int const tm_yearCorrection = -1900;
    int const tm_monthCorrection = -1;
    int const tm_isdst_dontKnow = -1;

#if !defined(DEBUG_DATETIME_TIMEGM_ENVVARTZ) && !(defined(UNIX) && !defined(DEBUG_DATETIME_TIMEGM))
    static bool isLeap(int year)
    {
        return
            (year % 4) ? false
            : (year % 100) ? true
            : (year % 400) ? false
            : true;
    }

    static int daysIn(int year)
    {
        return isLeap(year) ? 366 : 365;
    }
#endif
}

time_t utc(int year, int mon, int day, int hour, int min, int sec)
{
    struct tm time = {0};
    time.tm_year = year + tmUtil::tm_yearCorrection;
    time.tm_mon = mon + tmUtil::tm_monthCorrection;
    time.tm_mday = day;
    time.tm_hour = hour;
    time.tm_min = min;
    time.tm_sec = sec;
    time.tm_isdst = tmUtil::tm_isdst_dontKnow;

    #if defined(UNIX) && !defined(DEBUG_DATETIME_TIMEGM) //TODO remove && 00
        time_t result;
        result = timegm(&time);
        return result;
    #else
        #if !defined(DEBUG_DATETIME_TIMEGM_ENVVARTZ)
            //TODO check that math is correct
            time_t fromEpochUtc = mktime(&time);

            struct tm localData;
            struct tm utcData;
            struct tm* loc = localtime_r (&fromEpochUtc, &localData);
            struct tm* utc = gmtime_r (&fromEpochUtc, &utcData);
            int utcYear = utc->tm_year - tmUtil::tm_yearCorrection;
            int gmtOff =
                (loc-> tm_sec - utc-> tm_sec)
                + (loc-> tm_min - utc-> tm_min) * 60
                + (loc->tm_hour - utc->tm_hour) * 60 * 60
                + (loc->tm_yday - utc->tm_yday) * 60 * 60 * 24
                + (loc->tm_year - utc->tm_year) * 60 * 60 * 24 * tmUtil::daysIn(utcYear);

            #ifdef UNIX
                if (loc->tm_gmtoff != gmtOff)
                {
                    StringBuilder err("loc->tm_gmtoff=", StringBuilder((int)(loc->tm_gmtoff)), " but gmtOff=", StringBuilder(gmtOff));
                    THROWEXCEPTION(err);
                }
            #endif

            int resultInt = fromEpochUtc + gmtOff;
            time_t result;
            result = (time_t)resultInt;
            return result;
        #else
            //TODO Find a way to do this without manipulating environment variables
            time_t result;
            char *tz;
            tz = getenv("TZ");
            setenv("TZ", "", 1);
            tzset();
            result = mktime(&time);
            if (tz)
                setenv("TZ", tz, 1);
            else
                unsetenv("TZ");
            tzset();
            return result;
        #endif
    #endif
}

N.B. StringBuilder is an internal class, it doesn't matter for the purposes of this question.

More info:

I know that this can be done easily using boost, et al. But this is NOT and option. I need it to be done mathematically, or using a c or c++ standard function, or combinations thereof.

timegm appears to solve this problem, however, it doesn't appear to part of the C / POSIX standard. This code currently is compiled on multiple platforms (Linux, OSX, WIndows, iOS, Android (NDK)), so I need to find a way to make it work across all of these platforms, even if the solution involves #ifdef $PLATFORM type things.

Yesseniayester answered 27/6, 2012 at 2:57 Comment(6)
Do you have any other libraries available? Do you have a list of timezones & their offsets?Carrew
@Carrew No, I do not want to use any libraries, as they incur much development overhead when cross compiling. Also, no I do not have a list of timezones and their offsets. Take a look at my update above, I have in there a way to calculate timezone offsets - does it look correct to you?Yesseniayester
<UGLY HACK> You could convert it to a string with strftime(), replace the timezone in the string and then convert it back with mktime(strptime()) </UGLY HACK>, but I would just convince the powers that be that boost is in fact an option.Gravamen
@Gravamen Could you post that as an answer?Yesseniayester
Sure, but I'm actually kind of ashamed that I even posted it as a comment :-)Gravamen
@Gravamen : even though, as you say, it is ugly (not to mention, not very efficient), it does mean that I [1] do not have to do any math (and have to worry about its correctness), or [2] manipulate any environment variables (and worry about multi-threading issues), thus avoiding the pitfalls of both of the methods I have come up with thus far. So you may answer with dignity intact!Yesseniayester
G
4

It makes me want to throw up in my mouth a little bit, but you could convert it to a string with strftime(), replace the timezone in the string and then convert it back with strptime() and into a time_t with mktime(). In detail:

#ifdef UGLY_HACK_VOIDS_WARRANTY
time_t convert_time(const struct tm* tm)
{
    const size_t BUF_SIZE=256;
    char buffer[BUF_SIZE];
    strftime(buffer,256,"%F %H:%M:%S %z", tm);
    strncpy(&buffer[20], "+0001", 5); // +0001 is the time-zone offset from UTC in hours
    struct tm newtime = {0};
    strptime(buffer, "%F %H:%M:%S %z", &newtime);
    return mktime(&newtime);
}
#endif

However, I would highly recommend you convince the powers that be that boost is an option after all. Boost has great support for custom timezones. There are other libraries that do this elegantly as well.

Gravamen answered 10/7, 2012 at 0:10 Comment(1)
@ smocking : awarding the bounty to you, as it works, and the only problem it really has is that it is uglyYesseniayester
T
4

If all you want is to convert a struct tm given in UTC to a time_t then you can do it like this:

#include <time.h>

time_t utc_to_time_t(struct tm* timeinfo)
{
    tzset(); // load timezone information (this can be called just once)

    time_t t = mktime(timeinfo);
    return t - timezone;
}

This basically converts the UTC time to time_t as if the given time was local, then applies a timezone correction to the result to bring it back to UTC.

Tested on gcc/cygwin and Visual Studio 2010.

I hope this helps!

Update: As you very well pointed out, my solution above may return time_t value that is one hour off when the daylight time savings state of the queried date is different than the one for the current time.

The solution for that problem is to have an additional function that can tell you if a date falls in the DST region or not, and use that and the current DST flag to adjust the time returned by mktime. This is actually easy to do. When you call mktime() you just have to set the tm_dst member to -1 and then the system will do its best to figure out the DST at the given time for you. Assuming we trust the system on this, then you can use this information to apply a correction:

#include <time.h>

time_t utc_to_time_t(struct tm* timeinfo)
{
    tzset(); // load timezone information (this can be called just once)

    timeinfo->tm_isdst = -1; // let the system figure this out for us
    time_t t = mktime(timeinfo) - timezone;

    if (daylight == 0 && timeinfo->tm_isdst != 0)
        t += 3600;
    else if (daylight != 0 && timeinfo->tm_isdst == 0)
        t -= 3600;
    return t;
}
Thunderclap answered 4/7, 2012 at 17:54 Comment(4)
Miguel : Thanks for your answer. One problem that I see with this is that the value of timezone in time.h represents the difference in time from UTC at the present moment, and not at the point of time which you are calculating. This means that the time calculated will be off by one hour when you are computing times which are on the opposite daylight savings time that you are presently in (6 months per year). Have you taken this into account; or correct me if I am mistaken?Yesseniayester
Hmm. You are actually right, my solution can have an error of one hour, so the only way to solve that would be to have a historic database of daylight time savings changes in all the locations you want to support. Turns out such database exists, you can get it from iana.org/time-zones. They provide the data and also time functions that use it. I think it would be fairly easy to modify their implementation of mktime to assume a UTC timezone. The functions in this library compile on Unix, but it does not look like it would be a lot of work to port them to windows.Thunderclap
Or else, you can look at PHP's DateTime implementation which is based on the same database and compiles everywhere.Thunderclap
@bguiz: I believe I have found a complete solution to the problem that is still very portable. This is a pretty interesting problem!Thunderclap
G
4

It makes me want to throw up in my mouth a little bit, but you could convert it to a string with strftime(), replace the timezone in the string and then convert it back with strptime() and into a time_t with mktime(). In detail:

#ifdef UGLY_HACK_VOIDS_WARRANTY
time_t convert_time(const struct tm* tm)
{
    const size_t BUF_SIZE=256;
    char buffer[BUF_SIZE];
    strftime(buffer,256,"%F %H:%M:%S %z", tm);
    strncpy(&buffer[20], "+0001", 5); // +0001 is the time-zone offset from UTC in hours
    struct tm newtime = {0};
    strptime(buffer, "%F %H:%M:%S %z", &newtime);
    return mktime(&newtime);
}
#endif

However, I would highly recommend you convince the powers that be that boost is an option after all. Boost has great support for custom timezones. There are other libraries that do this elegantly as well.

Gravamen answered 10/7, 2012 at 0:10 Comment(1)
@ smocking : awarding the bounty to you, as it works, and the only problem it really has is that it is uglyYesseniayester
E
2

If you are on Linux or other UNIx or UNIX-like system then you might have a timegm function that does what you want. The linked manual page have a portable implementation so you can make it yourself. On Windows I know of no such function.

Ethos answered 27/6, 2012 at 5:15 Comment(2)
thanks for your input - doesn't fully solve my problem though, please see my update to the question.Yesseniayester
Just pinging you so you get my latest update! Do take a look, I'd like to hear your comments.Yesseniayester
B
1

After beating my head against this for days trying to get a timegm(1) function that works on Android (which does not ship with one), I finally discovered this simple and elegant solution, which works beautifully:

time_t timegm( struct tm *tm ) {
  time_t t = mktime( tm );
  return t + localtime( &t )->tm_gmtoff;
}

I don't see why this wouldn't be a suitable cross-platform solution.

I hope this helps!

Bigamous answered 29/1, 2014 at 18:53 Comment(1)
It wouldn't be suitable, because tm_gmtoff is not portable.Jipijapa
G
1
time_t my_timegm2(struct tm *tm)
{
    time_t ret = tm->tm_sec + tm->tm_min*60 + tm->tm_hour*3600 + tm->tm_yday*86400;
    ret += ((time_t)31536000) * (tm->tm_year-70);
    ret += ((tm->tm_year-69)/4)*86400 - ((tm->tm_year-1)/100)*86400 + ((tm->tm_year+299)/400)*86400;
    return ret;
}
Goins answered 4/2, 2022 at 20:37 Comment(0)
D
0

There seems to be a simpler solution:

#include <time64.h>
time_t timegm(struct tm* const t) 
{
  return (time_t)timegm64(t);
}

Actually I have not testet yet if really it works, because I still have a bit of porting to do, but it compiles.

Deyo answered 8/12, 2014 at 21:50 Comment(0)
S
0

Here's my solution:

#ifdef WIN32
#   define timegm _mkgmtime
#endif

struct tm timeinfo;
timeinfo.tm_year = year - 1900;
timeinfo.tm_mon = mon - 1;
timeinfo.tm_mday = day;
timeinfo.tm_hour = hour;
timeinfo.tm_min = min;
timeinfo.tm_sec = sec;

return timegm(&timeinfo);

This should work both for unix and windows

Savour answered 30/8, 2018 at 14:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.