1

While running the following C code, I noticed that mktime() apparently does some time zone conversions when called.

void process_time(struct tm * pt) {
    printf("pt %02d-%02d-%02d %02d:%02d:%02d\n", pt->tm_year, pt->tm_mon, pt->tm_mday,
            pt->tm_hour, pt->tm_min, pt->tm_sec);

    ret = mktime(pt);

    printf("pt %02d-%02d-%02d %02d:%02d:%02d\n", pt->tm_year, pt->tm_mon, pt->tm_mday,
            pt->tm_hour, pt->tm_min, pt->tm_sec);

    /* more code here */
}

In some cases (which turned out to be due to some struct tm members not being properly initialized), I noticed that after the call to mktime(), pt->tm_hour was one less than it was before. Local system time is an hour east of UTC, so that corresponds to a local-to-UTC conversion.

According to this source, struct tm should merely have the usual YYYY-MM-DD-hh-mm-ss fields, in addition to weekday, day of the year and a DST flag—no mention of the time zone or UTC offset.

However, when I examine the struct on Ubuntu (/usr/include/time.h), I notice two extra fields for UTC offset and a time zone string.

What is the interface contract for struct tm? Does it mandate these time zone conversions, and if so, how are struct tm members supposed to be interpreted regarding time zones after mktime() has normalized them?

user149408
  • 5,385
  • 4
  • 33
  • 69
  • 1
    I would not expect `tm_hour` to change value, *unless* it or `tm_min` or `tm_sec` were nonnormalized, or if `tm_isdst` was passed in as 0 or 1, and was mismatched with the actual dst status of the time being converted. In general, you should set `tm_isdst` to -1 before calling `mktime`. Does this help? How were you passing `tm_isdst`? – Steve Summit Feb 01 '18 at 20:05
  • For much more information on best practices for time programming, see [this SO question](https://stackoverflow.com/questions/2532729/daylight-saving-time-and-time-zone-best-practices) and [this web page](http://www.catb.org/esr/time-programming/). – Steve Summit Feb 02 '18 at 16:15

2 Answers2

2

What is the interface contract for struct tm?

The key parts of mktime() specification is:

The mktime function converts the broken-down time, expressed as local time ... The original values of the tm_wday and tm_yday components of the structure are ignored, and the original values of the other components are not restricted to the ranges indicated above. ... On successful completion, ... components are set to represent the specified calendar time, but with their values forced to the ranges indicated above.
C11dr §7.27.2.3 2

The values of tm_wday and tm_yday are ignored. Members specifying the year, month, day, hour, minute, second and the .isdst contribute, but other platform specified ones can affect the result. This could include nanosecond, timezone, UTC offset, etc. members.

Robust code sets all members.

struct tm t = {0};        // clear **all** members
t.tm_year = 2018 - 1900;  // Assign some members
t.tm_mon = 2 - 1;
t.tm_mday = 1; // today
t.tm_isdst = -1;          // Let system determine if DST applies for this date
process_time(&t);

I noticed that after the call to mktime(), pt->tm_hour was one less than it was before.

This is likely due to .tm_isdst < 0.

The value of tm_isdst is positive if Daylight Saving Time is in effect, zero if Daylight Saving Time is not in effect, and negative if the information is not available. §7.27.1 4
Footnote 320: A negative value causes it to attempt to determine whether Daylight Saving Time is in effect for the specified time.

Add .tm_isdst to 2 lines in process_time() to see the effect.

 printf("pt %02d-%02d-%02d %02d:%02d:%02d  %d\n", 
     pt->tm_year, pt->tm_mon, pt->tm_mday, 
     pt->tm_hour, pt->tm_min, pt->tm_sec, pt->tm_isdst);

how are struct tm members supposed to be interpreted regarding time zones after mktime() has normalized them?

As normalized local time.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
1

chux answered most of this. Here's a bit more info.

You asked about tm_gmtoff and tm_zone. These are nonstandard additions to struct tm, although they're both very useful and very common. They are ignored on input to mktime, but set on output. They probably have nothing to do with the unusual result you saw.

Steve Summit
  • 45,437
  • 7
  • 70
  • 103
  • `tm_gmtoff and tm_zone` [...] both very useful and very common` indeed, though it makes me wonder why the original authors never thought about including timezone information in `struct tm`, or about a `mktime()` variant capable of processing UTC timestamps… – user149408 Feb 01 '18 at 20:32
  • 1
    @user149408 As you may know, the third thing that's both very useful and very common, though nonstandard, is `timegm`, the `mktime` variant for UTC timestamps. These things were left out originally, I think, because it took a while for the C/Unix community to really figure out how to handle time zones well. The need for the `tm_zone` and `tm_gmtoff` members, and the inadequacy of the mechanisms that preceded them, took a while to be realized. – Steve Summit Feb 01 '18 at 20:45
  • @SteveSummit Concerning `tm_gmtoff, tm_zone` members being very common: Aside from glibc compliant libraries, Any examples of a library having these in `struct tm`? I have not noticed these as common - especially in embedded compilers. – chux - Reinstate Monica Feb 01 '18 at 21:29
  • @chux Besides glibc, I believe they're present in BSD and MacOS and many (though probably not all) "mainframe" Unix editions (Solaris, etc.). But I wouldn't argue with an impression that they're not common in the embedded world. – Steve Summit Feb 01 '18 at 21:49
  • Solaris might [not](https://github.com/oetiker/rrdtool-1.x/issues/330) have `tm_gmtoff` and MS does not appear to have it either. IMO, a missing `mktime_UTC()` is a hole in the Standard C library. – chux - Reinstate Monica Feb 01 '18 at 22:22