LastSecondsToLive
LastSecondsToLive

Reputation: 744

Custom time struct to time_t

In my current project I have a C struct to hold a timestamp, which looks like this:

struct timestamp {
   uint16_t year;
   uint8_t month;
   uint8_t day;
   uint8_t hour;
   uint8_t min;
   uint8_t second;
}

Now I want to calculate the difference between two of these timestamps in seconds. For now I'm converting my timestamp struct to the C standard struct struct tm (defined in <time.h>). Then I convert the struct to time_t with mtkime(), which takes a pointer to a struct tm and returns a time_t. And difftime() to calculate the difference between two time_t's in seconds.

I don't want to write my own difftime(), since I do not want to deal with leapyears or even worse leap seconds by myself, and I don't use the struct tm in my code because it holds a lot of values I don't frequently need (like week-day or year-day).

Here is an example, what I do at the moment:

void customTimestampToStructTM(struct customTimestamp *in, struct tm *out) {
  out->tm_year = in->year;
  out->tm_mon  = in->mon;
  out->tm_mday = in->day;
  out->tm_hour = in->hour;
  out->tm_min  = in->min;
  out->tm_sec  = in->sec;
}

void someFunction() {
   struct customTimestamp c1;
   struct customTimestamp c2;
   // Fill c1 and c2 with data here.

   struct tm tmpC1;
   struct tm tmpC2;

   customTimestampToStructTM(&c1, &tmpC1);
   customTimestampToStructTM(&c2, &tmpC2);

   double diffInSeconds = difftime(mktime(tmpC1), mktime(tmpC2));
   // Use diffInSeconds
}

This works, but seem incredibly inefficient. How can I speed this up? I read here that mktime doesn't use the other fields in the struct tm - except for isdst. Is there a convenient way to convert my struct to time_t, without using struct tm as a bridge and without the need to deal with leapyears/seconds?

To clarify: time_t holds dates in amount of milliseconds passed since a specific date (1 Jan 1970).

Upvotes: 1

Views: 2471

Answers (2)

Nominal Animal
Nominal Animal

Reputation: 39426

I shall assume the timestamps always refer to UTC time, in which case Daylight Savings Time does not apply (and you'll want to specify tm.isdst = 0).

(I suspect that it would be optimal in this case to have the time_t in UTC, but the broken-down fields in local time. Below, I'll just assume that the local timezone is UTC, with no DST changes.)

Personally, I'd save both the time_t and the split fields, and use helper functions to set/modify the timestamps.

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>

struct timestamp {
    time_t  packed;
    int16_t year;
    int8_t  month;
    int8_t  day;
    int8_t  hour;
    int8_t  min;
    int8_t  sec;
};

static inline int set_timestamp_time(struct timestamp *const ref, const time_t when)
{
    struct tm temp = { 0 };

    if (!gmtime_r(&when, &temp))
        return ERANGE; /* Year didn't fit. */

    ref->packed = when;
    ref->year   = temp.tm_year + 1900;
    ref->month  = temp.tm_mon  + 1;
    ref->day    = temp.tm_mday;
    ref->hour   = temp.tm_hour;
    ref->min    = temp.tm_min;
    ref->sec    = temp.tm_sec;

    return 0;
}

static inline int set_timestamp(struct timestamp *const ref,
                                const int year, const int month, const int day,
                                const int hour, const int min, const int sec)
{
    struct tm temp = { 0 };

    temp.tm_year = year - 1900;
    temp.tm_mon  = month - 1;
    temp.tm_mday = day;
    temp.tm_hour = hour;
    temp.tm_min  = min;
    temp.tm_sec  = sec;

    /* We assume timestamps are in UTC, and Daylight Savings Time does not apply. */
    temp.tm_isdst = 0;

    ref->packed = mktime(&temp);

    ref->year  = temp.tm_year + 1900;
    ref->month = temp.tm_mon + 1;
    ref->day   = temp.tm_mday;
    ref->hour  = temp.tm_hour;
    ref->min   = temp.tm_min;
    ref->sec   = temp.tm_sec;

    return 0;
}

set_timestamp() sets the timestamp based on split fields (year, month, day, hour, minute, second), whereas set_timestamp_time() sets it based on POSIX time. Both functions always update all timestamp fields.

This allows fast access to both the Unix time, and the split fields, but uses slightly more memory (8 bytes per timestamp, typically; 160 megabytes additional memory for 20 million records).


If you do not need the exact number of seconds between two timestamps, but only use the time_t to compare whether one is before or after another, then I recommend using a single int64_t to describe your timestamps:

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

/* Timestamps can be compared as integers, like POSIX time_t's.
 * The difference between two timestamps is at least their
 * difference in seconds, but may be much larger.
 *
 * Zero is not a valid timestamp!
*/

typedef int64_t  timestamp;

#define TIMESTAMP_YEAR(t)  ((int64_t)(t) / 67108864)
#define TIMESTAMP_MONTH(t) (((uint32_t)(t) >> 22) & 15)
#define TIMESTAMP_DAY(t)   (((uint32_t)(t) >> 17) & 31)
#define TIMESTAMP_HOUR(t)  (((uint32_t)(t) >> 12) & 31)
#define TIMESTAMP_MIN(t)   (((uint32_t)(t) >>  6) & 63)
#define TIMESTAMP_SEC(t)    ((uint32_t)(t)        & 63)

static inline time_t timestamp_time(const timestamp t, struct tm *const tm_to)
{
    struct tm  temp = { 0 };
    time_t     result;
    uint32_t   u = t & 67108863U;

    temp.tm_sec  = u & 63; u >>= 6;
    temp.tm_min  = u & 63; u >>= 6;
    temp.tm_hour = u & 31; u >>= 5;
    temp.tm_mday = u & 31; u >>= 5;
    temp.tm_mon  = u - 1;
    temp.tm_year = ((int64_t)t / 67108864) - 1900;

    /* UTC time, thus Daylight Savings Time does not apply. */
    temp.tm_isdst = 0;

    result = mktime(&temp);

    if (tm_to)
        *tm_to = temp;

    return result;
}

static inline double difftimestamp(const timestamp t1, const timestamp t2)
{
    return difftime(timestamp_time(t1, NULL), timestamp_time(t2, NULL));
}

static inline timestamp set_timestamp_time(const time_t when, struct tm *const tm_to)
{
    struct tm  temp = { 0 };

    if (!gmtime_r(&when, &temp))
        return 0;

    if (tm_to)
        *tm_to = temp;

    return (int64_t)67108864 * ((int64_t)temp.tm_year + 1900)
         + (int64_t)((temp.tm_mon + 1) << 22)
         + (int64_t)(temp.tm_mday << 17)
         + (int64_t)(temp.tm_hour << 12)
         + (int64_t)(temp.tm_min << 6)
         + (int64_t)temp.tm_sec;
}

static inline timestamp set_timestamp(const int year, const int month, const int day,
                                      const int hour, const int min, const int sec,
                                      struct tm *const tm_to, time_t *const time_to)
{
    struct tm  temp = { 0 };

    temp.tm_year = year - 1900;
    temp.tm_mon  = month - 1;
    temp.tm_mday = day;

    temp.tm_hour = hour;
    temp.tm_min  = min;
    temp.tm_sec  = sec;

    temp.tm_isdst = 0; /* Since timestamps are in UTC, Daylight Savings Time does not apply. */

    if (time_to)
        *time_to = mktime(&temp);

    if (tm_to)
        *tm_to = temp;

    return (int64_t)67108864 * ((int64_t)temp.tm_year + 1900)
         + (int64_t)((temp.tm_mon + 1) << 22)
         + (int64_t)(temp.tm_mday << 17)
         + (int64_t)(temp.tm_hour << 12)
         + (int64_t)(temp.tm_min << 6)
         + (int64_t)temp.tm_sec;
}

The idea here is that you can compare two timestamps trivially; a < b if and only if timestamp a is before b; a == b if and only if the timestamps refer to the same second, and a > b if and only if a is after b. At the same time, the accessor macros TIMESTAMP_YEAR(a), TIMESTAMP_MONTH(a), TIMESTAMP_DAY(a), TIMESTAMP_HOUR(a), TIMESTAMP_MIN(a), and TIMESTAMP_SEC(a) allow very fast access to the individual date and time components. (On typical Intel/AMD 64-bit hardware, it may be even faster than accessing byte-sized fields.)

The difftimestamp() function yields the exact number of seconds between two timestamps, but it is quite slow. (As I mentioned, this approach is best only if you don't need this, or only need it rarely.)

timestamp_time() converts a timestamp to a time_t, optionally saving the struct tm fields to a specified pointer (if not NULL).

set_timestamp_time() returns a timestamp based on a time_t. If the year does not fit in an int, it will return 0 (which is NOT a valid timestamp). If the second parameter is not NULL, the corresponding struct tm is stored there.

set_timestamp() returns a timestamp based on year, month, day, hour, minute, and second. If they refer to an impossible date or time, they are corrected (by mktime()). If the seventh parameter is not NULL, the resulting struct tm is stored there. If the eighth parameter is not NULL, then the resulting time_t is stored there.

Upvotes: 0

Craig Estey
Craig Estey

Reputation: 33666

mktime [and localtime] are non-trivial for all edge cases. They're also highly optimized, so you're unlikely to do better speedwise.

So, just use them [doing the fast fill of (e.g.) struct tm temp you're already doing].

But, a speedup is to add time_t tod to your struct. Fill it from mktime once when you create your struct. This can save many repetitive/duplicate calls to mktime.

You can even defer the mktime call (i.e. only some of your structs may need it). Set tod to a sentinel value (e.g. -2). When you actually need to use tod, fill it from mktime if tod is the sentinel value

Upvotes: 3

Related Questions