Stasik Stasik - 3 months ago 12
C Question

System call mktime ignores tm_isdst flag

another one question regarding mktime and DST

Linux, Ubuntu, time zone is set to Europe/Berlin i.e. current time is CEST:

>date
Mon Aug 22 16:08:10 CEST 2016
>date --utc
Mon Aug 22 14:08:14 UTC 2016


everything okay so far.

Now I try to run the following code:

#include <stdio.h>
#include <time.h>
int main()
{

struct tm tm = {0};
int secs;

tm.tm_sec = 0;
tm.tm_min = 0;
tm.tm_hour = 12;
tm.tm_mon = 9 - 1;
tm.tm_mday = 30;
tm.tm_year = 2016 - 1900;

tm.tm_isdst = 0;
secs = mktime(&tm);
printf("%i\n", secs);

tm.tm_isdst = 1;
secs = mktime(&tm);
printf("%i\n", secs);

tm.tm_isdst = -1;
secs = mktime(&tm);
printf("%i\n", secs);

return 0;
}


and get

1475233200
1475233200
1475233200


which is in all three cases wrong (1 hour offset):

>date -d @1475233200
Fri Sep 30 13:00:00 CEST 2016


So I am a bit puzzled now, is my timezone somehow broken? Why is tm_isdst flag ignored completely?

Edit: @Nominal Animal had the answer: mktime modifies tm_hour! I wonder where it is documented?!

#include <stdio.h>
#include <time.h>

void reset(struct tm* tm){
(*tm) = (const struct tm){0};

tm->tm_sec = 0;
tm->tm_min = 0;
tm->tm_hour = 12;
tm->tm_mon = 9 - 1;
tm->tm_mday = 30;
tm->tm_year = 2016 - 1900;
}

int main()
{

struct tm tm;
int secs;

reset(&tm);
tm.tm_isdst = 0;
secs = mktime(&tm);
printf("%i\n", secs);

reset(&tm);
tm.tm_isdst = 1;
secs = mktime(&tm);
printf("%i\n", secs);

reset(&tm);
tm.tm_isdst = -1;
secs = mktime(&tm);
printf("%i\n", secs);

return 0;
}


gives

1475233200
1475229600
1475229600

Answer

I think I can now see how one would find this confusing. Think of mktime() as having signature

time_t mktime_actual(struct tm *dst, const struct tm *src);

where the time_t result is calculated based on (normalized) *src, and the normalized fields and whether daylight savings time applies at that time, is saved to *dst.

It is just that the C language developers historically chose to use only one pointer, combining both src and dst. The above logic still stands, though.

See the `man mktime man page, especially this part:

The mktime() function converts a broken-down time structure, expressed as local time, to calendar time representation. The function ignores the values supplied by the caller in the tm_wday and tm_yday fields. The value specified in the tm_isdst field informs mktime() whether or not daylight saving time (DST) is in effect for the time supplied in the tm structure: a positive value means DST is in effect; zero means that DST is not in effect; and a negative value means that mktime() should (use timezone information and system databases to) attempt to determine whether DST is in effect at the specified time.

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. Calling mktime() also sets the external variable tzname with information about the current timezone.

If the specified broken-down time cannot be represented as calendar time (seconds since the Epoch), mktime() returns (time_t) -1 and does not alter the members of the broken-down time structure.

In other words, if you change your test program a bit, say into

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

static const char *dst(const int flag)
{
    if (flag > 0)
        return "(>0: is DST)";
    else
    if (flag < 0)
        return "(<0: Unknown if DST)";
    else
        return "(=0: not DST)";
}

static struct tm newtm(const int year, const int month, const int day,
                       const int hour, const int min, const int sec,
                       const int isdst)
{
    struct tm t = { .tm_year  = year - 1900,
                    .tm_mon   = month - 1,
                    .tm_mday  = day,
                    .tm_hour  = hour,
                    .tm_min   = min,
                    .tm_sec   = sec,
                    .tm_isdst = isdst };
    return t;
}

int main(void)
{
    struct tm   tm = {0};
    time_t secs;

    tm = newtm(2016,9,30, 12,0,0, -1);
    secs = mktime(&tm);
    printf("-1: %04d-%02d-%02d %02d:%02d:%02d %s %lld\n",
           tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
           tm.tm_hour, tm.tm_min, tm.tm_sec, dst(tm.tm_isdst), (long long)secs);

    tm = newtm(2016,9,30, 12,0,0, 0);
    secs = mktime(&tm);
    printf(" 0: %04d-%02d-%02d %02d:%02d:%02d %s %lld\n",
           tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
           tm.tm_hour, tm.tm_min, tm.tm_sec, dst(tm.tm_isdst), (long long)secs);

    tm = newtm(2016,9,30, 12,0,0, 1);
    secs = mktime(&tm);
    printf("+1: %04d-%02d-%02d %02d:%02d:%02d %s %lld\n",
           tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,
           tm.tm_hour, tm.tm_min, tm.tm_sec, dst(tm.tm_isdst), (long long)secs);

    return EXIT_SUCCESS;
}

then running it produces output

-1: 2016-09-30 12:00:00 (>0: is DST) 1475226000
 0: 2016-09-30 13:00:00 (>0: is DST) 1475229600
+1: 2016-09-30 12:00:00 (>0: is DST) 1475226000

In other words, it behaves exactly as described (in the quote above). This behaviour is documented in C89, C99, and POSIX.1 (I think C11 also, but haven't checked).