2-digit year to 4 digit specifier in strptime

Viewed 64

I came across an interesting bug in our code recently. We parse time-stamps in the format %Y-%m-%dT%H:%M:%S using strptime()

A user entered a 2 digit year by mistake and a very wrong time-stamp was used.

I was surprised however to note that parsing 22 gives a year of -1878 for tm_year whereas if %y had been used we would get 122. Why is this?

According to the posix and C standard library specification %Y is a 4 digit year specifier. I cannot find any documentation anywhere that specifies what happens if less than 4 digits are provided. So I think it is in the murky land of undefined behaviour.

1 Answers

This is indeed undefined behaviour.

If you look at one (quite ugly) implementation of strptime here You see:

        case 'y':
    match_year_in_century:
      /* Match year within century.  */
      get_number (0, 99, 2);
      /* The "Year 2000: The Millennium Rollover" paper suggests that
         values in the range 69-99 refer to the twentieth century.  */
      tm->tm_year = val >= 69 ? val : val + 100;
      /* Indicate that we want to use the century, if specified.  */
      s.want_century = 1;
      s.want_xday = 1;
      break;
    case 'Y':
      /* Match year including century number.  */
      get_number (0, 9999, 4);
      tm->tm_year = val - 1900;
      s.want_century = 0;
      s.want_xday = 1;
      break;

So the year is read as a number from which 1900 subtracted. Years in struct tm count from 1900. Whereas if your time-format was %y you would have got a sensible answer of 22 -> 2022 because

When a century is not otherwise specified, values in the range [69,99] shall refer to years 1969 to 1999 inclusive, and values in the range [00,68] shall refer to years 2000 to 2068 inclusive;

Why doesn't %Y fall back to %y behaviour for years <100? Because it doesn't have to. It could but the wording of the standard doesn't require it.

So it would seem the safe thing to do when using strptime() is to check the number of digits before calling it and either reject 2-digit years or change the format specifier to %y if only 2-digits are provided.

Alternatively if you want it fall back to %y behaviour and you know your implementation behaves this way you could post process the year as in:

tm->tm_year = tm->tm_year < 0 ? tm->tm_year : tm->tm_year + 1900;
tm->tm_year = tm->tm_year >= 69 ? val : tm->tm_year + 100;

But then you are yourself relying on undefined behaviour. You should also reject 3-digit years somehow as these will most likely be typos.

Also note the comment:

It is expected that in a future version of IEEE Std 1003.1-2001 the default century inferred from a 2-digit year will change. (This would apply to all commands accepting a 2-digit year as input.)

Related