Search code examples
cmktime

Interface contract for struct tm and mktime() regarding time zone conversions


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?


Solution

  • 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.