Subject: bin/6961: strftime(3) gives wrong week number (patch supplied)
To: None <gnats-bugs@gnats.netbsd.org>
From: Wolfgang Helbig <helbig@Informatik.BA-Stuttgart.DE>
List: netbsd-bugs
Date: 02/07/1999 17:53:48
>Number:         6961
>Category:       bin
>Synopsis:       strftime(3) gives wrong week number (patch supplied)
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    bin-bug-people (Utility Bug People)
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Sun Feb  7 09:05:00 1999
>Last-Modified:
>Originator:     Wolfgang Helbig
>Organization:
	
>Release:        NetBSD-current Fri Feb  5 18:29:06 CET 1999
>Environment:
System: NetBSD rvc1 1.3I NetBSD 1.3I (RVC1) #4: Sun Feb 7 10:59:47 CET 1999 helbig@rvc1:/usr/src/sys/arch/i386/compile/RVC1 i386


>Description:
	The %V conversion specifier produces a wrong week number, e. g.
	according to ISO 8601 the week number of 31 Dec 1991 is 1 but
	strftime says it is 53.
>How-To-Repeat:
	
>Fix:
	I merged in the algorithm used in the latest version of
	strftime from elsi.nci.nih.gov. It corrects the `%V'
	conversion specifier and provides two new ones, `%G' and
	`%g', which expand to the year which contains the greater
	part of the week. Following is a patcht to strftime.c and
	strftime.3 (two mention the new specifiers).
--- /usr/src/lib/libc/time/strftime.c	Wed Dec  2 13:11:34 1998
+++ strftime.c	Sun Feb  7 16:58:52 1999
@@ -227,32 +227,94 @@
 				    pt, ptlim))
 					return (0);
 				continue;
-			case 'V':
+			case 'V':	/* ISO 8601 week number */
+			case 'G':	/* ISO 8601 year (four digits) */
+			case 'g':	/* ISO 8601 year (two digits) */
+/*
+** From Arnold Robbins' strftime version 3.0:  "the week number of the
+** year (the first Monday as the first day of week 1) as a decimal number
+** (01-53)."
+** (ado, 1993-05-24)
+**
+** From "http://www.ft.uni-erlangen.de/~mskuhn/iso-time.html" by Markus Kuhn:
+** "Week 01 of a year is per definition the first week which has the
+** Thursday in this year, which is equivalent to the week which contains
+** the fourth day of January. In other words, the first week of a new year
+** is the week which has the majority of its days in the new year. Week 01
+** might also contain days from the previous year and the week before week
+** 01 of a year is the last week (52 or 53) of the previous year even if
+** it contains days from the new year. A week starts with Monday (day 1)
+** and ends with Sunday (day 7).  For example, the first week of the year
+** 1997 lasts from 1996-12-30 to 1997-01-05..."
+** (ado, 1996-01-02)
+*/
 				{
-				/* ISO 8601 Week Of Year:
-				 *  If the week (Monday - Sunday) containing
-				 * January 1 has four or more days in the new 
-				 * year, then it is week 1; otherwise it is 
-				 * week 53 of the previous year and the next
-				 * week is week one.
-				 */
+					int	year;
+					int	yday;
+					int	wday;
+					int	w;
 
-				int week = MON_WEEK(t);
+					year = t->tm_year + TM_YEAR_BASE;
+					yday = t->tm_yday;
+					wday = t->tm_wday;
+					for ( ; ; ) {
+						int	len;
+						int	bot;
+						int	top;
 
-				int days = (((t)->tm_yday + 7 -
-				    ((t)->tm_wday ? (t)->tm_wday - 1 : 6)) % 7);
-
-
-				if (days >= 4) {
-					week++;
-				} else if (week == 0) {
-					week = 53;
+						len = isleap(year) ?
+							DAYSPERLYEAR :
+							DAYSPERNYEAR;
+						/*
+						** What yday (-3 ... 3) does
+						** the ISO year begin on?
+						*/
+						bot = ((yday + 11 - wday) %
+							DAYSPERWEEK) - 3;
+						/*
+						** What yday does the NEXT
+						** ISO year begin on?
+						*/
+						top = bot -
+							(len % DAYSPERWEEK);
+						if (top < -3)
+							top += DAYSPERWEEK;
+						top += len;
+						if (yday >= top) {
+							++year;
+							w = 1;
+							break;
+						}
+						if (yday >= bot) {
+							w = 1 + ((yday - bot) /
+								DAYSPERWEEK);
+							break;
+						}
+						--year;
+						yday += isleap(year) ?
+							DAYSPERLYEAR :
+							DAYSPERNYEAR;
+					}
+#ifdef XPG4_1994_04_09
+					if ((w == 52
+					     && t->tm_mon == TM_JANUARY)
+					    || (w == 1
+						&& t->tm_mon == TM_DECEMBER))
+						w = 53;
+#endif /* defined XPG4_1994_04_09 */
+					if (*format == 'V') {
+						if (!_conv(w, 2, '0',
+							pt, ptlim))
+							return (0);
+					} else if (*format == 'g') {
+						if (!_conv(year % 100, 2, '0',
+							pt, ptlim))
+							return (0);
+					} else	if (!_conv(year, 4, '0',
+							pt, ptlim))
+							return (0);
 				}
-
-				if (!_conv(week, 2, '0', pt, ptlim))
-					return (0);
 				continue;
-				}
 			case 'W':
 				if (!_conv(MON_WEEK(t), 2, '0', pt, ptlim))
 					return (0);
--- /usr/src/lib/libc/time/strftime.3	Thu Feb 12 15:05:20 1998
+++ strftime.3	Sun Feb  7 17:31:27 1999
@@ -102,6 +102,16 @@
 .It Cm \&%e
 is replaced by the day of month as a decimal number [1,31];
 single digits are preceded by a blank.
+.It Cm \&%G
+is replaced by the ISO 8601 year with century as a decimal number.
+.TP
+.It Cm \&%g
+is replaced by the ISO 8601 year without century as a decimal number (00-99).
+This is the year that includes the greater part of the week. (Monday as the
+first day of a week). See also the
+.Ql \&%V
+conversion specification.
+.TP
 .It Cm \&%H
 is replaced by the hour (24-hour clock) as a decimal number [00,23].
 .It Cm \&%I
@@ -149,9 +159,12 @@
 as a decimal number [1,7].
 .It Cm \&%V
 is replaced by the week number of the year (Monday as the first day of
-the week) as a decimal number [01,53].  If the week containing January
-1 has four or more days in the new year, then it is week 1; otherwise
-it is week 53 of the previous year, and the next week is week 1.
+the week) as a decimal number [01,53]. According to ISO 8601 the week
+containing January 1 is week 1 if it has four or more days in the new year,
+otherwise it is week 53 of the previous year, and the next week is week 1.
+The year is given by the
+.Ql \&%G
+conversion specification.
 .It Cm \&%W
 is replaced by the week number of the year (Monday as the first day of
 the week) as a decimal number [00,53].
@@ -188,6 +201,8 @@
 .Ql \&%C ,
 .Ql \&%D ,
 .Ql \&%e ,
+.Ql \&%g ,
+.Ql \&%G ,
 .Ql \&%h ,
 .Ql \&%k ,
 .Ql \&%l , 
>Audit-Trail:
>Unformatted: