Конвертация времени

Итак, столкнулся с довольно тривиальной задачей — посчитать промежуток времени в секундах, минутах, часах, и днях между двумя датами и временем. Задача, в общем-то, не новая и легко решается на ПК.
Суть этого поста — адаптировать подобные преобразования для МК.
Сразу оговорюсь, что мы не будем учитывать зональность времени, а будем вычислять по гринвичу.

Конвертация из YYYY:MM:DD:HH:MM:SS в uint32_t


Итак существует несколько проблем:
— високосный год
— високосное столетие
— високосное четырехсотлетие
— разные длины месяцев.
— февраль находится в неудобном для вычисления положения: где-то между началом и концом года.

Итак, попробуем сделать так, чтобы облегчить вычисления.
Первое что нужно сделать — поместить февраль в хвост вычислений. Сделаем как-бы альтернативный год, в котором «новый год» будет 1 марта. Таким образом если мы будем сразу правильно вычислять начало заданного года, т.е. 1 марта, то с февралем у нас проблем не возникнет, так как он не будет влиять на смещение порядкового номера дат в этом году, просто 28 февраля будет 364-м порядковым числом (потому что считаем от нуля), а 29 февраля — 365-м. Приняли.
Второе: Так как у нас год стал стабильным на всем его протяжении, кроме самого конца, нам потребуется всего одна таблица дней смещений.
вот она:
const uint16_t day_offset[12] = {0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337};
где элементы массива указывают на смещение в кол-ве дней на каждый месяц, начиная с марта. Впрочем, можно вычислять и иным способом, по формуле:
day_offset = (153 * month + 2) / 5;
где month — номер месяца, причем март = 0; апрель = 1 и т. д.
В общем-то и все. Напишем функцию:
const uint16_t day_offset[12] = {0, 31, 61,92, 122, 153, 184, 214, 245, 275,306, 337};
typedef struct
{
	uint8_t sec;
	uint8_t min;
	uint8_t hour;
	uint8_t date;
	uint8_t month;
	uint8_t day;
	int16_t year;
} sDateTime;

uint32_t EncodeDateTime(sDateTime *dt)
{
	uint8_t a = dt->month < 3; // а = 1, если месяц январь или февраль
	int16_t y = dt->year - a - 2000;  // y = отнимаем от года 1, если а = 1, а так же 2000;
	uint8_t m = dt->month + 12 * a - 3; // аналогия выражения (12 + month - 3) % 12; делаем март нулевым месяцем года.
	return (dt->date - 1 + day_offset[m] + y * 365 + y / 4 - y / 100 + y / 400) * 86400 +
               (int)dt->hour * 3600 + (int)dt->min * 60 + dt->sec;
}

Началом отсчета будет 1 марта 2000 года 00:00:00. Для совместимости с Unix time можно прибавить константу 951868800
Эта дата удобна не только с позиции «круглого» года, но и тем, что мы находимся в самом начале 400-летнего цикла григорианского календаря. А еще — сравнительно небольшие текущие числа, которые еще лет 50 не будут создавать проблем в числе с размерностью int, и тем более в uint32_t.

Итак, с помощью этой функции мы делаем порядковое число секунды, начиная с момента 1 марта 2000 00:00:00
Теперь узнать промежуток в секундах между двумя датами не составит труда.

Ну а если требуется отобразить интервал в днях, часах, минутах и секундах, то напишем следующую функцию:
typedef struct
{
	uint32_t Day;
	uint8_t Hour;
	uint8_t Min;
	uint8_t Sec;
} sDayTime;

void DecodeDayTime(uint32_t idt, sDayTime *dt)
{
	ldiv_t a = ldiv(idt, 86400);
	dt->Day = a.quot;
	a = ldiv(a.rem, 3600);
	dt->Hour = a.quot;
	a = ldiv(a.rem, 60);
	dt->Min = a.quot;
	dt->Sec = a.rem;
}

Конвертация из uint32_t в YYYY:MM:DD:HH:MM:SS

Что ж, возможно нам понадобится узнать, какое число и время будет спустя 100500 секунд относительно заданного.
Для этого нам нужна обратная функция перевода числа в удобоваримую дату/время.

void DecodeDateTime(uint32_t idt, sDateTime *dt)
{
    sDayTime day;
    ldiv_t century, year_of_century;
    uint32_t day_of_century, a, m;
    DecodeDayTime(idt, &day);
    dt->sec = day.Sec;
    dt->min = day.Min;
    dt->hour = day.Hour;
    century = ldiv(day.Day * 4 + 3, 146097); // Вычисляем кол-во 100-летий
    day_of_century = century.rem / 4; // Остаток дней в столетии
    year_of_century = ldiv(day_of_century * 4 + 3, 1461); // Вычисляем кол-во лет в столетии
    day_of_year = year_of_century.rem / 4; // Остаток дней в году
    m = (5 * day_of_year + 2) / 153; // номер месяца, где март = 0, апрель = 1 и т.д.
    dt->date = day_of_year + 1 - day_offset[m]; // находим день в месяце
    a = m < 10;
    dt->month = m + 3 - 12 * a; // вычисляем месяц в году
    dt->year = 100 * century.quot + year_of_century.quot + 2000 + a; // вычисляем год
    dt->day = (day.Day + 3) % 7; // вычисляем день недели
}
  • +8
  • 12 августа 2015, 17:34
  • Mihail

Комментарии (35)

RSS свернуть / развернуть
Скорее всего опечатка
void DecodeDateTime(unsigned long *idt, sDateTime *dt)
{
    sDayTime day;
    ldiv_t a, c;
    int32_t b, d, e;
    
    DecodeDayTime(unsigned long *idt, sDayTime *day);
0
о да, поправил.
0
Программные календари на мой взгляд опасны, ошибки могут вылезти спустя пару месяцев )
0
для этого нужно вдумчиво просмотреть весь календарь в точках перехода. Начало — конец месяца.
Вообще этот алгоритм работал на устройстве, который показывал, сколько осталось дней, часов минут секунд до начала олимпиады. так что было время его проверить)))
+1
Программные календари на мой взгляд опасны, ошибки могут вылезти спустя пару месяцев )

Эту фразу можно отнести к любому ПО, оно опасно, ошибки могут вылезти спустя месяцы. :)

ИМХО, если мы говорим о отсчете времени, самое веселье для программиста — это перевод летнего/
зимнего времени, ввод «дополнительных секунд», и пересечение часовых поясов. Особенно, весело когда локальные часы переводят назад. Когда получается, например, что клиент ввел ПИН в банкомате и получил деньги, но по местному времени он сначала получил деньги, а потом, спустя практически час, ввел ПИН. Та еще веселуха …
+1
для этого придумали юнит-тесты :)
0
Понравился хитрый ход с 1 марта.

Но лучше использовать time.h из стандартной библиотеки. Все тоже самое и даже больше.

Была у нас история. Месяц не могли найти баг, а оказалось разработчик использовал библиотеку для работы со временем, написанную непонятно кем, и баг был в ней. Разработчик еще говорил, что библиотека годами проверена, а оказалось вон как. В результате переписали на стандартной библиотеке и все работает нормально.
+2
Проверка: сколько секунд прошло между 28.03.2010 0:00 и 29.03.2010 0:00 в московском часовом поясе?
Какой результат у вашей библиотеки? (правильный ответ: 82800)
0
я в начале статьи указал, что не описываю зональность.
Интересней другое: Сколько секунд между 30 окт 2005 2:00 и 30 окт 2005 2:30? Судя по-всему здесь вообще два решения.
0
void DecodeDateTime(uint32_t idt, sDateTime *dt)
{
    sDayTime day;
    ldiv_t a, c;
    int32_t b, d, e;


… F — G
H — I — J — K — L — M — N — O — P
Q — R — S — T — U- V,
W — X — Y and Z
Now I know my ABC's
Next time won't you sing with me.

Упс, извиняюсь, увлекся …

Я хотел сказать, что у Вас в функции DecodeDateTime() используется переменная «m»
m = (5 * d + 2) / 153; // номер месяца, где март = 0, апрель = 1 и т.д.

которая не имеет деклареции…
0
  • avatar
  • e_mc2
  • 13 августа 2015, 00:22
исправил.
0
и вылезла другая ошибка
m = (5 * d + 2) / 153; // номер месяца, где март = 0, апрель = 1 и т.д.
d — не определена и не инициализирована. Вероятно там подразумевается day_of_year?
0
ага. невнимательный я стал…
0
… X — Y — Z!!!
Oh, well, you see
Now I know the ABC!

Мы так пели песенку в школе)))
0
Народ, все до нас придумано астрономами и церковью. А кое-кто даже реализовал: we.easyelectronics.ru/Soft/funkcii-kalendarya-i-vremeni-na-odnom-registre.html
Реализация мною проверерена — работает, все четко и понятно. Но за идею нового года с 1 марта респект!
0
+1
она не учитывается в системе. словно ее и не было
0
UTC (всемирное координированное время) — атомная шкала времени, аппроксимирующая UT1. Это международный стандарт, на котором базируется время часовых поясов. В UTC в качестве единицы времени используется секунда СИ, поэтому UTC идёт синхронно с международным атомным временем (TAI). Обычно в сутках UTC 86 400 секунд СИ, но для поддержания расхождения UTC и UT1 не более чем 0,9 с, при необходимости, 30 июня или 31 декабря добавляется (или, теоретически, вычитается) секунда координации. К настоящему времени (июнь 2015 года) все секунды координации были положительными. Если не требуется высокая точность, то UTC можно использовать как аппроксимацию UT1. Таким образом, шкала времени UTC, в отличие от других версий всемирного времени, является равномерной, но не является непрерывной. Разница между UT1 и UTC, обозначаемая как DUT1 (DUT1 = UT1 − UTC), постоянно отслеживается и еженедельно публикуется на сайте IERS в Бюллетене А (Bulletin — A).

Таким образом, если часы показывают UTC (а не UT1), то секунда координации должна учитываться.
Если её не учитывать то невозможно хранить например такую дату: 1992-7-1 23:59:60 UTC
+1
не будем об этом. Мне ни к чему это. Тем более, что никто не знает, когда будет високосная секунда в будущем.
0
тем более если большие дяди, внедрившие unix time забили на это, то, думаю, здесь полезнее иметь совместимость с Unix Time, чем с UTC.
0
При использовании Unix Time секунды предшествующие секунде координации должны длится 2 секунды (если секунда координации добавляется а не вычитается). Т.о. если Вас интересует именно разница во времени то её все равно надо учитывать.
Или если например Ваш таймер отсчитывает время в секундах, то он должен пропустить тик. Иначе с каждой такой секундой ошибка будет увеличиваться.
0
Вообще учитывать или не учитывать такие нюансы надо рассматривать в контексте конкретной задачи.
Вопрос в том что читатель статьи должен понимать какие нюансы существуют.
0
Учет всех коррекций календаря, високосных годов и прочей требухи делается с помощью так называемого Julian Day (не путать с Юлианским календарем) без таблиц и прочих хаков. Пример кода на С для конверсии из день-месяц-год в Julian Day и обратно. При желании функции можно еще больше упростить если забить на учет Юлианского календаря (который чаще всего в типичных приложениях не нужен). Если полученное число сдвинуть на нужное число разрядов влево, то в младшие разряды можно поместить секунды с начала дня и таким образом упаковать дату и время в одной переменной (без учета таймзон). В большинстве практических задач удобно держать время в одной таймзоне, а пересчет в другие таймзоны делать только в момент формирования текстового представления (того, что показывают пользователю).
0
  • avatar
  • evsi
  • 13 августа 2015, 16:03
возможно вы не уловили основной посыл статьи. Задача давно решена, а здесь адаптация для МК. Поменьше вычислений чтобы. Поэтому юлианский день не применялся. Ни к чему. У нас свой локальный отсчет, что вполне применимо для разного рода несложных задач, таких, например, как вычислить количество дней, часов, минут и секунд до начала чемпионата по футболу в 18-м году, чтобы на табло отображать. Ввел текущую дату-время — ввел дату и время события — вуаля! Табло работает. Ну и прочие фсякие рекламные штучки, показывающие остаток до конца/начала акции и прочее.
0
Именно потому, что я понял задачу, я и предложил вариант с Юлианским днем. Он врядли будет сложнее вычислительно (особенно если его порезать на предмет поддержки юлианского календаря). Да и таблиц ему не надо.
0
юлианский день исчисляется в днях. Удобно это или нет — на вкус и цвет. У меня бОльшая схожесть с unix-time, где единица исчисления — секунда. Тем более, что все перетрубации с датами, которые происходили в прошлом — в прошлом. В большинстве задач обращаться к прошлым датам — не применяется. Обычно как: Создали устройство. С этого момента начинается его «жизнь». Поэтому углубляться цифрами в давние века — ни к чему. А именно это и предполагается при использовании юлианского дня. А держать число в секундах от 4000 лет до нашей эры — накладно. Нужно long. Со всеми последствиями для МК, особенно 8-ми битных. Так что мне кажется, найден некий компромисс, где приводится решение довольно конкретной задачи к конкретном диапазоне дат.
0
Про таблицы. Они и у меня не слишком нужны. В статье же указано про это:
Так как у нас год стал стабильным на всем его протяжении, кроме самого конца, нам потребуется всего одна таблица дней смещений.
вот она:
const uint16_t day_offset[12] = {0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337};

где элементы массива указывают на смещение в кол-ве дней на каждый месяц, начиная с марта. Впрочем, можно вычислять и иным способом, по формуле:
day_offset = (153 * month + 2) / 5;

где month — номер месяца, причем март = 0; апрель = 1 и т. д.
Таблица была введена, поскольку деление на 5 может быть более затратным, чем поиск по таблице нужного значения.
0
следовательно:
uint32_t EncodeDateTime(sDateTime *dt)
{
        uint8_t a = dt->month < 3; // а = 1, если месяц январь или февраль
        int16_t y = dt->year - a - 2000;  // y = отнимаем от года 1, если а = 1, а так же 2000;
        uint8_t m = dt->month + 12 * a - 3; // аналогия выражения (12 + month - 3) % 12; делаем март нулевым месяцем года.
        return (dt->date - 1 + (153 * month + 2) / 5 + y * 365 + y / 4 - y / 100 + y / 400) * 86400 +
               (int)dt->hour * 3600 + (int)dt->min * 60 + dt->sec;
}
аналогично тому, что указано в статье. Без таблиц
0
*month = m
0
… Ну и прочие фсякие рекламные штучки, показывающие остаток до конца/начала акции и прочее.
Ну для рекламных штучек, еще необходимо поправку на неравномерность вращения Земли делать — иначе несолидно!
0
ага, а еще поправку на то, что клиент может находиться на разном уровне от центра земли, ведь известно, что время замедляется с увеличением гравитационного поля.
0
Я публиковал на форуме библиотеку для работы с RTC STM32

Там есть функции


//преобразовать время из RTC в ticks
uint32_t RTC_GetTicksFromTime(uint8_t timetype, const RTCTIME *rtc);

//конвертирует ticks в структуру RTCTIME
void RTC_TicksToRTC (uint32_t ticks, RTCTIME *rtc);
0
  • avatar
  • alk0v
  • 24 августа 2015, 13:26
Выкладываю кишочки предложенных функций
/*------------------------------------------*/
/* Convert time structure to timeticks      */
/*------------------------------------------*/

uint32_t RTC_GetTicksFromTime(uint8_t timetype, const RTCTIME *rtc)
{
	uint32_t utc, i, y;
	y = rtc->year - 1970;
	if (y > 2106 || !rtc->month || !rtc->mday) return 0;

	utc = y / 4 * 1461; y %= 4;
	utc += y * 365 + (y > 2 ? 1 : 0);
	for (i = 0; i < 12 && i + 1 < rtc->month; i++) {
		utc += numofdays[i];
		if (i == 1 && y == 2) utc++;
	}
	utc += rtc->mday - 1;
	utc *= 86400;
	if(timetype==time_midnight)
	{
		return utc;
	}
	else if(timetype==time_current)
	{
		utc += rtc->hour * 3600 + rtc->min * 60 + rtc->sec;
		return utc;
	}
	else return 0;
}

/*------------------------------------------*/
/* Get time in calendar form                */
/*------------------------------------------*/

int RTC_GetTime(uint8_t timetype, RTCTIME *rtc)
{
	uint32_t n,i,d,utc;
	utc = RTC_GetCounter();
	/* Compute  hours */
	if(timetype==time_current)
	{
		rtc->sec = (uint8_t)(utc % 60); utc /= 60;
		rtc->min = (uint8_t)(utc % 60); utc /= 60;
		rtc->hour = (uint8_t)(utc % 24); utc /= 24;
	}
	if(timetype==time_midnight)
	{
		rtc->sec = 0;
		rtc->min = 0;
		rtc->hour = 0;
		utc/=86400;
	}
	rtc->wday = (uint8_t)((utc + 4) % 7);
	rtc->year = (uint16_t)(1970 + utc / 1461 * 4); utc %= 1461;
	n = ((utc >= 1096) ? utc - 1 : utc) / 365;
	rtc->year += n;
	utc -= n * 365 + (n > 2 ? 1 : 0);
	for (i = 0; i < 12; i++) {
		d = numofdays[i];
		if (i == 1 && n == 2) d++;
		if (utc < d) break;
		utc -= d;
	}
	rtc->month = (uint8_t)(1 + i);
	rtc->mday = (uint8_t)(1 + utc);
	return 1;
}
0
Было бы здорово сравнить, что быстрее/эффективнее :)
0
  • avatar
  • alk0v
  • 25 августа 2015, 12:07
мне кажется, невооруженным глазом видно, что мои функции пошустрей будут. По крайней мере, в них нет циклов.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.