Cheiron
Cheiron

Reputation: 3736

localtime, mktime: second normalization and DST behaviour

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)

Upvotes: 1

Views: 345

Answers (1)

Sander De Dycker
Sander De Dycker

Reputation: 16243

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.

Upvotes: 0

Related Questions