Почему timelocal и mktime не справляются с переходом на летнее время правильно?

Я пытаюсь понять, как работают UNIX timelocal и mktime. Предположительно они обрабатывают летнее время, когда вы передаете правильное значение в struct tmtm_isdst поле.

Я тестирую очень специфический момент времени. Согласно базе данных часовых поясов для "America/New_York", летнее время сместилось 30 октября 2005 года в 01:00. Вот вывод из zdump -v America/New_York который вы можете подтвердить в своей системе. Я показываю только подмножество данных около 2005 года (прокрутите вправо, чтобы увидеть значения gmtoff):

Америка /New_York  Sun 3 апреля 06:59:59 2005 UT = вс 3 апреля 01:59:59 2005 EST isdst=0 gmtoff=-18000
Америка / Нью-Йорк, воскресенье, 3 апреля 07:00:00 2005 UT = вс, 3 апреля, 03:00:00 EDT isdst=1 gmtoff=-14400
Америка / Нью-Йорк вс 30 октября 05:59:59 2005 UT = вс 30 октября 01:59:59 2005 EDT isdst=1 gmtoff=-14400
Америка / Нью-Йорк вс 30 октября 06:00:00 2005 UT = вс 30 октября 01:00:00 2005 EST isdst=0 gmtoff=-18000
Америка /New_York  Sun 2 апреля 06:59:59 2006 UT = вс 2 апреля 01:59:59 2006 EST isdst=0 gmtoff=-18000
Америка /New_York  Sun 2 апреля 07:00:00 2006 UT = вс 2 апреля 03:00:00 2006 EDT isdst=1 gmtoff=-14400

Чтобы проверить этот переход, я настраиваю struct tm содержать 01:30 в этот конкретный день. Если я передам 0 для tm_isdst это должно дать мне gmtoffset -18000. Если я пройду 1 и включу летнее время, то значение gmtoffset должно быть -14400.

Вот код, который я использую для тестирования на Darwin/OSX и 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;
}

Запуск этого в разных ОС дает разные результаты. На FreeBSD этот код выводит (прокрутите вправо, чтобы увидеть значения gmtoffset):

Тест с временной шкалой isdst составляет [0] тм: сек [0] мин [30] час [1] мдень [30] пн [10] год [2005] день недели [1] yday [303] isdst [1] зона [EDT] gmtoff [-14400]

isdst равен [1] тм: сек [0] мин [30] час [1] мдень [30] пн [10] год [2005] день недели [1] день [303] isdst [1] зона [ EDT] gmtoff [-14400] isdst равно [-1]
tm: сек [0] мин [30] час [1] мдень [30] пн [10] год [2005] день недели [1] yday [303] isdst [1] zone [EDT] gmtoff [-14400] Тест с mktime
isdst равен [0] tm: sec [0] min [30] hour [2] mday [30] mon [10] year [2005] wday [1] yday [ 303] isdst [1] зона [EDT] gmtoff [-14400]

isdst составляет [1] тм: сек [0] мин [30] час [1] мдень [30] пн [10] год [2005] день недели [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] день [303] isdst [1] зона [EDT] gmtoff [-14400]

В darwin/OSX точно такой же код производит это (прокрутите вправо, чтобы увидеть значения gmtoffset):

Тест с timelocal
isdst is [0]
tm: sec [0] min [30] час [1] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

isdst is [1]
tm: sec [0] min [30] час [1] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

isdst is [-1]
tm: sec [0] min [30] час [1] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

Тест с mktime
isdst is [0]
tm: sec [0] min [30] час [1] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

isdst is [1]
tm: sec [0] min [30] час [0] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

isdst is [-1]
tm: sec [0] min [30] час [1] mday [30] mon [10] год [2005] wday [1] yday [303] isdst [0] зона [EST] gmtoff [-18000]

На мой взгляд, похоже, что ОБА из них ошибаются. tm_isdst поле, кажется, не влияет на tm_gmtoff поле. tm_hour изменения выходного сигнала при использовании mktime но смещение все равно неверно.

Если вы измените tm_mday днями раньше или днями gmtoffset вообще не меняется, что меня смущает.

Я делаю что-то не так или я неправильно понял, как работают эти функции?

1 ответ

Время UNIX очень крутое. Оказывается, моя ошибка связана с tm_year поле в struct tm, Предполагается, что он представляет количество лет с 1900 года, поэтому значение в этом поле должно быть 105, а не 2005 (например, 2005 - 1900 = 105). Это теперь дает правильный ответ.

Это определение структуры можно найти на этой очень полезной странице.

Другие вопросы по тегам