1

Take the following piece of code:

static void printTime(const struct tm* t, const time_t stamp){
    printf("%d-%d-%d, %d:%d:%d (DST %s) (stamp: %zu)\n",
            1900 + t->tm_year, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, t->tm_isdst ? "Active" : "Inactive", stamp);
}

int main(){
    time_t t = 1540633936;
    struct tm tStruct;
    localtime_r(&t, &tStruct);
    printTime(&tStruct, t);
    for (unsigned i = 0; i < 14; ++i){
        tStruct.tm_sec += 7200;
        //tStruct.tm_hour += 2;
        tStruct.tm_isdst = -1;
        t = mktime(&tStruct);
        localtime_r(&t, &tStruct);
        printTime(&tStruct, t);
    }
    return 0;
}

It shows two ways to increment the date. The documentation of mktime tells me:

The mktime() function modifies the fields of the tm structure as follows: tm_wday and tm_yday are set to values determined from the contents of the other fields; if structure members are outside their valid interval, they will be normalized (so that, for example, 40 October is changed into 9 November); tm_isdst is set (regardless of its initial value) to a positive value or to 0, respectively, to indicate whether DST is or is not in effect at the specified time.

Based on this I expect normalization to work in such a way that adding 7200 seconds is equivalent to adding two hours. But the output differs:

tStruct.tm_sec += 7200;

Gives:

2018-10-27, 11:52:16 (DST Active) (stamp: 1540633936)
2018-10-27, 13:52:16 (DST Active) (stamp: 1540641136)
2018-10-27, 15:52:16 (DST Active) (stamp: 1540648336)
2018-10-27, 17:52:16 (DST Active) (stamp: 1540655536)
2018-10-27, 19:52:16 (DST Active) (stamp: 1540662736)
2018-10-27, 21:52:16 (DST Active) (stamp: 1540669936)
2018-10-27, 23:52:16 (DST Active) (stamp: 1540677136)
2018-10-28, 1:52:16 (DST Active) (stamp: 1540684336)
2018-10-28, 2:52:16 (DST Inactive) (stamp: 1540691536)
2018-10-28, 3:52:16 (DST Inactive) (stamp: 1540695136)
2018-10-28, 5:52:16 (DST Inactive) (stamp: 1540702336)
2018-10-28, 7:52:16 (DST Inactive) (stamp: 1540709536)
2018-10-28, 9:52:16 (DST Inactive) (stamp: 1540716736)
2018-10-28, 11:52:16 (DST Inactive) (stamp: 1540723936)
2018-10-28, 13:52:16 (DST Inactive) (stamp: 1540731136)

(notice the wrong timejumps directly after DST change)

tStruct.tm_hour += 2;

Gives:

2018-10-27, 11:52:16 (DST Active) (stamp: 1540633936)
2018-10-27, 13:52:16 (DST Active) (stamp: 1540641136)
2018-10-27, 15:52:16 (DST Active) (stamp: 1540648336)
2018-10-27, 17:52:16 (DST Active) (stamp: 1540655536)
2018-10-27, 19:52:16 (DST Active) (stamp: 1540662736)
2018-10-27, 21:52:16 (DST Active) (stamp: 1540669936)
2018-10-27, 23:52:16 (DST Active) (stamp: 1540677136)
2018-10-28, 1:52:16 (DST Active) (stamp: 1540684336)
2018-10-28, 3:52:16 (DST Inactive) (stamp: 1540695136)
2018-10-28, 5:52:16 (DST Inactive) (stamp: 1540702336)
2018-10-28, 7:52:16 (DST Inactive) (stamp: 1540709536)
2018-10-28, 9:52:16 (DST Inactive) (stamp: 1540716736)
2018-10-28, 11:52:16 (DST Inactive) (stamp: 1540723936)
2018-10-28, 13:52:16 (DST Inactive) (stamp: 1540731136)
2018-10-28, 15:52:16 (DST Inactive) (stamp: 1540738336)

Which is the expected behaviour (to me, at least).

So, my question is: is there actually an error? Or is this documented behaviour, somewhere?

This behaviour also happens when tm_hour needs to be changed by mktime. Take the following example:

tStruct.tm_hour += 25;

Gives:

2018-10-27, 11:52:16 (DST Active) (stamp: 1540633936)
2018-10-28, 12:52:16 (DST Inactive) (stamp: 1540727536)
2018-10-29, 13:52:16 (DST Inactive) (stamp: 1540817536)
2018-10-30, 14:52:16 (DST Inactive) (stamp: 1540907536)
2018-10-31, 15:52:16 (DST Inactive) (stamp: 1540997536)
2018-11-1, 16:52:16 (DST Inactive) (stamp: 1541087536)
2018-11-2, 17:52:16 (DST Inactive) (stamp: 1541177536)
2018-11-3, 18:52:16 (DST Inactive) (stamp: 1541267536)
2018-11-4, 19:52:16 (DST Inactive) (stamp: 1541357536)
2018-11-5, 20:52:16 (DST Inactive) (stamp: 1541447536)
2018-11-6, 21:52:16 (DST Inactive) (stamp: 1541537536)
2018-11-7, 22:52:16 (DST Inactive) (stamp: 1541627536)
2018-11-8, 23:52:16 (DST Inactive) (stamp: 1541717536)
2018-11-10, 0:52:16 (DST Inactive) (stamp: 1541807536)
2018-11-11, 1:52:16 (DST Inactive) (stamp: 1541897536)
tStruct.tm_sec += 90000

Gives:

2018-10-27, 11:52:16 (DST Active) (stamp: 1540633936)
2018-10-28, 11:52:16 (DST Inactive) (stamp: 1540723936)
2018-10-29, 12:52:16 (DST Inactive) (stamp: 1540813936)
2018-10-30, 13:52:16 (DST Inactive) (stamp: 1540903936)
2018-10-31, 14:52:16 (DST Inactive) (stamp: 1540993936)
2018-11-1, 15:52:16 (DST Inactive) (stamp: 1541083936)
2018-11-2, 16:52:16 (DST Inactive) (stamp: 1541173936)
2018-11-3, 17:52:16 (DST Inactive) (stamp: 1541263936)
2018-11-4, 18:52:16 (DST Inactive) (stamp: 1541353936)
2018-11-5, 19:52:16 (DST Inactive) (stamp: 1541443936)
2018-11-6, 20:52:16 (DST Inactive) (stamp: 1541533936)
2018-11-7, 21:52:16 (DST Inactive) (stamp: 1541623936)
2018-11-8, 22:52:16 (DST Inactive) (stamp: 1541713936)
2018-11-9, 23:52:16 (DST Inactive) (stamp: 1541803936)
2018-11-11, 0:52:16 (DST Inactive) (stamp: 1541893936)
Cheiron
  • 3,620
  • 4
  • 32
  • 63
  • Note: `"%zu"` in `printf("(stamp: %zu)\n", stamp);` is not defined as the print specifier for `time_t` . There is none defined. A reasonable alternative is to cast to a wide type like `intmax_t` or maybe even wide FP: `printf("(stamp: %jd)\n", (intmax_t) stamp);` – chux - Reinstate Monica Sep 03 '18 at 15:19

1 Answers1

0

Depending on your exact time zone (and jurisdiction), at some time in the early morning on 2018-10-28, the clock is moved back 1 hour because DST ends. From your examples, it seems that happens at 3:00 in your timezone / jurisdiction.

In the first case (adding 7200 seconds to 2018-10-28, 1:52:16), the tm_sec value is outside of the normal range (0 - 59), so mktime can determine that you've added 2 hours, and since it knows that that's traversing the DST boundary, it adjusts the time accordingly. This results in 2018-10-28, 2:52:16, which is 2 hours after 2018-10-28, 1:52:16.

For the next increment in the first case (adding 7200 seconds to 2018-10-28, 2:52:16), the exact same thing happens again (since you're again traversing the DST boundary - you have reset tm_isdst to -1 after all). This results in 2018-10-28, 3:52:16, which is 2 hours after 2018-10-28, 2:52:16.

In the second case (adding 2 hours to 2018-10-28, 1:52:16), the tm_hour value is still inside of the normal range (0 - 23), so mktime cannot determine that you've added 2 hours, and it just treats it like a local time. This results in 2018-10-28, 3:52:16, which is 3 hours after 2018-10-28, 1:52:16.

To avoid issues like these :

  • do not reset tm_isdst to -1 unless needed (and you understand what will happen)
  • work as much as possible with UTC timestamps, and only convert to local time when displaying.
Sander De Dycker
  • 16,053
  • 1
  • 35
  • 40
  • No, that is not it. I changed the question to show this, but in short: adding 25 hours does not have the same behaviour as adding 9000 seconds, but in both cases it is clear that I changed the respective field. – Cheiron Sep 03 '18 at 11:49
  • @Cheiron : i you add 25 hours to 2018-10-28, 1:52:16, `mktime` can determine that you added hours, but it can't be sure that that addition traversed a DST boundary (you could have added 3 to 23 instead of 25 to 1), so it plays it safe, and does not adjust the time. – Sander De Dycker Sep 03 '18 at 11:57
  • So the behaviour is actually different, and adding hours will have nicer behaviour with respect to DST changes, right? – Cheiron Sep 03 '18 at 11:59
  • @Cheiron : it depends what you mean by "nicer". I would go for the recommendations at the end of my answer. – Sander De Dycker Sep 03 '18 at 12:02
  • But I do need to figure out how much time in seconds was spend to move, for example, one day forward in local time. Therefore, your recommendations will not help my case. My code is based on https://stackoverflow.com/questions/310363/how-to-add-one-day-to-a-time-obtained-from-time. – Cheiron Sep 03 '18 at 12:04
  • 1
    @Cheiron : your question asks to explain the behavior, which is what my answer addresses. Addressing your previous comment (which is really a different question) where you explain what you actually want to achieve : `mktime` is not designed to do that. It will do its best to try to guess what you intended. But when it comes to DST, there's too much ambiguity, and it won't always get it "right". On top of that, different implementations of `mktime` will guess differently. So you can't rely on the behavior for one platform to be the same on another. – Sander De Dycker Sep 03 '18 at 12:35