-1

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?

  • 1
    DST changes at 02:00, not 01:00. – Barmar Feb 17 '17 at 23:16
  • 3
    Months in `struct tm` go from `0` to `11`, so October is `9`, not `10`. You're printing the results for November 30, not October 30. – Barmar Feb 17 '17 at 23:25
  • I solved it. The `tm->tm_year` field is based from 1900. So for 2005 I need to put (2005 - 1900) = 105 in that field. Then it works. As for @barmar's comment, the code adjusts for the 0-11 month counting already so that isn't the error. The `set_tm` function subtracts 1 from the month value. – Chuck Remes Feb 17 '17 at 23:42
  • That too. I just noticed that you were already adding/subtracting 1 for `tm_mon`. – Barmar Feb 17 '17 at 23:44
  • You should post that as an answer. You're allowed to answer your own questions. – Barmar Feb 17 '17 at 23:44
  • "Am I doing something wrong" --> Code should have check the return value of `mktime(tm); localtime_r()`. Certainly that would have narrowed the problem quickly. – chux - Reinstate Monica Feb 18 '17 at 00:33
  • I removed that error checking in the interest of space (though I used plenty). None of these calls were throwing errors, so it would not have narrowed the problem space at all. – Chuck Remes Feb 18 '17 at 03:04

1 Answers1

-2

UNIX time is very screwy. Turns out that my mistake has to do with the tm_year field in the struct tm. It is supposed to represent the number of years since 1900, so the value that goes in that field should be 105 and not 2005 (e.g. 2005 - 1900 = 105). This now yields the correct answer.

This definition of the struct can be found on this very useful page.

  • This was actually a very useful rubber duck session. I posted this question and then took the dogs for a walk. The answer came to me on the walk. :) – Chuck Remes Feb 17 '17 at 23:46
  • 1
    There's nothing "unix" about this. Unix time is integer seconds since the epoch in a unit that's called UTC but behaves more like UT1. What you're looking at, `struct tm` or "broken down time", is completely specified by ISO C and has nothing to do with unix. – R.. GitHub STOP HELPING ICE Feb 18 '17 at 01:13
  • That's a useless comment. – Chuck Remes Feb 18 '17 at 03:05
  • I don't see how so. It's a comment on the irrelevance/mispreresentation of the first sentence of your answer. – R.. GitHub STOP HELPING ICE Feb 18 '17 at 04:17
  • Sure, right. My pdp-11 used this same setup. Same for OS/390. My Amiga took the same approach. Ditto for MacOS prior to release 10. What's that? C ran on all of those platforms but didn't impose this screwiness? This stupid time setup is PRIMARILY from UNIX. This is an OS mistake and not a C language problem. – Chuck Remes Feb 18 '17 at 06:17
  • -1 for digging in on something you're completely wrong on. See *7.27.1 Components of time, ¶4*: "...int tm_year; // years since 1900..." – R.. GitHub STOP HELPING ICE Feb 18 '17 at 14:16
  • -2 for being a useless pedant. I figured out the 1900 thing already; see answer. – Chuck Remes Feb 18 '17 at 20:10
  • I'm sorry. My ADD is getting the better of me. I'll leave you alone now. – Chuck Remes Feb 18 '17 at 20:18