I'm trying to understand how UNIX timelocal and mktime work. Supposedly they handle daylight savings time when you pass the proper value in the struct tm
tm_isdst
field.
I am testing a very specific moment in time. According to the timezone database for "America/New_York" the daylight savings shifted on Oct 30, 2005 at 01:00. Here's the output from zdump -v America/New_York
which you can confirm on your own system. I'm only showing a subset of the data around the year 2005 (scroll right to see gmtoff values):
America/New_York Sun Apr 3 06:59:59 2005 UT = Sun Apr 3 01:59:59 2005 EST isdst=0 gmtoff=-18000 America/New_York Sun Apr 3 07:00:00 2005 UT = Sun Apr 3 03:00:00 2005 EDT isdst=1 gmtoff=-14400 America/New_York Sun Oct 30 05:59:59 2005 UT = Sun Oct 30 01:59:59 2005 EDT isdst=1 gmtoff=-14400 America/New_York Sun Oct 30 06:00:00 2005 UT = Sun Oct 30 01:00:00 2005 EST isdst=0 gmtoff=-18000 America/New_York Sun Apr 2 06:59:59 2006 UT = Sun Apr 2 01:59:59 2006 EST isdst=0 gmtoff=-18000 America/New_York Sun Apr 2 07:00:00 2006 UT = Sun Apr 2 03:00:00 2006 EDT isdst=1 gmtoff=-14400
To test this transition I am setting up a struct tm
to contain 01:30 on that specific day. If I pass 0 for tm_isdst
it should give me a gmtoffset of -18000. If I pass 1 and enable daylight savings, then gmtoffset should be -14400.
Here's the code I'm using to test on both Darwin/OSX and FreeBSD:
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void print_tm(struct tm* tm) {
printf("tm: sec [%d] min [%d] hour [%d] mday [%d] mon [%d] year [%d] wday [%d] yday [%d] isdst [%d] zone [%s] gmtoff [%ld]\n",
tm->tm_sec,
tm->tm_min,
tm->tm_hour,
tm->tm_mday,
tm->tm_mon + 1,
tm->tm_year,
tm->tm_wday,
tm->tm_yday + 1,
tm->tm_isdst,
tm->tm_zone,
tm->tm_gmtoff);
}
struct tm* set_tm(int sec, int min, int hour, int mday, int mon, int year, int wday, int yday, int isdst, int gmtoff, char* zone) {
struct tm* tm;
tm = malloc(sizeof(struct tm));
memset(tm, 0, sizeof(struct tm));
tm->tm_sec = sec;
tm->tm_min = min;
tm->tm_hour = hour;
tm->tm_mday = mday;
tm->tm_mon = mon - 1;
tm->tm_year = year;
tm->tm_wday = wday;
tm->tm_yday = yday - 1;
tm->tm_isdst = isdst;
tm->tm_zone = zone;
tm->tm_gmtoff = gmtoff;
return tm;
}
void test_timelocal(struct tm* tm, int isdst) {
time_t seconds = -1;
if(!setenv("TZ", "America/New_York", 1)) {
printf("isdst is [%d]\n", isdst);
tm->tm_isdst = isdst;
tzset();
seconds = timelocal(tm);
localtime_r(&seconds, tm);
print_tm(tm);
} else {
printf("setenv failed with [%s]\n", strerror(errno));
}
printf("\n");
}
void test_mktime(struct tm* tm, int isdst) {
time_t seconds = -1;
if(!setenv("TZ", "America/New_York", 1)) {
printf("isdst is [%d]\n", isdst);
tm->tm_isdst = isdst;
tzset();
seconds = mktime(tm);
localtime_r(&seconds, tm);
print_tm(tm);
} else {
printf("setenv failed with [%s]\n", strerror(errno));
}
printf("\n");
}
int main(void) {
struct tm* tm;
printf("Test with timelocal\n");
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_timelocal(tm, 0);
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_timelocal(tm, 1);
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_timelocal(tm, -1);
printf("Test with mktime\n");
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_mktime(tm, 0);
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_mktime(tm, 1);
tm = set_tm(0, 30, 1, 30, 10, 2005, 0, 0, 0, 0, "");
test_mktime(tm, -1);
return 0;
}
Running this on various OSes gives different results. On FreeBSD this code outputs (scroll right to see gmtoffset values):
Test with timelocal isdst is [0] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] isdst is [1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] isdst is [-1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] Test with mktime isdst is [0] tm: sec [0] min [30] hour [2] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] isdst is [1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] isdst is [-1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400]
On darwin/OSX the exact same code produces this (scroll right to see gmtoffset values):
Test with timelocal isdst is [0] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000] isdst is [1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000] isdst is [-1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000] Test with mktime isdst is [0] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000] isdst is [1] tm: sec [0] min [30] hour [0] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000] isdst is [-1] tm: sec [0] min [30] hour [1] mday [30] mon [10] year [2005] wday [1] yday [303] isdst [0] zone [EST] gmtoff [-18000]
To my eye it looks like BOTH of them get it WRONG. The tm_isdst
field appears to have no effect on the tm_gmtoff
field. The tm_hour
output changes when using mktime
but the offset is still wrong.
If you change the tm_mday
to days earlier or days later the gmtoffset does not change at all which confuses me.
Am I doing something wrong or have I misinterpreted how these functions work?