0

What is the shortest/most elegant way (i.e. use existing lib functions) to parse a string in the form of 20180810T143000Z to a time_t? Note that the literal always represents a UTC timestamp.

I started parsing the string and assigning values to a struct tm *tm in order to do a mktime(tm) at the end. That feels overly complicated, though.

  • 1
    Which time library do you use? I would copy each needed character with an array index to a buffer and convert it to year, month, day, hours, minutes. After that I would use the library to convert that into a time_t for the number of seconds since 1970. Straightforward is not overly complicated in my opinion. Can you show what you have so far? sscanf might work, but some think that is not a elegant function. – Jot Aug 12 '18 at 20:55
3

Parsing the string really is the only way. However there are many ways of doing it.

My preferred method is to first check that the format is correct, by looking for the T and Z being in the right place:

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

And the parsing is just making numbers with multiplication:

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

You can clean things up with a macro if you like:

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

Then:

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);

And then, yes, put them in a struct tm (or directly assign them calculation results without using the intermediate variables) and call mktime().

  • @Juraj Yeah, but you haven't included any answer in your answer, only a block of code ;) – Majenko Aug 12 '18 at 21:32
  • 1
    "first check that the format is correct" - a valid point but what would you do in the else branch? – Marcel Stör Aug 13 '18 at 6:32
  • 1
    I can't tell you that. It depends on the rest of your program. The answer may be "nothing", or it may be "tell the user", it "request a new timestamp", or any number of other things. – Majenko Aug 13 '18 at 8:11
  • 1
    @Majenko sorry, I was a little unspecific. I'm still getting familiar with Arduino (as you can tell). What I meant was that in most other environments/platforms I'm familiar with I'd probably throw an exception. So, I believe returning -1 and handle this outside the function will make this more robust. – Marcel Stör Aug 13 '18 at 11:35
  • 1
    @MarcelStör Sure - if that's what you want to do. – Majenko Aug 13 '18 at 12:52
1

For the sake of completeness here's my "answer" working with Strings rather than 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);
}
  • 1
    That poor poor heap. I feel for it... – Majenko Aug 13 '18 at 8:13
  • 1
    This should work, but note that every call to the substring() method allocates a new String on the heap. Even if you have plenty of free memory, the multiple calls to malloc() and free() can make this quite inefficient. – Edgar Bonet Aug 13 '18 at 8:20
  • 1
    @EdgarBonet yes, I know, point taken. What is fed into this function is the (partial) result of an HTTP response readStringUntil(). I guess it'd be more efficient to first convert calTimestamp into a char[]? – Marcel Stör Aug 13 '18 at 9:43
  • 2
    Quite likely. The String method c_str() returns the internal char * pointer, so no real conversion is needed. – Edgar Bonet Aug 13 '18 at 10:09
1

This is not complete without sscanf.

I took the sketch by @Juraj and declared seperate integers to be sure that every %d would match with an integer.

#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() {
}

Marcel Stör, there are now four good solutions. In my opinion they are equally good.

1

Here is yet another way to convert the time stamp string to time_t. There are several great answers here already, but you may want to compare binary file sizes. This sketch's is 3884 bytes (IDE Version 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');

  // Assemble time elements into time_t.
  time_t t = makeTime(myTimeElements);

  // Print out the contents of "t" one "piece" at a time using the "time_t" functions.
  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(){}
1
#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 T
  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() {

}

You can install TimeLib in Library Manager. It works on all Arduino platforms.

  • 1
    In buff[i++] * 10 + buff[i++], the order of evaluation of the i++ is unspecified. It may work as expected with any particular combination of compiler version and settings, and fail on the next one. gcc 5.4 warns me that “operation on ‘i’ may be undefined”. The correct way to do that is tm.Month = buff[i++] * 10; tm.Month += buff[i++];. – Edgar Bonet Aug 13 '18 at 8:10
  • 1
    @EdgarBonet thanks, I just proposed this change: arduino.stackexchange.com/review/suggested-edits/38715 – Marcel Stör Aug 13 '18 at 11:29
  • 1
    @MarcelStör: What I wrote for tm.Month is valid for every instance where i++ appears more than once in the same expression. – Edgar Bonet Aug 13 '18 at 12:00
  • 1
    @EdgarBonet I know, makes total sense. That's why proposed that edit. In the end it was ok to get rejected because many more lines are actually affected. – Marcel Stör Aug 13 '18 at 12:22
  • 1
    Re “the compiler has no reason to evaluate [...]”: it also has no reason to evaluate them in the order you expect! Please, learn about sequence points in C++ before making such wrong assumptions. Note specifically that “Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression.” – Edgar Bonet Aug 13 '18 at 20:41
1

Alternative to my first answer would be to use standard C functions from time.h and sscanf. C function strptime can't parse the timestamp without delimiters. But sscanf can parse your input.

#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() {
}

In AVR "%02hhd" must be used because the corresponding members in struct tm are int8_t.

  • The %d is for an integer, also for an avr microcontroller. The difference is that the TimeLib TimeElements does not use integers, but bytes. – Jot Aug 14 '18 at 7:06
  • @Jot, this is C time.h – Juraj Aug 14 '18 at 7:16
  • Yes, for the C time.h the tm_year and other elements are all integers. Also for a avr microcontroller the %d matches an integer. The %d expects an 'int', not a 2 or 4 byte variable. – Jot Aug 14 '18 at 7:34
  • %d on AVR works for sprintf, but sscanf read wrong without hh. try it – Juraj Aug 14 '18 at 8:08
  • 1
    I used hh because a compiler warning saying that it is int8_t and it helped to get correct data. I opened time.h, but it was for samd or for esp and there was int. I had limited time so I didn't investigate it further than. Of course %d is for int. I tested int and removed the note. Then I discovered that in tm really is unt8_t in tm. – Juraj Aug 15 '18 at 4:25
-1
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
  • The question was how to parse the string to a time_t, and you are not answering it. – Edgar Bonet Apr 14 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.