Как разобрать 20180810T143000Z в time_t

Каков самый короткий/элегантный способ (т. е. использовать существующие функции библиотеки) для анализа строки в форме 20180810T143000Z в time_t? Обратите внимание, что литерал всегда представляет собой временную метку UTC.

Я начал анализировать строку и присваивать значения struct tm *tm, чтобы в конце выполнить mktime(tm). Однако это кажется слишком сложным.

, 👍1

Обсуждение

Какую библиотеку времени вы используете? Я копировал каждый необходимый символ с индексом массива в буфер и конвертировал его в год, месяц, день, часы, минуты. После этого я бы использовал библиотеку, чтобы преобразовать это в time_t для количества секунд с 1970 года. На мой взгляд, это не слишком сложно. Можете ли вы показать, что у вас есть на данный момент? sscanf может работать, но некоторые считают, что это не элегантная функция., @Jot


7 ответов


Лучший ответ:

4

Разбор строки — единственный способ. Однако есть много способов сделать это.

Мой предпочтительный метод — сначала проверить правильность формата, проверив правильность расположения букв T и Z:

if (timeString[8] == 'T' && timeString[15] == 'Z') {
    ... parse in here
}

А синтаксический анализ — это просто получение чисел с умножением:

int year = (timeString[0] - '0') * 1000 +
           (timeString[1] - '0') * 100 +
           (timeString[2] - '0') * 10 +
           (timeString[3] - '0');

Если хотите, вы можете очистить ситуацию с помощью макроса:

#define NUM(off, mult) ((timeString[(off)] - '0') * (mult))

Тогда:

int year =   NUM(0, 1000) + NUM(1, 100) + NUM(2, 10) + NUM(3, 1);
int month =  NUM(4, 10)   + NUM(5, 1);
int day =    NUM(6, 10)   + NUM(7, 1);
int hour =   NUM(9, 10)   + NUM(10, 1);
int minute = NUM(11, 10)  + NUM(12, 1);
int second = NUM(13, 10)  + NUM(14, 1);

И затем, да, поместите их в struct tm (или напрямую присвойте им результаты вычислений без использования промежуточных переменных) и вызовите mktime().

,

@Juraj Да, но ты не включил в свой ответ никакого ответа, только блок кода;), @Majenko

«сначала проверьте правильность формата» — верный пункт, но что бы вы сделали в ветке «else»?, @Marcel Stör

Я не могу вам этого сказать. Это зависит от остальной части вашей программы. Ответом может быть «ничего», «сообщить пользователю», «запросить новую временную метку» или что-то еще., @Majenko

@Маженко, извини, я немного неконкретно выразился. Я все еще знакомлюсь с Arduino (как вы понимаете). Я имел в виду, что в большинстве других сред/платформ, с которыми я знаком, я, вероятно, выдал бы исключение. Итак, я считаю, что возврат -1 и обработка этого вне функции сделают это более надежным., @Marcel Stör

@MarcelStör Конечно, если ты этого хочешь., @Majenko


2
#include <Time.h>

void setup() {

  Serial.begin(115200);

  char buff[] = "20180810T143000Z";
  for (int i = 0; i < sizeof(buff); i++) {
    buff[i] = buff[i] - '0';
  }
  int yr = buff[0] * 1000 + buff[1] * 100 + buff[2] * 10 + buff[3];
  if (yr > 99)
    yr = yr - 1970;
  else
    yr += 30;
  TimeElements tm;
  tm.Year = yr;
  tm.Month = buff[4] * 10 + buff[5];
  tm.Day = buff[6] * 10 + buff[7];
  // 8 Т
  tm.Hour = buff[9] * 10 + buff[10];
  tm.Minute = buff[11] * 10 + buff[12];
  tm.Second = buff[13] * 10 + buff[14];

  time_t t = makeTime(tm);

  sprintf(buff, "%02d%02d%02d %02d%02d%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));

  Serial.println(buff);

}

void loop() {

}

Вы можете установить TimeLib в диспетчере библиотек. Он работает на всех платформах Arduino.

,

Да, я изучал strptime, но пришел к выводу, что без разделителей он, вероятно, не будет работать. Поэтому я даже не пытался., @Marcel Stör

В buff[i++] * 10 + buff[i++] порядок вычисления i++ не указан. Он может работать должным образом с любой конкретной комбинацией версии и настроек компилятора и не работать при следующей. gcc 5.4 предупреждает меня, что «операция над 'i' может быть неопределенной». Правильный способ сделать это: tm.Month = buff[i++] * 10; tm.Month += buff[i++];., @Edgar Bonet

@EdgarBonet, спасибо, я только что предложил это изменение: https://arduino.stackexchange.com/review/suggested-edits/38715, @Marcel Stör

@MarcelStör, я прочитал комментарий Эдгара, но этот код не какой-то фрагмент, а полноценный рабочий пример. Мне не нравится редактировать это без тестирования. И я не могу это проверить сейчас., @Juraj

@MarcelStör: То, что я написал для tm.Month, справедливо для каждого случая, когда i++ встречается более одного раза в одном и том же выражении., @Edgar Bonet

@EdgarBonet Я знаю, это имеет смысл. Вот почему предложил это редактирование. В конце концов, получить отказ было нормально, потому что на самом деле это затронуло гораздо больше строк., @Marcel Stör

в этом случае использование i++ хорошо. у компилятора нет причин вычислять какое-то подвыражение с i++ позже в выражении перед некоторым подвыражением с i++ ранее в выражении. Я изменю это. Я видел предупреждение, но у меня было на это ограниченное время., @Juraj

Что касается «компилятору нет причин оценивать [...]»: у него также нет причин оценивать их в том порядке, в котором вы ожидаете! Пожалуйста, узнайте о точках последовательности в C++, прежде чем делать такие неверные предположения. Обратите внимание, что «между предыдущей и следующей точкой последовательности сохраненное значение объекта должно быть изменено не более одного раза при вычислении выражения»., @Edgar Bonet


1

Для полноты картины мой «ответ» работает с String, а не с char[].

time_t convertToTime(String calTimestamp) {
  struct tm tm;
  Serial.println("Parsing " + calTimestamp);
  String year = calTimestamp.substring(0, 4);
  String month = calTimestamp.substring(4, 6);
  if (month.startsWith("0")) {
    month = month.substring(1);
  }
  String day = calTimestamp.substring(6, 8);
  if (day.startsWith("0")) {
    month = day.substring(1);
  }
  tm.tm_year = year.toInt() - 1900;
  tm.tm_mon = month.toInt() - 1;
  tm.tm_mday = day.toInt();
  tm.tm_hour = calTimestamp.substring(9, 11).toInt();
  tm.tm_min = calTimestamp.substring(11, 13).toInt();
  tm.tm_sec = calTimestamp.substring(13, 15).toInt();
  return mktime(&tm);
}
,

Эта бедная, бедная куча. Я сочувствую этому..., @Majenko

Это должно работать, но учтите, что каждый вызов метода substring() выделяет в куче новую строку. Даже если у вас много свободной памяти, множественные вызовы malloc() и free() могут сделать это совершенно неэффективным., @Edgar Bonet

@EdgarBonet да, я знаю, точка зрения принята. В эту функцию передается (частичный) результат HTTP-ответа readStringUntil(). Я думаю, было бы более эффективно сначала преобразовать «calTimestamp» в «char[]»?, @Marcel Stör

Вполне вероятно. Метод String c_str() возвращает внутренний указатель char *, поэтому никакого реального преобразования не требуется., @Edgar Bonet


1

Без sscanf это будет невозможно.

Я взял скетч @Juraj и объявил отдельные целые числа, чтобы быть уверенным, что каждый %d будет соответствовать целому числу.

#include <Time.h>

void setup() {

  Serial.begin(115200);

  char buff[] = "20180810T143000Z";

  TimeElements tm;

  int yr, mnth, d, h, m, s;
  sscanf( buff, "%4d%2d%2dT%2d%2d%2dZ", &yr, &mnth, &d, &h, &m, &s);

  tm.Year = yr - 1970;
  tm.Month = mnth;
  tm.Day = d;
  tm.Hour = h;
  tm.Minute = m;
  tm.Second = s;

  time_t t = makeTime(tm);

  sprintf(buff, "%02d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));

  Serial.println(buff);
}

void loop() {
}

Марсель Штёр, теперь есть четыре хороших решения. На мой взгляд, они одинаково хороши.

,

1

Альтернативой моему первому ответу было бы использование стандартных функций C из time.h и sscanf. Функция C strptime не может анализировать временную метку без разделителей. Но sscanf может проанализировать ваш ввод.

#include <time.h>

void setup() {

  Serial.begin(115200);

  const char* buff = "20180810T143000Z";

  tm tms;
  sscanf(buff, "%04d%02d%02dT%02d%02d%02d", &(tms.tm_year), &(tms.tm_mon), &(tms.tm_mday), &(tms.tm_hour), &(tms.tm_min), &(tms.tm_sec));
  tms.tm_year -= 1900;
  tms.tm_mon -= 1;
  tms.tm_isdst = 0;
  time_t t = mktime(&tms);

  Serial.println(ctime(&t));
}

void loop() {
}

В AVR необходимо использовать «%02hhd», поскольку соответствующие члены в структуре tm — int8_t.

,

%d означает целое число, а также для микроконтроллера avr. Разница в том, что TimeElements TimeLib использует не целые числа, а байты., @Jot

@Jot, это время C.h, @Juraj

Да, для C time.h tm_year и другие элементы являются целыми числами. Также для микроконтроллера avr %d соответствует целому числу. %d ожидает «int», а не 2- или 4-байтовую переменную., @Jot

%d на AVR работает для sprintf, но sscanf читается неправильно без hh. попробуй, @Juraj

Я пробовал это разными способами с разными данными в стеке, используя Arduino 1.8.5 с платой Arduino Uno. «int» — это два байта, «short int» — тоже два байта, а «short short int» не существует. Формат «%d» работает с «int», а «%hd», похоже, делает то же самое, что и «%d». Формат «%hhd» читает байт со знаком. Все так, как я помню, никаких странностей. Если вы сможете доказать свою правоту с помощью эскиза, можете ли вы создать для этого новую тему?, @Jot

@Jot, это происходит только тогда, когда я использую члены структуры tm в качестве параметров sscanf. Предупреждение: "%d" ожидает аргумент типа "int*", но аргумент 4 имеет тип "int8_t* {он же знаковый символ*}". и отсканированные значения неверны. При использовании обычной переменной int hh не требуется., @Juraj

AVR имеет int8_t tm_hour, @Juraj

Теперь я вижу это: tm для avr имеет элементы int8_t и int16_t. Извините, это действительно не все целые числа для avr. Я не знал этого. Спасибо. Согласны ли вы, что %d в sscanf соответствует «int» для avr? Я вижу, что вы удалили это запутанное последнее предложение., @Jot

Я использовал hh, потому что компилятор предупреждал, что это int8_t, и это помогло получить правильные данные. Я открыл time.h, но он был для samd или для esp и там был int. У меня было ограниченное время, поэтому я не стал исследовать этот вопрос дальше. Конечно, %d предназначен для int. Я проверил int и удалил заметку. Потом я обнаружил, что в tm действительно есть unt8_t в tm., @Juraj


2

Вот еще один способ преобразовать строку отметки времени в time_t. Здесь уже есть несколько отличных ответов, но вы можете сравнить размеры двоичных файлов. Размер этого скетча составляет 3884 байта (версия IDE 1.0.6.2, GCC 4.2.1).

#include <Time.h>
TimeElements myTimeElements;
char timeString[] = "20180810T143000Z";

void setup(){

  Serial.begin(9600);

  myTimeElements.Year = CalendarYrToTm((timeString[0] - '0') * 1000 + (timeString[1] - '0') * 100 + (timeString[2] - '0') * 10 + (timeString[3] - '0'));
  myTimeElements.Month = (timeString[4] - '0') * 10 + (timeString[5] - '0');
  myTimeElements.Day = (timeString[6] - '0') * 10 + (timeString[7] - '0');
  myTimeElements.Hour = (timeString[9] - '0') * 10 + (timeString[10] - '0');
  myTimeElements.Minute = (timeString[11] - '0') * 10 + (timeString[12] - '0');
  myTimeElements.Second = (timeString[13] - '0') * 10 + (timeString[14] - '0');

  // Собираем элементы времени в time_t.
  time_t t = makeTime(myTimeElements);

  // Распечатываем содержимое "t" по одному "куску" за раз, используя функции "time_t".
  Serial.println(year(t));
  Serial.println(month(t));
  Serial.println(day(t));
  Serial.println(hour(t));
  Serial.println(minute(t));
  Serial.println(second(t));

}

void loop(){}
,

0
String datetimestr = "20180810T143000Z";
int splitted = datetimestr.indexOf("T");
String datestr = datetimestr.substring(0, splitted); //20180810
String timestr = datetimestr.substring(splitted + 1, datetimestr.length() - 1); //143000
,

Вопрос заключался в том, как проанализировать строку ** до time_t**, и вы не ответили на него., @Edgar Bonet