strftime %y and negative years

Paul Eggert eggert at CS.UCLA.EDU
Fri Aug 20 19:14:30 UTC 2004


"Clive D.W. Feather" <clive at demon.net> writes:

> Note, by the way, that strftime is only supposed to work when the relevant
> fields are in their "normal range". No such range is given for tm_year.

I intepret this to mean that strftime is supposed to work regardless
of the value of tm_year.

However, the standard says that %C always generates a value in the
range [00,99] so it would appear there's an inconsistency here.  I
suppose one could argue that %C has undefined behavior for years
outside the range [0, 9999].  But this appears to me to be a defect in
the standard -- at least, things are quite unclear here.  I would
prefer it if strftime were required to handle all tm_year values.

There is no similar restriction on the range for %Y, which suggests
that strftime %Y must handle all tm_year values.

For %y the range is [00,99], which argues for using modulus rather
than remainder.


> It would be interesting to see what they do with %C as well:

Solaris interprets %C completely differently: it treats it as a
request to output the same string that the "date" command outputs by
default.  The strftime man page says that there is a
"standard-conforming" strftime somewhere but doesn't say how to get
it.  I couldn't figure it out so I gave up looking for it.


> The glibc and OpenBSD behaviours appear to be using the % operator.

glibc uses %, but adjusts negative remainders to make them positive,
so that it's actually using modulus.  I think OpenBSD uses plain %.


> As for Solaris, my best guess is that it's calculating:
>     '0' + tm_year / 10 % 10
>     '0' + tm_year % 10

Yes, that sounds plausible, as Unix Version 7 does something similar.
Solaris also mishandles %Y for negative and/or large years.  For
example, strftime %Y prints the year -1 as "000/", and prints the year
2**31 (i.e., tm_year == 2**31 - 1900) as "-*,(".  This is consistent
with your theory.

Here's a test program you can use to try out your implementation.
It's not strictly conforming code (it relies on floating point) but it
should work on all practical platforms.  Only glibc "passes" the test,
in then sense that it produces a coherent set of values for all inputs
(it always uses modulus for %y, and for %C it always truncates towards
minus infinity).  Solaris botches %C entirely, and mishandles %y for
years before 1900, mishandles %Y for years before 0.  OpenBSD uses
signed remainder for negative years, though I'd argue that having %y
generate "-" is bogus.  OpenBSD and Solaris both clearly mishandle
tm_year values close to INT_MAX.

#include <string.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static void
process (int tm_year)
{
  struct tm tm;
  char y[1000];
  char C[1000];
  char Y[1000];
  tm.tm_year = tm_year;
  strftime (y + 1, sizeof y - 2, "%y", &tm); y[0] = '"'; strcat (y, "\"");
  strftime (C + 1, sizeof C - 2, "%C", &tm); C[0] = '"'; strcat (C, "\"");
  strftime (Y + 1, sizeof Y - 2, "%Y", &tm); Y[0] = '"'; strcat (Y, "\"");
  printf ("%13d %13.0f %13s %13s %13s\n",
	  tm_year, tm_year + 1900.0, y, C, Y);
}

int main (int argc, char **argv)
{
  printf ("%13s %13s %13s %13s %13s\n", "tm_year", "year", "%y", "%C", "%Y");
  if (argc <= 1)
    {
      #define near(x) (x) - 1900, (x) - 1900 + 1, (x) - 1900 + 2
      static int test[] =
	{
	  near (INT_MIN + 1900),
	  near (-1001), near (-101), near (-11), near (-1),
	  near (9), near (99), near (999),
	  near (1899), near (1969), near (1999), near (2099),
	  near (INT_MAX - 1), near (INT_MAX + 1900.0 - 2)
	};
      int i;
      for (i = 0; i < sizeof test / sizeof *test; i++)
	{
	  if (i == 0 || test[i - 1] + 1 != test[i])
	    printf ("\n");
	  process (test[i]);
	}
    }
  else
    while (*++argv)
      process (atoi (*argv));
  return 0;
} 



More information about the tz mailing list