Strftime's %C and %y formats versus wide-ranging tm_year values

Paul Eggert eggert at CS.UCLA.EDU
Wed Oct 6 23:12:46 UTC 2004


OK, here's a proposed patch to the tz code.  It is relative to the
latest published code on elsie.  It incorporates most of Arthur's
patch circulated yesterday, except it addresses the issues I mentioned
with C99IPMOD, with floating point, and with integer overflow
checking; the resulting code uses int arithmetic instead of assuming
that long and double are wider than int.

===================================================================
RCS file: RCS/tzfile.h,v
retrieving revision 1997.9
retrieving revision 1997.9.0.3
diff -pu -r1997.9 -r1997.9.0.3
--- tzfile.h	1997/12/29 14:31:51	1997.9
+++ tzfile.h	2004/10/06 22:25:15	1997.9.0.3
@@ -21,7 +21,7 @@
 
 #ifndef lint
 #ifndef NOID
-static char	tzfilehid[] = "@(#)tzfile.h	7.14";
+static char	tzfilehid[] = "@(#)tzfile.h	7.15";
 #endif /* !defined NOID */
 #endif /* !defined lint */
 
@@ -156,12 +156,21 @@ struct tzhead {
 #define EPOCH_YEAR	1970
 #define EPOCH_WDAY	TM_THURSDAY
 
+#define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
+
 /*
-** Accurate only for the past couple of centuries;
-** that will probably do.
+** 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(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
+#define isleap_sum(a, b)	isleap((a) % 400 + (b) % 400)
 
 #ifndef USG
 
===================================================================
RCS file: RCS/asctime.c,v
retrieving revision 2004.3
retrieving revision 2004.3.0.2
diff -pu -r2004.3 -r2004.3.0.2
--- asctime.c	2004/08/11 15:59:06	2004.3
+++ asctime.c	2004/10/06 23:03:13	2004.3.0.2
@@ -5,7 +5,7 @@
 
 #ifndef lint
 #ifndef NOID
-static char	elsieid[] = "@(#)asctime.c	7.22";
+static char	elsieid[] = "@(#)asctime.c	7.23";
 #endif /* !defined NOID */
 #endif /* !defined lint */
 
@@ -15,8 +15,8 @@ static char	elsieid[] = "@(#)asctime.c	7
 #include "tzfile.h"
 
 #if STRICTLY_STANDARD_ASCTIME
-#define ASCTIME_FMT	"%.3s %.3s%3d %.2d:%.2d:%.2d %ld\n"
-#define ASCTIME_FMT_B	ASCTIME_FMT
+#define ASCTIME_FMT	"%.3s %.3s%3d %.2d:%.2d:%.2d %d\n"
+#define ASCTIME_FMT_B	"%.3s %.3s%3d %.2d:%.2d:%.2d %d%d\n"
 #else /* !STRICTLY_STANDARD_ASCTIME */
 /*
 ** Some systems only handle "%.2d"; others only handle "%02d";
@@ -31,14 +31,14 @@ static char	elsieid[] = "@(#)asctime.c	7
 ** 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"
+#define ASCTIME_FMT	"%.3s %.3s%3d %02.2d:%02.2d:%02.2d %-4d\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
 ** 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"
+#define ASCTIME_FMT_B	"%.3s %.3s%3d %02.2d:%02.2d:%02.2d     %d%d\n"
 #endif /* !STRICTLY_STANDARD_ASCTIME */
 
 #define STD_ASCTIME_BUF_SIZE	26
@@ -74,7 +74,8 @@ char *				buf;
 	};
 	register const char *	wn;
 	register const char *	mn;
-	long			year;
+	int			decade;
+	int			y;
 	char			result[MAX_ASCTIME_BUF_SIZE];
 
 	if (timeptr->tm_wday < 0 || timeptr->tm_wday >= DAYSPERWEEK)
@@ -83,16 +84,34 @@ char *				buf;
 	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,
-		year);
+	if (-999 - TM_YEAR_BASE <= timeptr->tm_year
+	    && timeptr->tm_year <= 9999 - TM_YEAR_BASE) {
+		(void) sprintf(result,
+			ASCTIME_FMT,
+			wn, mn,
+			timeptr->tm_mday, timeptr->tm_hour,
+			timeptr->tm_min, timeptr->tm_sec,
+			timeptr->tm_year + TM_YEAR_BASE);
+	} else {
+		decade = timeptr->tm_year / 10 + TM_YEAR_BASE / 10;
+		y = timeptr->tm_year % 10;
+		if (y < 0 && 0 < decade) {
+			y += 10;
+			decade--;
+		} else if (decade < 0 && 0 < y) {
+			y -= 10;
+			decade++;
+		}
+		(void) sprintf(result,
+			ASCTIME_FMT_B,
+			wn, mn,
+			timeptr->tm_mday, timeptr->tm_hour,
+			timeptr->tm_min, timeptr->tm_sec,
+			decade, y < 0 ? -y : y);
+	}
 	if (strlen(result) < STD_ASCTIME_BUF_SIZE || buf == buf_asctime) {
 		(void) strcpy(buf, result);
 		return buf;
===================================================================
RCS file: RCS/strftime.c,v
retrieving revision 2004.4
retrieving revision 2004.4.0.4
diff -pu -r2004.4 -r2004.4.0.4
--- strftime.c	2004/09/09 15:48:53	2004.4
+++ strftime.c	2004/10/06 23:03:13	2004.4.0.4
@@ -1,12 +1,6 @@
-/*
-** 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";
+static char	elsieid[] = "@(#)strftime.c	7.70";
 /*
 ** Based on the UCB version with the ID appearing below.
 ** This is ANSIish only when "multibyte character == plain character".
@@ -114,10 +108,8 @@ static const struct lc_time_T	C_time_loc
 
 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 *));
+static char *	_yconv P((int, int, int, int, char *, const char *));
 
 extern char *	tzname[];
 
@@ -125,7 +117,6 @@ extern char *	tzname[];
 #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
@@ -217,9 +208,8 @@ label:
 				** something completely different.
 				** (ado, 1993-05-24)
 				*/
-				pt = _conv((int) ((t->tm_year +
-					(long) TM_YEAR_BASE) / 100),
-					"%02d", pt, ptlim);
+				pt = _yconv(t->tm_year, TM_YEAR_BASE, 1, 0,
+					    pt, ptlim);
 				continue;
 			case 'c':
 				{
@@ -387,13 +377,14 @@ label:
 ** (ado, 1996-01-02)
 */
 				{
-					long	year;
+					int	year;
+					int	base;
 					int	yday;
 					int	wday;
 					int	w;
 
 					year = t->tm_year;
-					year += TM_YEAR_BASE;
+					base = TM_YEAR_BASE;
 					yday = t->tm_yday;
 					wday = t->tm_wday;
 					for ( ; ; ) {
@@ -401,7 +392,7 @@ label:
 						int	bot;
 						int	top;
 
-						len = isleap(year) ?
+						len = isleap_sum(year, base) ?
 							DAYSPERLYEAR :
 							DAYSPERNYEAR;
 						/*
@@ -420,7 +411,7 @@ label:
 							top += DAYSPERWEEK;
 						top += len;
 						if (yday >= top) {
-							++year;
+							++base;
 							w = 1;
 							break;
 						}
@@ -429,8 +420,8 @@ label:
 								DAYSPERWEEK);
 							break;
 						}
-						--year;
-						yday += isleap(year) ?
+						--base;
+						yday += isleap_sum(year, base) ?
 							DAYSPERLYEAR :
 							DAYSPERNYEAR;
 					}
@@ -444,12 +435,12 @@ label:
 					if (*format == 'V')
 						pt = _conv(w, "%02d",
 							pt, ptlim);
-					else if (*format == 'g') {
-						*warnp = IN_ALL;
-						pt = _conv(int(year % 100),
-							"%02d", pt, ptlim);
-					} else	pt = _lconv(year, "%04ld",
-							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':
@@ -484,15 +475,14 @@ label:
 					*warnp = warn2;
 				}
 				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);
+  			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,9 +546,10 @@ label:
 					diff = -diff;
 				} else	sign = "+";
 				pt = _add(sign, pt, ptlim);
-				diff /= 60;
-				pt = _conv((diff/60)*100 + diff%60,
-					"%04d", pt, ptlim);
+				diff /= SECSPERMIN;
+				diff = (diff / MINSPERHOUR) * 100 +
+					(diff % MINSPERHOUR);
+				pt = _conv(diff, "%04d", pt, ptlim);
 				}
 				continue;
 			case '+':
@@ -596,26 +587,59 @@ const char * const	ptlim;
 }
 
 static char *
-_lconv(n, format, pt, ptlim)
-const long		n;
-const char * const	format;
-char * const		pt;
+_add(str, pt, ptlim)
+const char *		str;
+char *			pt;
 const char * const	ptlim;
 {
-	char	buf[INT_STRLEN_MAXIMUM(long) + 1];
-
-	(void) sprintf(buf, format, n);
-	return _add(buf, pt, ptlim);
+	while (pt < ptlim && (*pt = *str++) != '\0')
+		++pt;
+	return pt;
 }
 
 static char *
-_add(str, pt, ptlim)
-const char *		str;
+_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;
 {
-	while (pt < ptlim && (*pt = *str++) != '\0')
-		++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.
+	*/
+
+	int	top, yy;
+
+	yy = a % 100 + b % 100;
+	top = a / 100 + b / 100 + yy / 100;
+	yy %= 100;
+
+	if (yy < 0 && 0 < top) {
+		yy += 100;
+		top--;
+	} else if (top < 0 && 0 < yy) {
+		yy -= 100;
+		top++;
+	}
+
+	if (convert_top) {
+		if (top == 0 && yy < 0) {
+			pt = _add("-0", pt, ptlim);
+		} else {
+			pt = _conv(top, "%02d", pt, ptlim);
+		}
+	}
+
+	if (convert_yy) {
+		pt = _conv(yy < 0 ? -yy : yy, "%02d", pt, ptlim);
+	}
+
 	return pt;
 }
 
===================================================================
RCS file: RCS/zdump.c,v
retrieving revision 2004.4
retrieving revision 2004.4.0.3
diff -pu -r2004.4 -r2004.4.0.3
--- zdump.c	2004/09/06 20:00:46	2004.4
+++ zdump.c	2004/10/06 23:03:13	2004.4.0.3
@@ -1,4 +1,4 @@
-static char	elsieid[] = "@(#)zdump.c	7.40";
+static char	elsieid[] = "@(#)zdump.c	7.41";
 
 /*
 ** This code has been made independent of the rest of the time
@@ -61,9 +61,25 @@ static char	elsieid[] = "@(#)zdump.c	7.4
 #endif /* !defined DAYSPERNYEAR */
 
 #ifndef isleap
-#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0)
+#define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
 #endif /* !defined isleap */
 
+#ifndef isleap_sum
+/*
+** 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)
+#endif /* !defined isleap_sum */
+
 #if HAVE_GETTEXT
 #include "locale.h"	/* for setlocale */
 #include "libintl.h"
@@ -321,7 +337,7 @@ struct tm *	oldp;
 		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 += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE);
 	result += newp->tm_yday - oldp->tm_yday;
 	result *= HOURSPERDAY;
 	result += newp->tm_hour - oldp->tm_hour;
@@ -384,6 +400,8 @@ register const struct tm *	timeptr;
 	};
 	register const char *	wn;
 	register const char *	mn;
+	int			decade;
+	int			y;
 
 	/*
 	** The packaged versions of localtime and gmtime never put out-of-range
@@ -398,9 +416,18 @@ register const struct tm *	timeptr;
 		(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",
+	decade = timeptr->tm_year / 10 + TM_YEAR_BASE / 10;
+	y = timeptr->tm_year % 10;
+	if (y < 0 && 0 < decade) {
+		y += 10;
+		decade--;
+	} else if (decade < 0 && 0 < y) {
+		y -= 10;
+		decade++;
+	}
+	(void) printf("%.3s %.3s%3d %.2d:%.2d:%.2d %s%03d%d",
 		wn, mn,
 		timeptr->tm_mday, timeptr->tm_hour,
 		timeptr->tm_min, timeptr->tm_sec,
-		timeptr->tm_year + (long) TM_YEAR_BASE);
+		decade == 0 && y < 0 ? "-" : "", decade, y < 0 ? -y : y);
 }



More information about the tz mailing list