cposixstrftime

How can I print current time as an ISO 8601 string with strftime?


I want to print the current time in ISO 8601 format as close as possible to Python's isoformat() function, which produces output like 2024-01-16T09:32:46.090+01:00.

As a starting point, I have this code that uses gettimeofday(), localtime(), and strftime():

#include <stdio.h> //printf
#include <sys/time.h>

int main(){
  struct timeval tv; 
  struct timezone tz;

  gettimeofday(&tv, &tz);

  char buf[30]; // 29 chars + '\0'
  strftime(buf, 30, "%Y-%m-%dT%H:%M:%S%z", localtime(&tv.tv_sec));

  printf("using strftime: %s\n", buf);
}

That generates 2024-01-16T09:32:46+01:00 (missing milliseconds).

I didn't not find any format specifier for strftime() to print milliseconds.


Solution

  • strftime() takes a struct tm as input, and struct tm has no notion of milliseconds/microseconds so strftime() can't get milliseconds out of it.

    In order to the the milliseconds you will need to resort to the struct timeval returned by gettimeofday() which has a member called tv_usec with the microseconds that you can easily convert to milliseconds by dividing by 1000. Or use clock_gettime() which provides nanoseconds. See an example with clock_gettime at the bottom.

    #include <stdio.h> //printf
    #include <sys/time.h>
    
    int main(){
      struct timeval tv; 
      struct timezone tz;
    
      gettimeofday(&tv, &tz);
    
      char buf[sizeof "9999-12-31T23:59:59.999+0000"]; // 28 chars + '\0'
      size_t bufsize = sizeof buf;
      int off = 0;
      struct tm *local = localtime(&tv.tv_sec);
      off = strftime(buf, bufsize, "%FT%T", local); // same as "%Y-%m-%dT%H:%M:%S"
      off += snprintf(buf+off, bufsize-off, ".%03d", tv.tv_usec/1000);
      off += strftime(buf+off, bufsize-off, "%z", local);
    
      printf("using strftime: %s\n", buf);
    }
    

    When executed it will produce 2024-01-16T14:08:15.079+0100 which matches ISO 8601 with milliseconds and UTC offset. Unfortunately the %z produces a UTC offset in the form +0100 (no colon) instead of +01:00

    gcc test.c -g -Wall -std=c11 && ./a.out
    using strftime: 2024-01-16T14:08:15.079+0100
    

    If you really really need the UTC offset with colon, then you can use struct timezone from gettimeofday(). You need to replace the off += strftime(buf+off, 30-off, "%z", local); to more convoluted:

      if (tz.tz_minuteswest >= 0) {
        off += snprintf(buf+off, 30-off, "+%02d:%02d", tz.tz_minuteswest/60, tz.tz_minuteswest%60);
      } else {
        off += snprintf(buf+off, 30-off, "-%02d:%02d", -tz.tz_minuteswest/60, -tz.tz_minuteswest%60);
      }
    

    Note that gettimeofday() is considered deprecated in favor of clock_gettime(), so the best way to do all this would be:

    #include <stdio.h> // printf
    #include <stdlib.h> // labs
    #include <sys/time.h>
    
    int main(){
      struct timespec tp;
      clock_gettime(CLOCK_REALTIME, &tp);
    
      char buf[sizeof "9999-12-31T23:59:59.999+00:00"]; // 29 chars + '\0'
      size_t bufsize = sizeof buf;
      int off = 0;
      struct tm *local = localtime(&tp.tv_sec);
      off = strftime(buf, bufsize, "%FT%T", local); // same as "%Y-%m-%dT%H:%M:%S" 
      off += snprintf(buf+off, bufsize-off, ".%03ld", tp.tv_nsec/1000000);
      off += snprintf(buf+off, bufsize-off, "%c%02ld:%02ld", local->tm_gmtoff >= 0 ? '+' : '-', labs(local->tm_gmtoff)/3600, labs(local->tm_gmtoff)%3600/60); 
    
      printf("using strftime: %s\n", buf);
    }
    

    The only changes are to use clock_gettime() instead of gettimeofday() and use the time zone information that localtime() provides in the struct tm as tm_gmtoff.

    gcc test.c -g -Wall -std=c11
    
    TZ="Europe/Madrid" ./a.out;
    using strftime: 2024-01-16T16:18:12.161+01:00
    
    TZ="America/Anchorage" ./a.out; 
    using strftime: 2024-01-16T06:18:12.173-09:00
    
    TZ="UTC" ./a.out
    using strftime: 2024-01-16T15:18:12.184+00:00
    
    TZ="Asia/Kolkata" ./a.out
    using strftime: 2024-01-16T23:38:31.766+05:30