How to convert a UTC date & time to a time_t in C++?
Asked Answered
S

4

11

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 this purpose but that is not standard and does not exist on my Solaris system.

The only solution I have found so far involves setting the local time zone to UTC with setenv and then call mktime. However this approach is not thread-safe, slow, not portable and even generates a memory leak on my system.

I have also seen approaches that tried to determine the current UTC offset using gmtime and then adding that to the result of mktime. But as far as I have seen all those approaches had gaps. After all, the conversion from the local time to UTC is not unique.

What do you think is the best solution?

Seventy answered 10/9, 2012 at 13:44 Comment(2)
@Fred Larson: This is not about converting a string but about calculating time_t from the single numbers.Seventy
I see. I misunderstood the question.Morrow
S
12

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.

Seventy answered 10/9, 2012 at 13:44 Comment(6)
I know this is a few months old, but your question wanted something portable, and this isn't, since you can't on time_t representing seconds since 1970, or being seconds at all, for that matter. The C standard (and, by reference, the C++ standard) doesn't define what how time_t represents time, just that it must be a real type capable of doing so.Fertilization
Sigh. Should be "can't rely on time_t representing seconds since 1970", and "doesn't define how time_t represents time". Too early in the morning.Fertilization
@PaulGriffiths: (Another months later ;-)) I see what you mean, but is there a better way? At least this is as "portable" as I need it, because time_t is seconds since 1970 on Linux, Windows and Solaris.Seventy
Lot's of different ways to define "better", here, but this answer of mine gives another method, and this code of mine implements that method.Fertilization
In retrospect, that answer gives only part of the method, that of calculating a time_t value for a specified number of seconds. The other part is computing the difference in seconds between two struct tms, one returned from localtime() and another returned from gmtime(). The comments in the code should hopefully make the logic clear. I think there's some unit tests in that project that cover some cases way out on the edge to demonstrate that it works.Fertilization
Fwiw, here is code for getting the days since (or before) 1970-01-01 which does not involve iteration: howardhinnant.github.io/date_algorithms.html#days_from_civilSufism
A
10

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;
}
Appearance answered 6/11, 2015 at 18:48 Comment(0)
D
1

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.

Dorise answered 23/1, 2018 at 23:19 Comment(0)
T
1

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;
}
Toluene answered 19/12, 2019 at 1:48 Comment(2)
Good answer, avoiding the loops. If you do static_cast<std::time_t> (utc.tm_year - 70) it appears to work for every timestamp (second) from 1970 up until at least 2200 (I bruteforced it), i.e. is immune both to the Year-2038 problem (signed 32bit overflow) and the Year-2106 problem (unsigned 32bit overflow), if std::time_t is 64bit.Jambalaya
could you add function that does time_t timeutc(int year, int month, int day, int hour, int second)Quadriplegia

© 2022 - 2024 — McMap. All rights reserved.