Вызов mktime() никогда не возвращается. Ошибка библиотеки?

Учитывая местное время.

При тестировании некоторого временного кода с потенциальными сотнями часовых поясов некоторые из них приводят к вызову mktime(), который никогда не возвращается для выбранных значений!

Подробно: код никогда не возвращается из time_t t = mktime(&tm) из print_time(). Процессор продолжает вращаться.

Это все tm в том случае, если часовой пояс и час сдвигаются, а не из-за перехода на летнее время.

Все три случая ниже относятся к часовым поясам Африки. Я подозреваю, что их больше, но этих трех должно быть достаточно, чтобы продемонстрировать ошибку, которая может существовать во многих часовых поясах.

Я ожидаю, что что-то будет быстро возвращено.

Вы видите такое же поведение без возврата?

Как лучше всего сообщить об этой ошибке?


Интересно, использует ли тест 1 или 0 для возврата .tm_isdst, mktime().


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

const char *tzname2 = "x";

void timezone_set(const char *tz) {
  if (setenv("TZ", tz, 1 /* overwrite */)) {
    fprintf(stderr, "Unable to set timezone '%s'\n", tz);
    exit(EXIT_FAILURE);
  }
  tzset();
  tzname2 = tz;
  puts("");
  puts(tz);
}

void print_time(struct tm tm, const char *s) {
  printf("%10s %4d/%02d/%02d %2d:%02d:%02d dst:%2d", s,
      tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
      tm.tm_sec, tm.tm_isdst);
  fflush(stdout);
  time_t t = mktime(&tm);  // Does not always return.
  printf("--> %10lld %4d/%02d/%02d %2d:%02d:%02d dst:%2d\n", (long long) t,
      tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
      tm.tm_sec, tm.tm_isdst);
  fflush(stdout);
}

void tz_test(int year, int month, int mday, int hour, int min, int sec, int dst) {
  for (int h = 0; h < 5; h++) {
    struct tm tm = {.tm_year = year - 1900, .tm_mon = month - 1, .tm_mday = mday,
        .tm_hour = hour, .tm_min = min, .tm_sec = sec, .tm_isdst = dst};
    tm.tm_hour = hour + h;
    print_time(tm, "Local");
  }
  puts("");
  fflush(stdout);
}

int main(void) {
  // https://timezonedb.com/time-zones/Africa/Algiers
  timezone_set("Africa/Algiers");
  tz_test(1971, 4, 25, 23, 0, 0, -1);
  // https://timezonedb.com/time-zones/Africa/Tripoli
  timezone_set("Africa/Tripoli");
  tz_test(1982, 4, 1, 0, 0, 0, -1);
  // https://timezonedb.com/time-zones/Africa/Windhoek
  timezone_set("Africa/Windhoek");
  tz_test(1994, 9, 4, 2, 0, 0, -1);
  return 0;
}

Пример вывода:

Africa/Algiers
     Local 1971/04/25 23:00:00 dst:-1

Примечания:

/usr/lib/gcc/x86_64-pc-cygwin/12/include
 /usr/include
 /usr/lib/gcc/x86_64-pc-cygwin/12/../../../../lib/../include/w32api
End of search list.
GNU C17 (GCC) version 12.4.0 (x86_64-pc-cygwin)
    compiled by GNU C version 12.4.0, GMP version 6.3.0, MPFR version 4.2.1, MPC version 1.3.1, isl version isl-0.26-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 2db83001ac4ad148ae13aae27a04c021
COLLECT_GCC_OPTIONS='-O0' '-g3' '-Wpedantic' '-Wall' '-Wextra' '-Wconversion' '-Wsign-conversion' '-c' '-std=c17' '-fmessage-length=0' '-Wformat=1' '-Wformat-security' '-Wformat=2' '-Wmaybe-uninitialized' '-Werror=stringop-truncation' '-v' '-MMD' '-MP' '-MF' 'mystrtod.d' '-MT' 'mystrtod.o' '-o' 'mystrtod.o' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/bin/as.exe -v --gdwarf-5 -o mystrtod.o /cygdrive/c/Users/TPC/AppData/Local/Temp/cccEEI89.s
GNU assembler version 2.43 (x86_64-pc-cygwin) using BFD version (GNU Binutils) 2.43
COMPILER_PATH=/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/:/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/bin/
LIBRARY_PATH=/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/lib/../lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../lib/:/lib/../lib/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-O0' '-g3' '-Wpedantic' '-Wall' '-Wextra' '-Wconversion' '-Wsign-conversion' '-c' '-std=c17' '-fmessage-length=0' '-Wformat=1' '-Wformat-security' '-Wformat=2' '-Wmaybe-uninitialized' '-Werror=stringop-truncation' '-v' '-MMD' '-MP' '-MF' 'mystrtod.d' '-MT' 'mystrtod.o' '-o' 'mystrtod.o' '-mtune=generic' '-march=x86-64' '-dumpdir' 'mystrtod.'

В godbolt (и моей локальной виртуальной машине CentOS7) объявление tzname конфликтует с тем, что объявлено в time.h, в частности extern char *tzname[2];. Если это закомментировать, все работает нормально. Наверное, дело в цигвинах.

dbush 02.09.2024 04:01

@dbush Я обновил код, чтобы использовать имя, отличное от tzname. Проблема остается.

chux - Reinstate Monica 02.09.2024 04:15

Возможно, попробуйте запустить под GDB, прервать и посмотреть, где он находится, когда не возвращается. Я согласен с @dbush, что проблема с cygwin кажется вероятной. Можно предположить, что это что-то связанное с переводом строки в источнике данных tz.

Gene 02.09.2024 04:28

Это не воспроизводится в Linux. Мне нужно было #define _POSIX_C_SOURCE 200112L, чтобы он скомпилировался.

Allan Wind 02.09.2024 04:35

Ни одно из этих местных времен не существует, поэтому вам должно быть возвращено -1. Кажется, вы нашли ошибку в их реализации mktime. Я не удивлен.

Schwern 02.09.2024 05:51

@Schwern «Ни одного из этих местных времен не существует, поэтому вам должен быть возвращен -1». Для проблем с дневным временем весной, когда в сутках 23 часа. Ожидаете ли вы, что подобная отметка времени этого пропущенного часа также вернет -1? Звучит так, будто это противоречит «...исходные значения других компонентов не ограничиваются диапазонами, указанными выше».

chux - Reinstate Monica 02.09.2024 07:11

Я могу воспроизвести это на Cygwin. Cygwin использует newlib, исходники доступны по адресу git://sourceware.org/git/newlib-cygwin.git. Вы можете сообщить об этом в списке рассылки Cygwin: cygwin.com/lists.html

Keith Thompson 02.09.2024 09:59

@AllanWind «Мне нужно было #define _POSIX_C_SOURCE 200112L, чтобы он скомпилировался». --> Это для setenv()? В противном случае я бы ожидал, что код соответствует стандарту C и должен скомпилироваться.

chux - Reinstate Monica 02.09.2024 15:07
tzset() и setenv()
Allan Wind 02.09.2024 18:31

@chux-ReinstateMonica «исходные значения других компонентов не ограничены указанными выше диапазонами» относится к таким вещам, как установка 25 часов. Это нормально. Проблема в том, что 1971/04/25 23:00:00 никогда не случалось в Африке/Алжире. Это разрыв в календаре. Вы просите mktime сообщить вам, когда в календаре произошло событие, которого никогда не было, и mktime потерял рассудок. Смотрите мой ответ.

Schwern 02.09.2024 21:37
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
10
191
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Поздравляем, вы нашли ошибку с переходом на летнее время!

Календари — это совокупность циклов (солнечных циклов, лунных циклов, произвольных циклов), которые мы пытаемся синхронизировать. Уже тяжело. Произвольные решения о часовых поясах и переходе на летнее время еще больше усложняют задачу. Не только потому, что они произвольны и могут быть решены с помощью базы данных, но и потому, что они вызывают разрывы в календаре. Вы можете написать совершенно правильное кажущееся календарное время и часовой пояс, которых никогда не было или которые происходили дважды. time.h не всегда работает с ними правильно или вообще.

Это происходит два раза в год в местах, где действует летнее время. На большей части территории США было воскресенье, 10 марта 2024 года, 1:59:59, а следующей секундой позже было воскресенье, 10 марта 2024 года, 3:00:00. Не было воскресенья, 10 марта 2024 года, 2:00:00. Нет Воскресенье, 10 марта 2024 г., 2:00:01.

Аналогично, когда часы достигнут воскресенья, 3 ноября 2024 года, 1:59:59, следующей секундой будет воскресенье, 3 ноября 2024 года, 1:00:00. Затем, когда они снова достигнут воскресенья, 3 ноября 2024 г., 1:59:59, следующей секундой будет воскресенье, 3 ноября 2024 г., 2:00:00. Дважды это будет воскресенье, 3 ноября 2024 года, 1:00:00. И в воскресенье, 3 ноября 2024 г., в 1:00:01 дважды. И в воскресенье, 3 ноября 2024 г., 1:59:59 дважды.

mktime и localtime должны идти туда и обратно; вы сможете поместить time_t, возвращенный mktime, обратно в localtime и получить обратно тот же оригинал tm. Но эти разрывы все портят. Это может привести к творческим значениям, сбоям или бесконечным циклам.

Я их нашла очень много.


Вы просите mktime преобразовать несуществующее календарное время. Я не имею в виду, что вы просите 25 часов, это другое. Я имею в виду, что перевод часов вперед на час означает, что существуют комбинации часовых поясов, дат и времени, которые не соответствуют точке в time_t. Перемещение часов назад означает, что одна комбинация часового пояса, даты и времени может соответствовать двум моментам времени. А time_t — это ровное количество секунд с полуночи 1 января 1970 года по всемирному координированному времени.

Например, если мы посмотрим на Африку/Алжир...

После воскресенья, 25 апреля 1971 г., 22:59:59:

Часы были переведены вперед: понедельник, 26 апреля 1971 г., 00:00:00.

Это означает, что в Африке/Алжире 25 апреля 1971 г. с 23:00 до 23:59:59 ничего не произошло. Не было ни 25.04.1971, 23:00, ни 25.04.1971, 23:01, ни 25.04.1971, 23:59. Не существует time_t, который можно было бы вставить в localtime, чтобы правильно получить tm от 25.04.1971, 23:00 в Африке/Алжире.

Аналогично для Африка/Триполи

После среды, 31 марта 1982 г., 23:59:59:

Часы были переведены вперед и стали четвергом, 1 апреля 1982 г., 01:00:00.

1982/04/01 0:00:00 не произошло в Африке/Триполи.

И Африка/Виндхук.

После воскресенья, 4 сентября 1994 г., 01:59:59:

Часы были переведены вперед: воскресенье, 4 сентября 1994 г., 03:00:00.

1994/09/04 02:00:00 не произошло в Африке/Виндхуке.


Стандарт C 7.27.2.3.3 говорит...

Функция mktime возвращает указанное календарное время, закодированное как значение типа time_t. Если календарное время невозможно представить, функция возвращает значение (time_t)(-1).

Когда вы запрашиваете mktime календарное время, которого не было, после прыжка вы должны получить обратно -1 или его эквивалент. Например, в MacOS...

// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
     Local 1971/04/25 23:00:00 dst:-1-->   41468400 1971/04/26  0:00:00 dst: 1
     Local 1971/04/25 24:00:00 dst:-1-->   41468400 1971/04/26  0:00:00 dst: 1
     Local 1971/04/25 25:00:00 dst:-1-->   41472000 1971/04/26  1:00:00 dst: 1
     Local 1971/04/25 26:00:00 dst:-1-->   41475600 1971/04/26  2:00:00 dst: 1
     Local 1971/04/25 27:00:00 dst:-1-->   41479200 1971/04/26  3:00:00 dst: 1

Он интерпретировал несуществующее 1971/04/25 23:00:00 как существующее 1971/04/26 0:00:00. Предположительно по той логике, что 41468399 — это 1971/04/25 22:59:59, а 1971/04/25 23:00:00 — на одну секунду позже, поэтому следует использовать localtime(41468400), что соответствует 1971/04/26 0:00:00. .

Однако конкретный mktime касается этих разрывов, он точно не должен зависать. Если он зависает, это указывает на ошибку в библиотеке. mktime может циклически проходить через все более точные предположения, и из-за разрыва он попадает в бесконечный цикл.


Кстати, вот что возвращает MacOS.

// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
     Local 1971/04/25 23:00:00 dst:-1-->   41468400 1971/04/26  0:00:00 dst: 1
     Local 1971/04/25 24:00:00 dst:-1-->   41468400 1971/04/26  0:00:00 dst: 1
     Local 1971/04/25 25:00:00 dst:-1-->   41472000 1971/04/26  1:00:00 dst: 1
     Local 1971/04/25 26:00:00 dst:-1-->   41475600 1971/04/26  2:00:00 dst: 1
     Local 1971/04/25 27:00:00 dst:-1-->   41479200 1971/04/26  3:00:00 dst: 1

// After Wednesday, 31 March, 1982 11:59:59 PM
// Clocks were moved forward to become Thursday, 01 April, 1982 01:00:00 AM
Africa/Tripoli
     Local 1982/04/01  0:00:00 dst:-1-->  386463600 1982/04/01  1:00:00 dst: 1
     Local 1982/04/01  1:00:00 dst:-1-->  386463600 1982/04/01  1:00:00 dst: 1
     Local 1982/04/01  2:00:00 dst:-1-->  386467200 1982/04/01  2:00:00 dst: 1
     Local 1982/04/01  3:00:00 dst:-1-->  386470800 1982/04/01  3:00:00 dst: 1
     Local 1982/04/01  4:00:00 dst:-1-->  386474400 1982/04/01  4:00:00 dst: 1

// After Sunday, 04 September, 1994 01:59:59 AM
// Clocks were moved forward to become Sunday, 04 September, 1994 03:00:00 AM
Africa/Windhoek
     Local 1994/09/04  2:00:00 dst:-1-->  778640400 1994/09/04  3:00:00 dst: 1
     Local 1994/09/04  3:00:00 dst:-1-->  778640400 1994/09/04  3:00:00 dst: 1
     Local 1994/09/04  4:00:00 dst:-1-->  778644000 1994/09/04  4:00:00 dst: 1
     Local 1994/09/04  5:00:00 dst:-1-->  778647600 1994/09/04  5:00:00 dst: 1
     Local 1994/09/04  6:00:00 dst:-1-->  778651200 1994/09/04  6:00:00 dst: 1

Это действительно вызывает странную ситуацию, когда два разных календарных времени сопоставляются с одним и тем же time_t. Это должно происходить только тогда, когда часы идут назад, а не вперед.

Может быть, это потому, что вы находитесь прямо на границе? Что, если мы спросим 1971/04/25 23:01:02? Очевидно, время, которого не существует...

// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
     Local 1971/04/25 23:01:02 dst:-1-->   41468462 1971/04/26  0:01:02 dst: 1
     Local 1971/04/25 24:01:02 dst:-1-->   41468462 1971/04/26  0:01:02 dst: 1
     Local 1971/04/25 25:01:02 dst:-1-->   41472062 1971/04/26  1:01:02 dst: 1
     Local 1971/04/25 26:01:02 dst:-1-->   41475662 1971/04/26  2:01:02 dst: 1
     Local 1971/04/25 27:01:02 dst:-1-->   41479262 1971/04/26  3:01:02 dst: 1

// After Wednesday, 31 March, 1982 11:59:59 PM
// Clocks were moved forward to become Thursday, 01 April, 1982 01:00:00 AM
Africa/Tripoli
     Local 1982/04/01  0:01:02 dst:-1-->  386463662 1982/04/01  1:01:02 dst: 1
     Local 1982/04/01  1:01:02 dst:-1-->  386463662 1982/04/01  1:01:02 dst: 1
     Local 1982/04/01  2:01:02 dst:-1-->  386467262 1982/04/01  2:01:02 dst: 1
     Local 1982/04/01  3:01:02 dst:-1-->  386470862 1982/04/01  3:01:02 dst: 1
     Local 1982/04/01  4:01:02 dst:-1-->  386474462 1982/04/01  4:01:02 dst: 1

// After Sunday, 04 September, 1994 01:59:59 AM
// Clocks were moved forward to become Sunday, 04 September, 1994 03:00:00 AM
Africa/Windhoek
     Local 1994/09/04  2:01:02 dst:-1-->  778640462 1994/09/04  3:01:02 dst: 1
     Local 1994/09/04  3:01:02 dst:-1-->  778640462 1994/09/04  3:01:02 dst: 1
     Local 1994/09/04  4:01:02 dst:-1-->  778644062 1994/09/04  4:01:02 dst: 1
     Local 1994/09/04  5:01:02 dst:-1-->  778647662 1994/09/04  5:01:02 dst: 1
     Local 1994/09/04  6:01:02 dst:-1-->  778651262 1994/09/04  6:01:02 dst: 1

То же самое, теперь еще более сомнительное, но лучше, чем повешение.

Спасибо за подробный ответ. «mktime и localtime должны быть двусторонними» -> mktime() указывает «... исходные значения других компонентов не ограничены диапазонами, указанными выше.», поэтому я ожидаю двустороннего time_t --> local struct tm --> time_t, но не обязательно туда и обратно местный struct tm --> time_t --> местный struct tm.

chux - Reinstate Monica 03.09.2024 00:17

Re: DST: с воскресеньем, 10 марта 2024 г., 2:00:00 и .tm_isdst = -1, я никогда не сталкивался с реализацией возврата time_t -1, но всегда с каким-то расширенным стандартным или летним временем. Кроме того, хорошо это или плохо, я столкнулся с кодом, который полагается на какой-то не -1 результат. ИМХО, возврат расширенного времени эры непосредственно перед, или сразу после, или -1 - все это совместимые реализации. Однако возврат не- -1 вызывает меньше всего удивления и на это рассчитывает большинство программистов. В идеале спецификация C должна указывать.

chux - Reinstate Monica 03.09.2024 00:26

@chux-ReinstateMonica Когда я говорю о круговом обмене, меня не волнует нормализация таких событий, как 32 октября. Для целей mktime 32 октября — это просто странный способ написать 1 ноября.

Schwern 03.09.2024 00:27

@chux-ReinstateMonica Да, вокруг этих разрывов много двусмысленности. IMO 1994/09/04 02:01:02 Африка/Виндхук — это недопустимое календарное время, которое не имеет представления time_t и должно иметь результат -1, чтобы пользователь мог знать, что у него недопустимое календарное время. Другие реализации угадывают недокументированными (и, вероятно, неучтенными) способами или просто ломаются. «В идеале это должно быть указано в спецификации C». 🤣 Я бы спросил, новенький ли ты здесь, но знаю, что нет. 😏

Schwern 03.09.2024 00:35

@chux-ReinstateMonica «Тем не менее, возврат значения, отличного от -1, вызывает меньше всего удивления, и на это рассчитывает большинство программистов...» По моему опыту, программисты (даже авторы библиотек времени) вообще не учитывают разрывы календаря. Притвориться, что их не существует, значит, проблема не исчезнет. Дважды в год мы получаем всевозможные загадочные ошибки, потому что библиотеки календарей возвращают кажущиеся верными, но неправильные ответы. По крайней мере, при наличии ошибки вы знаете, почему код не работает.

Schwern 03.09.2024 00:49

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