LINUX.ORG.RU

Local time to UTC и обратно (отчет о решении)


0

1

Здравствуйте. Давно свою задачу решил (начало тут: http://www.linux.org.ru/forum/development/5150307 ), но всё руки не доходили описать конкретно, что было нужно и как удалось достичь результата.

Итак, задача ставилась таким образом: перевести произвольно взятую дату/время из UTC в локальное время системы и обратно. Казалось бы задача совсем тривиальная, однако есть некоторые нюансы.

Судя по всему, исторически так сложилось, что структура данных time_t автоматически предполагает, что дата/время в этой структуре представлено только и исключительно в UTC, хотя ничего и не мешает эти секунды отсчитывать от 1 января 1970 года в любом часовом поясе. Для представления же локального времени используются другие структуры, но не time_t. Таким образом, функции преобразования времени из локального в UTC и обратно требуют разных типов данных для входных и выходных параметров.

В моём же случае дата/время были представлены в структуре, которая использовалась как для хранения времени в формате UTC, так и для локального времени, и мне была необходима функция, принимающая и отдающая дату в одном и том же формате, но осуществляющая «сдвиг» времени в нужном направлении и на нужное количество часов. Задача, таким образом, свелась к необходимости конвертации формата даты во всех возможных направлениях, и вызова нужных функций преобразования, однако количество подводных камней впечатляет. Здесь приведу то, что получилось в сухом остатке.

Для фактического преобразования из UTC в локальное время использовалась функция struct tm* localtime_r( const time_t* timer, struct tm* result ); (просто localtime не может быть использована в многопоточных приложениях, поскольку использует свой внутренний буфер). Грабля, на которую я наступил, была связана с тем, что эта функция повреждает области памяти, расположенные рядом с переданными ей структурами, пришлось вставлять PAD-ы. Вторая грабля - возвращаемый функцией в структуре tm номер года начинается с 1900 (т.е. 0 в результате=1900год, 110 в результате=2010год), а месяц - с нуля (0=январь). Неочевидно, и при чтении манов на глаза не попадалось.

Для обратного преобразования из локального времени в UTC использовалась функция time_t mktime ( struct tm * timeptr ); Здесь тоже без граблей не обошлось: в структуре tm есть поле is_dst, указывающее, включено ли летнее время. Оно должно быть установлено _до_ вызова mktime, однако его значение для произвольной даты заранее неизвестно. К счастью, это поле функция выставляет сама в процессе работы, но если переданное значение изначально было неверным, то и результат тоже получается неверным. Таким образом, для правильного преобразования функцию необходимо вызвать дважды - первый раз она выставит в правильное значение флаг is_dst, но, возможно вернет неверный результат, а второй вызов уже с правильным значением dst возвращает правильный результат. Между вызовами все поля структуры tm (кроме is_dst) нужно инициализировать заново, т.к. функция их модифицирует, приводя время к правильному (по её мнению) значению в зависимости от флага is_dst.

>Судя по всему, исторически так сложилось, что структура данных time_t автоматически предполагает, что дата/время в этой структуре представлено только и исключительно в UTC

time_t — число секунд с полуночи первого января 1970 года. К этому числу неприменимо понятие «часовой пояс».

эта функция повреждает области памяти, расположенные рядом с переданными ей структурами

Дай угадаю, в коде у тебя написано struct tm *ltime и функцию ты зовешь как localtime_r(&my_time, ltime)? Как насчет подучить C для начала?

Вторая грабля - возвращаемый функцией в структуре tm номер года начинается с 1900 (т.е. 0 в результате=1900год, 110 в результате=2010год), а месяц - с нуля (0=январь). Неочевидно, и при чтении манов на глаза не попадалось.

Скажи честно, ты маны-то вообще пробовал читать? man localtime:

tm_year The number of years since 1900.

Ну и наконец: man tzset. После него тебе будет доступно и смещение временеи локального часового пояса и флаг dst. Но чукча ведь пейсатель, да?

linuxfan
()
Ответ на: комментарий от linuxfan

> time_t — число секунд с полуночи первого января 1970 года. К этому числу неприменимо понятие «часовой пояс».

Да? А если подумать? Хинт - полночь первого января 1970 года тоже наступала в 24 часовых поясах.

Дай угадаю

Не угадаешь. Я писал не на С, но, поверь, прекрасно знаю, чем отличается структура данных в памяти от указателя на неё. Ошибка имела место, не исключено, что лишь в определенных версиях libc, но я наткнулся.

man localtime

Да, есть. Пропустил, значит.

Ну и наконец: man tzset. После него тебе будет доступно и смещение временеи локального часового пояса и флаг dst. Но чукча ведь пейсатель, да?

Ну и наконец, таких умных, как ты, полон интернет и так. С подобными же рассуждениями вместо ответа на поставленный вопрос. Я писал для тех, кто, возможно, будет ходить по этим же граблям и искать решение. Вот именно для них я и отвечу, что tzset для данной задачи не подходит, потому что выставит флаг daylight в значение, правильное для текущего времени, а не для того времени, которое нужно перевести. Двойной вызов mktime это решает правильно, может есть и другие способы, но я не стал их искать.

shamus24
() автор топика
Ответ на: комментарий от shamus24

в том топике вам же сказали что преобразовывать время не нужно, time_t всегда должно быть в UTC

ACR
()

А можно поконкретнее про порчу памяти ф-ией localtime_r()? google://«localtime_r memory corruption» ничего не дал. И что за «PAD-ы»?

Про 1900 и 0 в манах есть.

Про DST не понял. Т.е. на входе у Вас может быть структура с неправильным tm_isdst?

И сразу же вопрос к знатокам, чем плох такой вариант:

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

int main()
{
	tzset();
	
	time_t time_utc = time(NULL);
	printf("%s", ctime(&time_utc));
	
	time_utc += timezone - daylight * 3600;
	printf("%s", ctime(&time_utc));

	return 0;
}

hunt@zeus:~/temp$ ./time1 
Sat Oct  9 22:09:29 2010
Sat Oct  9 16:09:29 2010

timezone и daylight в POSIX же?

hunt
()
Ответ на: комментарий от hunt

У него нет на входе tm_isdst. У него задача перевести, допустим:

01:02:03 04-05-1980 MSK

в UTC или в любую другую временную зону, если я правильно помню. И задача не совсем однозначно решаемая, так как из-за перевода времени то на час вперёд, то на час назад, некоторые показания локальных часов неправильные, а некоторые повторятся дважды.

2>топикстартер. Может вам проще будет парсить нужный вам файл zoneinfo, у него был не очень сложный формат (man tzfile).

mky ★★★★★
()
Ответ на: комментарий от hunt

> в том топике вам же сказали что преобразовывать время не нужно, time_t всегда должно быть в UTC

Я уже написал, что понятно, что это именно так, хотя и не вполне понимаю, почему, например, строка «1-JAN-1980 15:54:48» может означать как время в UTC, так и время в любом часовом поясе (в каком именно, естественно, надо указывать отдельно, или просто подразумевать), а вот time_t - исключительно UTC. Но это так, и я не собираюсь это менять :-)

А можно поконкретнее про порчу памяти ф-ией localtime_r()?

У меня - так: { time_t A; struct tm B; ... ; localtime_r(&A, &B); }. Структуры, соответственно, расположены в памяти последовательно. До вызова функции localtime_r в A находится нормальное значение, после - мусор. Достаточно было добавить { time_t A; long _pad; struct tm B .... } как всё заработало как надо. Не пинайте, если не воспроизведётся у вас, я просто описал то, что видел сам.

Про DST не понял. Т.е. на входе у Вас может быть структура с неправильным tm_isdst?

С неизвестным, скажем так. Ну вот есть локальные дата/время «31-mar-2010 21:05». Каким разумным способом я могу выяснить, какой флаг dst для этой даты правильный? Выставляю всегда false, а mktime знает, какое для этой даты значение dst правильное и корректирует его, но при этом сдвигает и время, т.е. в данном конкретном примере dst для этой даты после вызова mktime станет true, а время изменится на 22:05, и это в исходной структуре tm, а не в результирующей time_t. Поскольку нам надо перевести всё-таки 21:05, то мы поля даты/времени заполняем ещё раз, флаг dst оставляем от предыдущего вызова, и ещё раз вызываем mktime.

Есть, конечно, ситуации, когда dst автоматически определить невозможно - это как раз период, когда стрелки переводятся на час назад, и любое значение времени в течение часа может быть как с флагом dst, так и без него, и оба будут правильными, но с разным результатом преобразования к UTC. Если это критично, надо что-то думать, но мне было не нужно. Забавно, кстати, что можно создать структуру tm, которая будет указывать несуществующее время - это тот час, который выпадает при переводе часов вперёд. Как оно будет себя вести в этой ситуации - не проверял.

shamus24
() автор топика
Ответ на: комментарий от shamus24

Первая цитата не моя, я как раз таки не вижу причин слепо следовать этому правилу. =) Хотя и не спорю, что в libc и POSIX так задумывалось.

Повторить действительно не получилось. Может дадите код, в котором бага проявляется?

Изучил вопрос и тоже не смог найти прямого, на мой взгляд, решения в рамках std c lib или POSIX. Приходится дважды вызывать mktime и несколько раз перекидывать TZ. Вот код (на работоспособность почти не тестил):

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

#define TIME_FORMAT "%F %T"

struct tm* convert_time(
	const char* time_str,
	char* tz_from,
	char* tz_to,
	struct tm* time_conv)
{
	char* tz_orig = getenv("TZ");
	
	if (tz_from) putenv(tz_from);
	tzset();
	
	strptime(time_str, TIME_FORMAT, time_conv);
	mktime(time_conv);
	time_t t = mktime(time_conv);
	
	if (tz_to) putenv(tz_to);
	tzset();
	
	localtime_r(&t, time_conv);
	
	if (tz_orig) putenv(tz_to);
	else         unsetenv("TZ");
	tzset();
	
	return time_conv;
}

int main(int argc, char* argv[])
{
	struct tm time_conv;
	
	convert_time(argv[1], argv[2], argv[3], &time_conv);
	
	char time_str[48];
	strftime(time_str, 48, "%c %Z", &time_conv);
	
	puts(time_str);
	
	return 0;
}
hunt@zeus:~/temp$ ./time1 "2010-10-10 22:21:57" "TZ=Europe/Moscow" "TZ=UTC"
Sun Oct 10 18:21:57 2010 UTC

Похоже mky прав, самое нормальное решение - напрямую с tzdata работать.

hunt
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.