I want to convert a UTC date & time given in numbers for year, month, day, etc. to a time_t. Some systems offer functions like mkgmtime
or timegm
For completeness, here's a version of mkgmtime() that takes a struct tm* as argument:
static time_t mkgmtime(const struct tm *ptm) {
time_t secs = 0;
// tm_year is years since 1900
int year = ptm->tm_year + 1900;
for (int y = 1970; y < year; ++y) {
secs += (IsLeapYear(y)? 366: 365) * SecondsPerDay;
}
// tm_mon is month from 0..11
for (int m = 0; m < ptm->tm_mon; ++m) {
secs += DaysOfMonth[m] * SecondsPerDay;
if (m == 1 && IsLeapYear(year)) secs += SecondsPerDay;
}
secs += (ptm->tm_mday - 1) * SecondsPerDay;
secs += ptm->tm_hour * SecondsPerHour;
secs += ptm->tm_min * SecondsPerMinute;
secs += ptm->tm_sec;
return secs;
}
Here is a solution I came up with for myself after not finding anything in the standard library to do this for me. This methods only uses basic arithmetic for it calculations making it much faster than looping over every year between 1970 and the date provided. But as with most of the previous answers, this one depends on time_t being implemented using Unix/Epoch time, and doesn't work for timestamps older than 1970, which is't necessary for me.
#include <ctime>
#include <cassert>
constexpr unsigned int count_leapyears(unsigned int year) {
assert(year > 0);
return year / 4 - year / 100 + year / 400;
}
time_t timeutc(tm utc) {
assert(utc.tm_year >= 70);
constexpr unsigned int const leaps_before_epoch = count_leapyears(1970);
unsigned int leapdays = count_leapyears(utc.tm_year + 1899) - leaps_before_epoch;
unsigned int unix_time;
unix_time = ((utc.tm_year - 70) * 365 + leapdays) * 86400;
unix_time += utc.tm_yday * 86400 + utc.tm_hour * 3600 + utc.tm_min * 60 + utc.tm_sec;
return unix_time;
}
I have decided to implement my own version of mkgmtime and it was easier than I thought.
const int SecondsPerMinute = 60;
const int SecondsPerHour = 3600;
const int SecondsPerDay = 86400;
const int DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool IsLeapYear(short year)
{
if (year % 4 != 0) return false;
if (year % 100 != 0) return true;
return (year % 400) == 0;
}
time_t mkgmtime(short year, short month, short day, short hour, short minute, short second)
{
time_t secs = 0;
for (short y = 1970; y < year; ++y)
secs += (IsLeapYear(y)? 366: 365) * SecondsPerDay;
for (short m = 1; m < month; ++m) {
secs += DaysOfMonth[m - 1] * SecondsPerDay;
if (m == 2 && IsLeapYear(year)) secs += SecondsPerDay;
}
secs += (day - 1) * SecondsPerDay;
secs += hour * SecondsPerHour;
secs += minute * SecondsPerMinute;
secs += second;
return secs;
}
My main concern was that mkgmtime
must be consistent with gmtime
. Such that gmtime(mktime(t))
returns the original input values. Therefore I have compared the results for all multiples of 61 between 0 and MAX_INT for time_t and they are indeed equal (at least on my system). Therefore the above routine is correct.
This outcome also means that the C library does not take leap seconds into account, which is a bad thing in itself but good for my purpose. The two functions will stay consistent for a long time. To be absolutely sure, my Timestamp class that uses this function always performs a quick check on program start and proves the consistency for a couple of meaningful values.
As noted above, while time_t
usually represents seconds elapsed since Jan 1, 1970, this is not specified anywhere. An implementation which uses a different internal representation may show up any time, and any code that makes assumptions about the inner workings of time_t
will not work correctly there.
After giving it some thought, I came up with the following:
time_t mkgmtime(struct tm * pt) {
time_t ret;
/* GMT and local time */
struct tm * pgt, * plt;
ret = mktime(pt);
pgt = g_memdup(gmtime(ret), sizeof(struct tm));
plt = g_memdup(localtime(ret), sizeof(struct tm));
plt->tm_year -= pgt->tm_year - plt->tm_year;
plt->tm_mon -= pgt->tm_mon - plt->tm_mon;
plt->tm_mday -= pgt->tm_mday - plt->tm_mday;
plt->tm_hour -= pgt->tm_hour - plt->tm_hour;
plt->tm_min -= pgt->tm_min - plt->tm_min;
plt->tm_sec -= pgt->tm_sec - plt->tm_sec;
ret = mktime(plt);
g_free(pgt);
g_free(plt);
return ret;
}
One could probably optimize this further by dropping plt
(using pt
in its place and omitting the localtime()
and g_free(plt)
calls).
This should work across all implementations which expose mktime()
, gmtime()
and localtime()
, including across DST switchover dates. (mktime()
will “normalize” out-of-range values, e.g. turning Jan 35 into Feb 4; I would also expect 9:50 DST in the middle of winter to become 8:50 standard time.)
It does suffer from one potential bug: if a time zone’s UTC offset changes for reasons not reflected in the DST flag, timestamps around the cutover time may get interpreted incorrectly: The standard case is when a legislation changes its time zone (e.g. Lithuania changed from Soviet time to CET after independence, and to EET a few years later). Some legislations had double DST in mid-summer, cycling through 3 different UTC offsets per year, which the DST flag cannot represent.