Below find the latest iteration on handling wide-ranging tm_year values. 1. The asctime function punts to strftime to generate output representing the year; the asctime documentation has been adjusted accordingly; a comment based on Paul Eggert's suggestion has been added. 2. zdump.c and strftime.c have been changed along the lines proposed by Paul Eggert; I've tried to make the code as similar as possible in the two places. I've stayed with the "% 400" version of isleap_sum since it's correct and relatively simple (if not efficient). I'd suggest not worrying about this too much. I don't know of any real-world applications where code relying on isleap is a bottleneck. I also foresee that with the extension of the range of time_t to the distant past, the days of being able to determine leap-year status with a macro are numbered. This round of changes does not split tzfile.h into caldefs.h and tzfile.h. --ado diff -c -r old/asctime.c new/asctime.c *** old/asctime.c Mon Oct 11 14:47:03 2004 --- new/asctime.c Thu Oct 14 13:36:37 2004 *************** *** 5,11 **** #ifndef lint #ifndef NOID ! static char elsieid[] = "@(#)asctime.c 7.22"; #endif /* !defined NOID */ #endif /* !defined lint */ --- 5,11 ---- #ifndef lint #ifndef NOID ! static char elsieid[] = "@(#)asctime.c 7.26"; #endif /* !defined NOID */ #endif /* !defined lint */ *************** *** 15,21 **** #include "tzfile.h" #if STRICTLY_STANDARD_ASCTIME ! #define ASCTIME_FMT "%.3s %.3s%3d %.2d:%.2d:%.2d %ld\n" #define ASCTIME_FMT_B ASCTIME_FMT #else /* !STRICTLY_STANDARD_ASCTIME */ /* --- 15,21 ---- #include "tzfile.h" #if STRICTLY_STANDARD_ASCTIME ! #define ASCTIME_FMT "%.3s %.3s%3d %.2d:%.2d:%.2d %s\n" #define ASCTIME_FMT_B ASCTIME_FMT #else /* !STRICTLY_STANDARD_ASCTIME */ /* *************** *** 29,37 **** ** Vintage programs are coded for years that are always four digits long ** and may assume that the newline always lands in the same place. ** For years that are less than four digits, we pad the output with ! ** spaces before the newline to get the newline in the traditional place. */ ! #define ASCTIME_FMT "%.3s %.3s%3d %02.2d:%02.2d:%02.2d %-4ld\n" /* ** For years that are more than four digits we put extra spaces before the year ** so that code trying to overwrite the newline won't end up overwriting --- 29,37 ---- ** Vintage programs are coded for years that are always four digits long ** and may assume that the newline always lands in the same place. ** For years that are less than four digits, we pad the output with ! ** leading zeroes to get the newline in the traditional place. */ ! #define ASCTIME_FMT "%.3s %.3s%3d %02.2d:%02.2d:%02.2d %-4s\n" /* ** For years that are more than four digits we put extra spaces before the year ** so that code trying to overwrite the newline won't end up overwriting *************** *** 38,44 **** ** a digit within a year and truncating the year (operating on the assumption ** that no output is better than wrong output). */ ! #define ASCTIME_FMT_B "%.3s %.3s%3d %02.2d:%02.2d:%02.2d %ld\n" #endif /* !STRICTLY_STANDARD_ASCTIME */ #define STD_ASCTIME_BUF_SIZE 26 --- 38,44 ---- ** a digit within a year and truncating the year (operating on the assumption ** that no output is better than wrong output). */ ! #define ASCTIME_FMT_B "%.3s %.3s%3d %02.2d:%02.2d:%02.2d %s\n" #endif /* !STRICTLY_STANDARD_ASCTIME */ #define STD_ASCTIME_BUF_SIZE 26 *************** *** 74,80 **** }; register const char * wn; register const char * mn; ! long year; char result[MAX_ASCTIME_BUF_SIZE]; if (timeptr->tm_wday < 0 || timeptr->tm_wday >= DAYSPERWEEK) --- 74,80 ---- }; register const char * wn; register const char * mn; ! char year[INT_STRLEN_MAXIMUM(int) + 2]; char result[MAX_ASCTIME_BUF_SIZE]; if (timeptr->tm_wday < 0 || timeptr->tm_wday >= DAYSPERWEEK) *************** *** 83,94 **** if (timeptr->tm_mon < 0 || timeptr->tm_mon >= MONSPERYEAR) mn = "???"; else mn = mon_name[timeptr->tm_mon]; - year = timeptr->tm_year + (long) TM_YEAR_BASE; /* ** We avoid using snprintf since it's not available on all systems. */ ! (void) sprintf(result, ! ((year >= -999 && year <= 9999) ? ASCTIME_FMT : ASCTIME_FMT_B), wn, mn, timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, --- 83,100 ---- if (timeptr->tm_mon < 0 || timeptr->tm_mon >= MONSPERYEAR) mn = "???"; else mn = mon_name[timeptr->tm_mon]; /* + ** Use strftime's %Y to generate the year, to avoid overflow problems + ** when computing timeptr->tm_year + TM_YEAR_BASE. + ** Assume that strftime is unaffected by other out-of-range members + ** (e.g., timeptr->tm_mday) when processing "%Y". + */ + (void) strftime(year, sizeof year, "%Y", timeptr); + /* ** We avoid using snprintf since it's not available on all systems. */ ! (void) sprintf(result, ! ((strlen(year) <= 4) ? ASCTIME_FMT : ASCTIME_FMT_B), wn, mn, timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, diff -c -r old/newctime.3 new/newctime.3 *** old/newctime.3 Mon Oct 11 14:44:49 2004 --- new/newctime.3 Thu Oct 14 13:00:48 2004 *************** *** 46,52 **** Thu Nov 24 18:22:48 1986\n\0 .br .ec ! Years requiring fewer than four characters are padded with trailing spaces. For years longer than four characters, the string is of the form .br .ce --- 46,52 ---- Thu Nov 24 18:22:48 1986\n\0 .br .ec ! Years requiring fewer than four characters are padded with leading zeroes. For years longer than four characters, the string is of the form .br .ce *************** *** 55,61 **** .ec .br with five spaces before the year. ! This unusual format is designed to make it less likely that older software that expects exactly 26 bytes of output will mistakenly output misleading values for out-of-range years. .PP --- 55,61 ---- .ec .br with five spaces before the year. ! These unusual formats are designed to make it less likely that older software that expects exactly 26 bytes of output will mistakenly output misleading values for out-of-range years. .PP *************** *** 65,71 **** return pointers to ``tm'' structures, described below. .I Localtime\^ corrects for the time zone and any time zone adjustments ! (such as Daylight Saving Time in the U.S.A.). After filling in the ``tm'' structure, .I localtime sets the --- 65,71 ---- return pointers to ``tm'' structures, described below. .I Localtime\^ corrects for the time zone and any time zone adjustments ! (such as Daylight Saving Time in the United States). After filling in the ``tm'' structure, .I localtime sets the *************** *** 234,237 **** Avoid using out-of-range values with .I mktime when setting up lunch with promptness sticklers in Riyadh. ! .\" @(#)newctime.3 7.16 --- 234,237 ---- Avoid using out-of-range values with .I mktime when setting up lunch with promptness sticklers in Riyadh. ! .\" @(#)newctime.3 7.17 diff -c -r old/strftime.c new/strftime.c *** old/strftime.c Mon Oct 11 14:46:51 2004 --- new/strftime.c Thu Oct 14 13:36:37 2004 *************** *** 1,12 **** - /* - ** XXX To do: figure out correct (as distinct from standard-mandated) - ** output for "two digits of year" and "century" formats when - ** the year is negative or less than 100. --ado, 2004-09-09 - */ - #ifndef lint #ifndef NOID ! static char elsieid[] = "@(#)strftime.c 7.67"; /* ** Based on the UCB version with the ID appearing below. ** This is ANSIish only when "multibyte character == plain character". --- 1,6 ---- #ifndef lint #ifndef NOID ! static char elsieid[] = "@(#)strftime.c 7.71"; /* ** Based on the UCB version with the ID appearing below. ** This is ANSIish only when "multibyte character == plain character". *************** *** 114,124 **** static char * _add P((const char *, char *, const char *)); static char * _conv P((int, const char *, char *, const char *)); - static char * _lconv P((long, const char *, char *, const char *)); static char * _fmt P((const char *, const struct tm *, char *, const char *, int *)); - size_t strftime P((char *, size_t, const char *, const struct tm *)); - extern char * tzname[]; #ifndef YEAR_2000_NAME --- 108,116 ---- static char * _add P((const char *, char *, const char *)); static char * _conv P((int, const char *, char *, const char *)); static char * _fmt P((const char *, const struct tm *, char *, const char *, int *)); + static char * _yconv P((int, int, int, int, char *, const char *)); extern char * tzname[]; #ifndef YEAR_2000_NAME *************** *** 125,131 **** #define YEAR_2000_NAME "CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS" #endif /* !defined YEAR_2000_NAME */ - #define IN_NONE 0 #define IN_SOME 1 #define IN_THIS 2 --- 117,122 ---- *************** *** 217,225 **** ** something completely different. ** (ado, 1993-05-24) */ ! pt = _conv((int) ((t->tm_year + ! (long) TM_YEAR_BASE) / 100), ! "%02d", pt, ptlim); continue; case 'c': { --- 208,215 ---- ** something completely different. ** (ado, 1993-05-24) */ ! pt = _yconv(t->tm_year, TM_YEAR_BASE, 1, 0, ! pt, ptlim); continue; case 'c': { *************** *** 387,399 **** ** (ado, 1996-01-02) */ { ! long year; int yday; int wday; int w; year = t->tm_year; ! year += TM_YEAR_BASE; yday = t->tm_yday; wday = t->tm_wday; for ( ; ; ) { --- 377,390 ---- ** (ado, 1996-01-02) */ { ! int year; ! int base; int yday; int wday; int w; year = t->tm_year; ! base = TM_YEAR_BASE; yday = t->tm_yday; wday = t->tm_wday; for ( ; ; ) { *************** *** 401,407 **** int bot; int top; ! len = isleap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; /* --- 392,398 ---- int bot; int top; ! len = isleap_sum(year, base) ? DAYSPERLYEAR : DAYSPERNYEAR; /* *************** *** 420,426 **** top += DAYSPERWEEK; top += len; if (yday >= top) { ! ++year; w = 1; break; } --- 411,417 ---- top += DAYSPERWEEK; top += len; if (yday >= top) { ! ++base; w = 1; break; } *************** *** 429,436 **** DAYSPERWEEK); break; } ! --year; ! yday += isleap(year) ? DAYSPERLYEAR : DAYSPERNYEAR; } --- 420,427 ---- DAYSPERWEEK); break; } ! --base; ! yday += isleap_sum(year, base) ? DAYSPERLYEAR : DAYSPERNYEAR; } *************** *** 446,455 **** pt, ptlim); else if (*format == 'g') { *warnp = IN_ALL; ! pt = _conv(int(year % 100), ! "%02d", pt, ptlim); ! } else pt = _lconv(year, "%04ld", pt, ptlim); } continue; case 'v': --- 437,446 ---- pt, ptlim); else if (*format == 'g') { *warnp = IN_ALL; ! pt = _yconv(year, base, 0, 1, pt, ptlim); + } else pt = _yconv(year, base, 1, 1, + pt, ptlim); } continue; case 'v': *************** *** 486,498 **** continue; case 'y': *warnp = IN_ALL; ! pt = _conv((int) ((t->tm_year + ! (long) TM_YEAR_BASE) % 100), ! "%02d", pt, ptlim); continue; case 'Y': ! pt = _lconv(t->tm_year + (long) TM_YEAR_BASE, ! "%04ld", pt, ptlim); continue; case 'Z': #ifdef TM_ZONE --- 477,488 ---- continue; case 'y': *warnp = IN_ALL; ! pt = _yconv(t->tm_year, TM_YEAR_BASE, 0, 1, ! pt, ptlim); continue; case 'Y': ! pt = _yconv(t->tm_year, TM_YEAR_BASE, 1, 1, ! pt, ptlim); continue; case 'Z': #ifdef TM_ZONE *************** *** 556,564 **** diff = -diff; } else sign = "+"; pt = _add(sign, pt, ptlim); ! diff /= 60; ! pt = _conv((diff/60)*100 + diff%60, ! "%04d", pt, ptlim); } continue; case '+': --- 546,555 ---- diff = -diff; } else sign = "+"; pt = _add(sign, pt, ptlim); ! diff /= SECSPERMIN; ! diff = (diff / MINSPERHOUR) * 100 + ! (diff % MINSPERHOUR); ! pt = _conv(diff, "%04d", pt, ptlim); } continue; case '+': *************** *** 596,614 **** } static char * - _lconv(n, format, pt, ptlim) - const long n; - const char * const format; - char * const pt; - const char * const ptlim; - { - char buf[INT_STRLEN_MAXIMUM(long) + 1]; - - (void) sprintf(buf, format, n); - return _add(buf, pt, ptlim); - } - - static char * _add(str, pt, ptlim) const char * str; char * pt; --- 587,592 ---- *************** *** 619,624 **** --- 597,641 ---- return pt; } + /* + ** POSIX and the C Standard are unclear or inconsistent about + ** what %C and %y do if the year is negative or exceeds 9999. + ** Use the convention that %C concatenated with %y yields the + ** same output as %Y, and that %Y contains at least 4 bytes, + ** with more only if necessary. + */ + + static char * + _yconv(a, b, convert_top, convert_yy, pt, ptlim) + const int a; + const int b; + const int convert_top; + const int convert_yy; + char * pt; + const char * const ptlim; + { + register int lead; + register int trail; + + #define DIVISOR 100 + lead = a / DIVISOR + b / DIVISOR; + trail = a % DIVISOR + b % DIVISOR; + while (trail < 0) { + trail += DIVISOR; + --lead; + } + if (lead < 0 && trail != 0) { + trail -= DIVISOR; + ++lead; + } + if (convert_top) + if (lead == 0 && trail < 0) + pt = _add("-0", pt, ptlim); + else pt = _conv(lead, "%02d", pt, ptlim); + if (convert_yy) + pt = _conv(((trail < 0) ? -trail : trail), "%02d", pt, ptlim); + } + #ifdef LOCALE_HOME static struct lc_time_T * _loc P((void)) diff -c -r old/tzfile.h new/tzfile.h *** old/tzfile.h Mon Oct 11 14:46:42 2004 --- new/tzfile.h Mon Oct 11 15:20:25 2004 *************** *** 21,27 **** #ifndef lint #ifndef NOID ! static char tzfilehid[] = "@(#)tzfile.h 7.14"; #endif /* !defined NOID */ #endif /* !defined lint */ --- 21,27 ---- #ifndef lint #ifndef NOID ! static char tzfilehid[] = "@(#)tzfile.h 7.16"; #endif /* !defined NOID */ #endif /* !defined lint */ *************** *** 156,167 **** #define EPOCH_YEAR 1970 #define EPOCH_WDAY TM_THURSDAY /* ! ** Accurate only for the past couple of centuries; ! ** that will probably do. */ ! #define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0)) #ifndef USG --- 156,176 ---- #define EPOCH_YEAR 1970 #define EPOCH_WDAY TM_THURSDAY + #define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0)) + /* ! ** Since everything in isleap is modulo 400 (or a factor of 400), we know that ! ** isleap(y) == isleap(y % 400) ! ** and so ! ** isleap(a + b) == isleap((a + b) % 400) ! ** or ! ** isleap(a + b) == isleap(a % 400 + b % 400) ! ** This is true even if % means modulo rather than Fortran remainder ! ** (which is allowed by C89 but not C99). ! ** We use this to avoid addition overflow problems. */ ! #define isleap_sum(a, b) isleap((a) % 400 + (b) % 400) #ifndef USG diff -c -r old/zdump.c new/zdump.c *** old/zdump.c Mon Oct 11 14:46:47 2004 --- new/zdump.c Thu Oct 14 13:00:49 2004 *************** *** 1,4 **** ! static char elsieid[] = "@(#)zdump.c 7.40"; /* ** This code has been made independent of the rest of the time --- 1,4 ---- ! static char elsieid[] = "@(#)zdump.c 7.43"; /* ** This code has been made independent of the rest of the time *************** *** 61,69 **** #endif /* !defined DAYSPERNYEAR */ #ifndef isleap ! #define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0) #endif /* !defined isleap */ #if HAVE_GETTEXT #include "locale.h" /* for setlocale */ #include "libintl.h" --- 61,76 ---- #endif /* !defined DAYSPERNYEAR */ #ifndef isleap ! #define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0)) #endif /* !defined isleap */ + #ifndef isleap_sum + /* + ** See tzfile.h for details on isleap_sum. + */ + #define isleap_sum(a, b) isleap((a) % 400 + (b) % 400) + #endif /* !defined isleap_sum */ + #if HAVE_GETTEXT #include "locale.h" /* for setlocale */ #include "libintl.h" *************** *** 321,327 **** return -delta(oldp, newp); result = 0; for (tmy = oldp->tm_year; tmy < newp->tm_year; ++tmy) ! result += DAYSPERNYEAR + isleap(tmy + (long) TM_YEAR_BASE); result += newp->tm_yday - oldp->tm_yday; result *= HOURSPERDAY; result += newp->tm_hour - oldp->tm_hour; --- 328,334 ---- return -delta(oldp, newp); result = 0; for (tmy = oldp->tm_year; tmy < newp->tm_year; ++tmy) ! result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE); result += newp->tm_yday - oldp->tm_yday; result *= HOURSPERDAY; result += newp->tm_hour - oldp->tm_hour; *************** *** 384,389 **** --- 391,398 ---- }; register const char * wn; register const char * mn; + int lead; + int trail; /* ** The packaged versions of localtime and gmtime never put out-of-range *************** *** 398,406 **** (int) (sizeof mon_name / sizeof mon_name[0])) mn = "???"; else mn = mon_name[timeptr->tm_mon]; ! (void) printf("%.3s %.3s%3d %.2d:%.2d:%.2d %ld", wn, mn, timeptr->tm_mday, timeptr->tm_hour, ! timeptr->tm_min, timeptr->tm_sec, ! timeptr->tm_year + (long) TM_YEAR_BASE); } --- 407,428 ---- (int) (sizeof mon_name / sizeof mon_name[0])) mn = "???"; else mn = mon_name[timeptr->tm_mon]; ! (void) printf("%.3s %.3s%3d %.2d:%.2d:%.2d ", wn, mn, timeptr->tm_mday, timeptr->tm_hour, ! timeptr->tm_min, timeptr->tm_sec); ! #define DIVISOR 10 ! lead = timeptr->tm_year / DIVISOR + TM_YEAR_BASE / DIVISOR; ! trail = timeptr->tm_year % DIVISOR + TM_YEAR_BASE % DIVISOR; ! while (trail < 0) { ! trail += DIVISOR; ! --lead; ! } ! if (lead < 0 && trail != 0) { ! trail -= DIVISOR; ! ++lead; ! } ! if (lead == 0) ! (void) printf("%d", trail); ! else (void) printf("%d%d", lead, ((trail < 0) ? -trail : trail)); }