On the asymmetry of mktime()

[Updated 4/27/2010]

Unix represents time in one of two ways: a
time_t value, which is the number of seconds since some arbitrary start; and a struct tm value, which represents time in calendar form: mm/dd/yyyy hh:mm:ss {dst}. The mktime() function converts a calendar form to a time_t value.

There are two ways to move forward and backward in time. One is to add or subtract seconds from a
time_t value. Four hours from now is time_t + 14,400. The other is to use mktime(). For example, given an initialized struct tm value, this code could be used to move ahead 4 hours:

       tm.tm_hour += 4;
       tm.tm_isdst = -1;
       t = mktime(&tm);

Since 4 hours is 14,400 seconds, one might also consider writing:

       tm.tm_sec += 14400;
       tm.tm_isdst = -1;
       t = mktime(&tm);

However, these two forms are not equivalent. This article explains why.
First, compare advancing time by adding to a time_t value with advancing time by calling mktime(). In Eastern Standard Time, the time_t value for 1/1/2010 @ 23:00:00 is 0x4b3ec4c0. In the figure below, time above the arrow advances by adding 3600 seconds at each interval to a time_t value. Time below the arrow uses mktime() to advance by 1 hour at each interval. In every case, when mktime() is called, tm.tm_isdst is -1. As the figure shows, in this case there is no difference between the two methods.

mktime_no_dst_transition

In the next figure, time is stepped across the start of DST which, in 2010, starts on 3/14 at 2AM. At the 3rd increment,
mktime() advances the clock from 1AM to 3AM. Also note what happens if 4 hours is added to the initial start time. The result is 0x4b9c8970, aka 3AM, which is the same as adding 3 hours to the start time. Unlike the previous example, there is not a one-to-one correspondence between adding seconds to a time_t time and adding hours to a calendar time.

mktime_spring_forward

Likewise, there is no one-to-one correspondence when time is stepped across the end of DST, 11/7/2010 at 2AM. Note that adding one hour to the first 1AM skips the second 1AM.

mktime_fall_back

It's possible to have
mktime() advance from the first 1AM to the second 1AM, but this requires that the tm.tm_isdst field be 1 before mktime() is invoked.

When is 24 hours from now? If you're interested in what a clock would show, it's always:

       tm.tm_hour += 24;
       tm.tm_isdst = -1;
       t = mktime(&tm);

11PM plus 24 hours, when
tm.tm_isdst is -1, always results in 11PM the next day, regardless of DST transitions. The following table uses 11PM as a baseline and shows the time reported by mktime() when adding 0 to 24 to tm.tm_hour. Fall DST values are in blue; spring DST values in green.

DST_Delta_Hours.Table

It has already been demonstrated that adding seconds to a
time_t time is not necessarily the same as adding hours to a struct tm time. What happens when seconds are added to a struct tm time? As the following table shows, 11PM plus 86,400 seconds could be 10PM the next day, 11PM the next day, or midnight the day after next, depending on DST. This table uses 11PM as a baseline and shows the time reported by mktime() when adding 0 to 86,400 to tm.tm_sec.

DST_Delta_Seconds_Table

Experimentation shows that Unix always treats adding seconds as a duration: that
tm.tm_sec +/- N is equivalent to time_t +/- N. But when tm.tm_isdst is -1, tm.tm_hour +/- N is not guaranteed to be the same as time_t +/- N * 60 * 60. What is true for tm.tm_hour also holds for tm.tm_min, tm.tm_mday, tm.tm_mon, and tm.tm_year. The apparent behavior, when tm.tm_isdst is -1, is that mktime() converts the normalized mm/dd/yyyy @ hh:mm to the equivalent time_t value, then adds the unnormalized tm.tm_sec.

The same results are obtained with Linux running Fedora release 10.

Examination of the code supports the hypothesis that
mktime() adds the unnormalized tm.tm_sec value. In hindsight, perhaps I should have looked at the code first.

The
Darwin source for localtime.c contains:

       /*
       **
First try without normalization of seconds
       ** (in case tm_sec contains a value associated with a leap second).
       ** If that fails, try with normalization of seconds.
       */
       t = time2sub(tmp, funcp, offset, okayp, FALSE);
       return *okayp ? t : time2sub(tmp, funcp, offset, okayp, TRUE);

mktime.c in the
GNU source for glibc, version 2.11.1, reads:

       if (LEAP_SECONDS_POSSIBLE && sec_requested != tm.tm_sec)
       {
          /*
Adjust time to reflect the tm_sec requested,
             not the normalized value.
             Also, repair any damage from a false match due
             to a leap second. */
          int sec_adjustment = (sec == 0 && tm.tm_sec == 60) - sec;
          
// note: sec_requested is the original value of tm.tm_sec
          t1 = t + sec_requested;
          t2 = t1 + sec_adjustment;
          if (((t1 < t) != (sec_requested < 0))
             | ((t2 < t1) != (sec_adjustment < 0))
             | ! convert (&t2, &tm))
          return -1;
          t = t2;
       }

However, running the test code on Windows XP gives different results. Windows normalizes
tm.tm_sec, bringing it into the range [0..59], when computing the result.

This may be because Windows is closer to the POSIX specification for
mktime() than Darwin or Linux. Both provide support for leap seconds, POSIX doesn't. Hence the two routines time2posix() and posix2time(). This comment is in the Darwin code:

       /*
       ** IEEE Std 1003.1-1988 (POSIX) legislates that 536457599
       ** shall correspond to "Wed Dec 31 23:59:59 UTC 1986", which
       ** is not the case if we are accounting for leap seconds.
       ** So, we provide the following conversion routines for use
       ** when exchanging timestamps with POSIX conforming systems.
       */

As my friend Lee commented, "Coder beware, and test, test, test."


blog comments powered by Disqus