Reasonable snprintf-like alternatives to strftime?
Asked Answered
G

1

8

Are there any standard (C, C++, POSIX, Linux...) alternatives to strftime which can be used to

  1. calculate the required size of the string buffer for a given format and time,
  2. truncate the output (instead of leaving the array contents undefined like strftime does) if buffer size is less than required for full output.

For example, snprintf-like semantics for date/time formatting which accept strftime format strings would do perfectly.

Functions like std::put_time in C++11 and later are not an option, because these may attempt to allocate extra memory dynamically and may throw exceptions.

Griseous answered 6/7, 2017 at 10:37 Comment(9)
Assume that someone will update your code in the next 7983 years, then count the number of bytes you need to format the time on your fingers. Doesn't really seem necessary to do it automatically. Just avoid long week and month names since they are locale dependent.Remunerative
No, there isn't but you can write your own relatively easily. The stdarg.h and vsnprintf can be helpful to achieve that.Volscian
@Volscian Well, it might be not so easy when it comes to weekday and month names according to current locale etc. stdarg and vsnprintf will not be necessary for the replacement, since strftime has a fixed number of arguments.Gounod
Why is dynamic memory allocation a problem? It would be easy: Allocate a buffer, try the strftime() if it fails, increase (double) the buffer size and iterate. You can even use a VLA on the stack if you want to avoid malloc()&coGounod
Yes, 512 bytes should be long enough in any language I can think of.Reede
@MichaëlRoy That depends on your format string, for each size you tell I can easily create one for you that exceeds it.Gounod
64k would not even make a dent in your heapReede
I would never do that sort of thing, personally. But I have worked with people who tried, so you have a point there. His triple 1MB buffers allocated on the stack didn't do too well on embedded controllers, though.Reede
If ISO 8601 style is OK, then you can use something like this #9528460. Then the buffer size will not change.Muggins
A
2

Could keep trying larger buffers until code succeeds (or decides this is too much). The below uses a VLA (not C++), to sneakily avoid "attempt to allocate extra memory dynamically" - wink wink.

Simply allocating a large buffer, say char buf[J_STRFTIME_MAX], should be sufficient for practical coding. @Michaël Roy and avoid the iterative approach.

#include <stdio.h>
#include <time.h>
#define J_STRFTIME_MAX 100

size_t j_strftime(char * s, size_t maxsize, const char * fmt, const struct tm * t) {
  size_t sz = strftime(s, maxsize, fmt, t);
  if (sz) {
    return sz;
  }
  size_t new_size = maxsize ? maxsize : 1;
  do {
    new_size *= 2;
    char new_s[new_size];
    sz = strftime(new_s, sizeof new_s, fmt, t);
    if (sz) {
      s[0] = 0;
      // strncat(s, new_s, maxsize);
      strncat(s, new_s, maxsize - 1);
      return strlen(new_s);
    }
  } while (sz < J_STRFTIME_MAX/2);
  return 0;
}

int main() {
  time_t now;
  time(&now);
  struct tm tm = *gmtime(&now);
  for (size_t i = 1; i < 30; i += 3) {
    char s[i];
    size_t sz = j_strftime(s, sizeof s, "%c", &tm);
    printf("%2zu %2zu <%s>\n", i, sz, s);
  }
}

Output

 1 24 <T>
 4 24 <Thu >
 7 24 <Thu Jul>
10 24 <Thu Jul  6>
13 24 <Thu Jul  6 14>
16 24 <Thu Jul  6 14:45>
19 24 <Thu Jul  6 14:45:00>
22 24 <Thu Jul  6 14:45:00 20>
25 24 <Thu Jul  6 14:45:00 2017>
28 24 <Thu Jul  6 14:45:00 2017>

Non-iterative

size_t j_strftime2(char * s, size_t maxsize, const char * fmt, const struct tm * t) {
  size_t sz = strftime(s, maxsize, fmt, t);
  if (sz == 0) {
    char new_s[J_STRFTIME_MAX];
    sz = strftime(new_s, sizeof new_s, fmt, t);
    if (sz == 0) {
      return 0;  // Too too big
    }
    s[0] = 0;
    // strncat(s, new_s, maxsize);
    strncat(s, new_s, maxsize - 1);
  }
  return sz;
}

[Edit] Code corrected.

Artistic answered 6/7, 2017 at 14:37 Comment(9)
To avoid unnecessary iterations, you could start with a larger value of new_size: size_t new_size = maxsize > 128 ? maxsize : 128;Pondweed
strftime could fail for other reasons besides the destination buffer size, your approach would definitely have undefined behavior in this case.Pondweed
@Pondweed True. A nice direct approach. Yet OP seemed to have an implied concern about memory usage, so the iterative approach creeps up.Artistic
Why not return sz in all cases, for the same semantics as snprintf? The user would allocate the minimum size in at most 2 steps (at the caller's level).Pondweed
@Pondweed Agree about returning the size needed (not including \0). Code amended.Artistic
@Pondweed The "other reasons" for failure are not detectable by the return value and as I see it are all problems with strftime() itself. What is a sample failure of concern?Artistic
an invalid specifier, an invalid tm structure, a multibyte encoding error... You can handle these by setting an arbitrary maximum for new_size and return 0 if it is reached.Pondweed
@Pondweed The "avoid unnecessary iterations" is handled when the user provides an ample sized buffer, which would be the usual expected code flow. OP's unusual requirement seems to be geared toward exceptional cases.Artistic
@Pondweed " an invalid specifier" --> UB. C11 §7.27.3.5 6. For tm with "values is outside the normal range", a prior call to the caller to gmtime(),locatime() can handle that. Returning from strftime() anything special under the 3 conditions you mentioned is not specified, so I do not see "handle these by setting an arbitrary maximum" as sufficient.Artistic

© 2022 - 2024 — McMap. All rights reserved.