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:
timegm
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.
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;
}