From: Jason Green To: djgpp-workers AT delorie DOT com Cc: Esa A E Peuha Subject: strftime patch Date: Sun, 14 Jan 2001 09:45:49 +0000 Message-ID: X-Mailer: Forte Agent 1.7/32.534 MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 8bit X-MIME-Autoconverted: from quoted-printable to 8bit by delorie.com id EAA06631 Reply-To: djgpp-workers AT delorie DOT com Attached below are two patches to make strftime() compliant with latest ISO C standard. The first patch fixes or adds support for the following conversion specifiers: %F, %g, %G, %V & %z. The first patch also adds %P and %s for compatibility with the GNU C library. The second patch modifies behaviour of %c, %p & %x, to be compliant with ISO C using the "C" locale. I have not read up about locales so I am assuming that behaviour in DJGPP should be according to the "C" locale. This patch is separate because the changes introduce slight incompatibility with glibc2. (Applying only the first patch gives an exact match here, using the test program attached below.) I will make a patch to the docs when I know how much of the changes are accepted and in what form. Thanks go to Esa A E Peuha for writing the code to calculate ISO 8601 week and year numbers. I have used the glibc2 man page and the last public draft of ISO C as references. Any inaccuracies in these documents will be reflected in the code. So I think someone with a copy of the final standard should compare strftime() definition with the one I used. gcc 2.95.2 emits a warning when strftime() is used with %G, this happens both in DJGPP and under Linux. So I think it would be good if someone with latest gcc sources installed could compile the example test program below to see if the problem still exists and/or look at the source for comments to understand if this is an issue of concern. #include int main (void) { char str[10]; time_t t; struct tm *tm_ptr; time(&t); tm_ptr = localtime(&t); strftime (str, sizeof (str), "%G", tm_ptr); return 0; } $ gcc -Wall -pedantic percentg.c percentg.c: In function `main': percentg.c:9: warning: ANSI C does not support `%G' -------------------------------------------------------------------- Here is a program to generally test strftime() function: #include #include int main (void) { const char *p, *testspec = "aAbBcCdDeFgGhHIjklmMnpPrRsStTuUVwWxXyYzZ"; char str[80], fmt[10]; time_t t = 987654321; struct tm *tm_ptr = gmtime(&t); for (p = testspec; *p; p++) { sprintf(fmt, "%%%c", *p); strftime (str, sizeof (str), fmt, tm_ptr); printf("%s = \"%s\"\n", fmt, str); } return 0; } -------------------------------------------------------------------- Here are heavily snipped excerpts from the last public draft of ISO C and the glibc2 man page: From n869.txt: [#3] Each conversion specifier is replaced by appropriate characters as described in the following list... %F is equivalent to ``%Y-%m-%d'' (the ISO 8601 date format). [tm_year, tm_mon, tm_mday] %g is replaced by the last 2 digits of the week-based year (see below) as a decimal number (00-99). [tm_year, tm_wday, tm_yday] %G is replaced by the week-based year (see below) as a decimal number (e.g., 1997). [tm_year, tm_wday, tm_yday] %p is replaced by the locale's equivalent of the AM/PM designations associated with a 12-hour clock. [tm_hour] %V is replaced by the ISO 8601 week number (see below) as a decimal number (01-53). [tm_year, tm_wday, tm_yday] %z is replaced by the offset from UTC in the ISO 8601 format ``-0430'' (meaning 4 hours 30 minutes behind UTC, west of Greenwich), or by no characters if no time zone is determinable. [tm_isdst] [#4] Some conversion specifiers can be modified by the | inclusion of an E or O modifier character to indicate an | alternative format or specification. If the alternative format or specification does not exist for the current locale, the modifier is ignored. [#7] In the "C" locale, the E and O modifiers are ignored and the replacement strings for the following specifiers are: %c equivalent to ``%A %B %d %T %Y''. %p one of ``am'' or ``pm''. %x equivalent to ``%A %B %d %Y''. Differences found in the glibc2 man page: %s The number of seconds since the Epoch, i.e., since 1970-01-01 00:00:00 UTC. (TZ) %p Either `AM' or `PM' according to the given time value, or the corresponding strings for the current locale. Noon is treated as `pm' and midnight as `am'. %P Like %p but in lowercase: `am' or `pm' or a corre sponding string for the current locale. (GNU) -------------------------------------------------------------------- Here are the patches libc: --- src/libc/ansi/time/strftime.c.orig Thu Jun 3 13:27:34 1999 +++ src/libc/ansi/time/strftime.c Sat Jan 13 17:57:06 2001 @@ -1,3 +1,5 @@ +/* Copyright (C) 2001 DJ Delorie, see COPYING.DJ for details */ +/* Copyright (C) 2000 DJ Delorie, see COPYING.DJ for details */ /* Copyright (C) 1999 DJ Delorie, see COPYING.DJ for details */ /* Copyright (C) 1998 DJ Delorie, see COPYING.DJ for details */ /* Copyright (C) 1994 DJ Delorie, see COPYING.DJ for details */ @@ -7,6 +9,11 @@ #define TM_YEAR_BASE 1900 +/* Leap year calculation */ +#define isleap(y) \ + ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0) +#define days_in_year(y) ((isleap(y))? 366:365) + static const char *afmt[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", }; @@ -41,9 +48,9 @@ } static int -_conv(int n, int digits, char pad) +_conv(unsigned int n, int digits, char pad) { - static char buf[10]; + static char buf[12]; char *p = buf + sizeof(buf) - 2; do { @@ -58,14 +65,73 @@ return _add(++p, 0); } +/* number_of_week: Helper function to iso_week_num() +*/ +static int +number_of_week (int wday, int yday) +{ + int day_of_week_now = (wday + 6) % 7; + int day_of_week_jan1 = (day_of_week_now + 53 * 7 - yday) % 7; + int days_since_monday_of_first_week = yday + day_of_week_jan1 - 7 + * (day_of_week_jan1 >= 4); + + return days_since_monday_of_first_week / 7 + 1; +} + +/* iso_year: Calculates an adjustment for tm_year to account for + ISO-8601 dates as used by %V, %g and %G. Returns -1, 0, or 1, + which can be added to tm_year to determine the ISO-8601 year. +*/ +static int +iso_year_adjust (const struct tm *t) +{ + if (t->tm_mon == 11 && + ((t->tm_wday == 1 && t->tm_mday >= 29) || + (t->tm_wday == 2 && t->tm_mday >= 30) || + (t->tm_wday == 3 && t->tm_mday == 31))) + /* day belongs to first week of next year */ + return 1; + else if (t->tm_mon == 0 && + ((t->tm_wday == 5 && t->tm_mday == 1) || + (t->tm_wday == 6 && t->tm_mday <= 2) || + (t->tm_wday == 0 && t->tm_mday <= 3))) + /* day belongs to last week of previous year */ + return -1; + else + /* the usual case */ + return 0; +} + +/* iso_week_num: Returns the ISO-8601 week number as required by %V. +*/ +static int +iso_week_num (const struct tm *t) +{ + switch (iso_year_adjust (t)) + { + case 1: + return 1; + case -1: + /* day belongs to last week of previous year */ + return number_of_week (t->tm_wday, t->tm_yday + + days_in_year (t->tm_year + TM_YEAR_BASE - 1)); + default: + /* the usual case */ + return number_of_week (t->tm_wday, t->tm_yday); + } +} + static size_t _fmt(const char *format, const struct tm *t, int upcase) { + long gmtoffset, gmtoffset_mins, gmtoffset_hrs; + struct tm t_copy; + for (; *format; ++format) { if (*format == '%') { - int pad = '0', space=' '; + int pad = '0', space=' ', modifier = 0; if (format[1] == '_') pad = space = ' ', format++; if (format[1] == '-') @@ -74,6 +140,11 @@ pad = space = '0', format++; if (format[1] == '^') upcase = 1, format++; + /* 'E' and 'O' should modify the specifier to indicate + an alternate format according to the current locale, + this is currently unsupported by DJGPP. */ + if ((format[1] == 'E') || (format[1] == 'O')) + modifier = format[1], format++; switch(*++format) { @@ -125,6 +196,20 @@ if (!_conv(t->tm_mday, 2, pad)) return 0; continue; + case 'F': + if (!_fmt("%Y-%m-%d", t, upcase)) + return 0; + continue; + case 'g': + if (!_conv((t->tm_year + iso_year_adjust(t) + TM_YEAR_BASE) + % 100, 2, pad)) + return 0; + continue; + case 'G': + if (!_conv(t->tm_year + iso_year_adjust(t) + TM_YEAR_BASE, + 4, pad)) + return 0; + continue; case 'H': if (!_conv(t->tm_hour, 2, pad)) return 0; @@ -159,6 +244,10 @@ if (!_add("\n", upcase)) return 0; continue; + case 'P': + if (!_add(t->tm_hour >= 12 ? "pm" : "am", upcase)) + return 0; + continue; case 'p': if (!_add(t->tm_hour >= 12 ? "PM" : "AM", upcase)) return 0; @@ -175,6 +264,11 @@ if (!_conv(t->tm_sec, 2, pad)) return 0; continue; + case 's': /* There must be a more efficient way to do this. */ + memcpy(&t_copy, t, sizeof(t_copy)); + if (!_conv(mktime(&t_copy), 0, 0)) + return 0; + continue; case 'T': case 'X': if (!_fmt("%H:%M:%S", t, upcase)) @@ -193,6 +287,10 @@ 2, pad)) return 0; continue; + case 'V': + if (!_conv(iso_week_num(t), 2, pad)) + return 0; + continue; case 'W': if (!_conv((t->tm_yday + 7 - (t->tm_wday ? (t->tm_wday - 1) : 6)) @@ -208,20 +306,31 @@ return 0; continue; case 'y': - case 'g': if (!_conv((t->tm_year + TM_YEAR_BASE) % 100, 2, pad)) return 0; continue; case 'Y': - case 'G': if (!_conv(t->tm_year + TM_YEAR_BASE, 4, pad)) return 0; continue; case 'z': - if (!_add(t->__tm_gmtoff<0 ? "-" : "+", 0)) - return 0; - if (!_conv(t->__tm_gmtoff, 4, pad)) - return 0; + gmtoffset_mins = (t->__tm_gmtoff / 60) % 60; + gmtoffset_hrs = t->__tm_gmtoff / 3600; + gmtoffset = (gmtoffset_hrs * 100) + gmtoffset_mins; + if (gmtoffset<0) + { + if (!_add("-", 0)) + return 0; + if (!_conv(-gmtoffset, 4, pad)) + return 0; + } + else + { + if (!_add("+", 0)) + return 0; + if (!_conv(gmtoffset, 4, pad)) + return 0; + } continue; case 'Z': if (!t->tm_zone || !_add(t->tm_zone, upcase)) --- src/libc/ansi/time/strftime.c.bak Sat Jan 13 17:57:06 2001 +++ src/libc/ansi/time/strftime.c Sat Jan 13 18:00:40 2001 @@ -181,7 +181,7 @@ return 0; continue; case 'c': - if (!_fmt("%a %b %e %H:%M:%S %Y", t, upcase)) + if (!_fmt("%A %B %d %T %Y", t, upcase)) return 0; continue; case 'e': @@ -249,7 +249,7 @@ return 0; continue; case 'p': - if (!_add(t->tm_hour >= 12 ? "PM" : "AM", upcase)) + if (!_add(t->tm_hour >= 12 ? "pm" : "am", upcase)) return 0; continue; case 'R': @@ -302,7 +302,7 @@ return 0; continue; case 'x': - if (!_fmt("%m/%d/%y", t, upcase)) + if (!_fmt("%A %B %d %Y", t, upcase)) return 0; continue; case 'y': -- [please cc replies if possible]