Учитывая местное время.
При тестировании некоторого временного кода с потенциальными сотнями часовых поясов некоторые из них приводят к вызову 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.'
@dbush Я обновил код, чтобы использовать имя, отличное от tzname
. Проблема остается.
Возможно, попробуйте запустить под GDB, прервать и посмотреть, где он находится, когда не возвращается. Я согласен с @dbush, что проблема с cygwin кажется вероятной. Можно предположить, что это что-то связанное с переводом строки в источнике данных tz.
Это не воспроизводится в Linux. Мне нужно было #define _POSIX_C_SOURCE 200112L
, чтобы он скомпилировался.
Ни одно из этих местных времен не существует, поэтому вам должно быть возвращено -1. Кажется, вы нашли ошибку в их реализации mktime. Я не удивлен.
@Schwern «Ни одного из этих местных времен не существует, поэтому вам должен быть возвращен -1». Для проблем с дневным временем весной, когда в сутках 23 часа. Ожидаете ли вы, что подобная отметка времени этого пропущенного часа также вернет -1? Звучит так, будто это противоречит «...исходные значения других компонентов не ограничиваются диапазонами, указанными выше».
Я могу воспроизвести это на Cygwin. Cygwin использует newlib
, исходники доступны по адресу git://sourceware.org/git/newlib-cygwin.git
. Вы можете сообщить об этом в списке рассылки Cygwin: cygwin.com/lists.html
@AllanWind «Мне нужно было #define _POSIX_C_SOURCE 200112L, чтобы он скомпилировался». --> Это для setenv()
? В противном случае я бы ожидал, что код соответствует стандарту C и должен скомпилироваться.
tzset()
и setenv()
@chux-ReinstateMonica «исходные значения других компонентов не ограничены указанными выше диапазонами» относится к таким вещам, как установка 25 часов. Это нормально. Проблема в том, что 1971/04/25 23:00:00 никогда не случалось в Африке/Алжире. Это разрыв в календаре. Вы просите mktime сообщить вам, когда в календаре произошло событие, которого никогда не было, и mktime потерял рассудок. Смотрите мой ответ.
Календари — это совокупность циклов (солнечных циклов, лунных циклов, произвольных циклов), которые мы пытаемся синхронизировать. Уже тяжело. Произвольные решения о часовых поясах и переходе на летнее время еще больше усложняют задачу. Не только потому, что они произвольны и могут быть решены с помощью базы данных, но и потому, что они вызывают разрывы в календаре. Вы можете написать совершенно правильное кажущееся календарное время и часовой пояс, которых никогда не было или которые происходили дважды. 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
.
Re: DST: с воскресеньем, 10 марта 2024 г., 2:00:00 и .tm_isdst = -1
, я никогда не сталкивался с реализацией возврата time_t -1
, но всегда с каким-то расширенным стандартным или летним временем. Кроме того, хорошо это или плохо, я столкнулся с кодом, который полагается на какой-то не -1
результат. ИМХО, возврат расширенного времени эры непосредственно перед, или сразу после, или -1 - все это совместимые реализации. Однако возврат не- -1
вызывает меньше всего удивления и на это рассчитывает большинство программистов. В идеале спецификация C должна указывать.
@chux-ReinstateMonica Когда я говорю о круговом обмене, меня не волнует нормализация таких событий, как 32 октября. Для целей mktime 32 октября — это просто странный способ написать 1 ноября.
@chux-ReinstateMonica Да, вокруг этих разрывов много двусмысленности. IMO 1994/09/04 02:01:02 Африка/Виндхук — это недопустимое календарное время, которое не имеет представления time_t и должно иметь результат -1, чтобы пользователь мог знать, что у него недопустимое календарное время. Другие реализации угадывают недокументированными (и, вероятно, неучтенными) способами или просто ломаются. «В идеале это должно быть указано в спецификации C». 🤣 Я бы спросил, новенький ли ты здесь, но знаю, что нет. 😏
@chux-ReinstateMonica «Тем не менее, возврат значения, отличного от -1, вызывает меньше всего удивления, и на это рассчитывает большинство программистов...» По моему опыту, программисты (даже авторы библиотек времени) вообще не учитывают разрывы календаря. Притвориться, что их не существует, значит, проблема не исчезнет. Дважды в год мы получаем всевозможные загадочные ошибки, потому что библиотеки календарей возвращают кажущиеся верными, но неправильные ответы. По крайней мере, при наличии ошибки вы знаете, почему код не работает.
В godbolt (и моей локальной виртуальной машине CentOS7) объявление
tzname
конфликтует с тем, что объявлено в time.h, в частностиextern char *tzname[2];
. Если это закомментировать, все работает нормально. Наверное, дело в цигвинах.