1

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
  • 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
    Commented Aug 12, 2018 at 20:55

7 Answers 7

4

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().

5
  • @Juraj Yeah, but you haven't included any answer in your answer, only a block of code ;)
    – Majenko
    Commented Aug 12, 2018 at 21:32
  • 1
    "first check that the format is correct" - a valid point but what would you do in the else branch? Commented Aug 13, 2018 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
    Commented Aug 13, 2018 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. Commented Aug 13, 2018 at 11:35
  • 1
    @MarcelStör Sure - if that's what you want to do.
    – Majenko
    Commented Aug 13, 2018 at 12:52
2

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

8
  • 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++];. Commented Aug 13, 2018 at 8:10
  • 1
    @EdgarBonet thanks, I just proposed this change: arduino.stackexchange.com/review/suggested-edits/38715 Commented Aug 13, 2018 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. Commented Aug 13, 2018 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. Commented Aug 13, 2018 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.” Commented Aug 13, 2018 at 20:41
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);
}
4
  • 1
    That poor poor heap. I feel for it...
    – Majenko
    Commented Aug 13, 2018 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. Commented Aug 13, 2018 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[]? Commented Aug 13, 2018 at 9:43
  • 2
    Quite likely. The String method c_str() returns the internal char * pointer, so no real conversion is needed. Commented Aug 13, 2018 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

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.

9
  • 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
    Commented Aug 14, 2018 at 7:06
  • @Jot, this is C time.h
    – Juraj
    Commented Aug 14, 2018 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
    Commented Aug 14, 2018 at 7:34
  • %d on AVR works for sprintf, but sscanf read wrong without hh. try it
    – Juraj
    Commented Aug 14, 2018 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
    Commented Aug 15, 2018 at 4:25
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
1
  • The question was how to parse the string to a time_t, and you are not answering it. Commented Apr 14, 2019 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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