support for locales

Paul Eggert yata!eggert at twinsun.com
Sun May 8 19:13:16 UTC 1994


Here is a patch that adds support for locales to the tz package's
strftime function and date command.  For example, if the LC_TIME
environment variable is set to "de", `date' will output something like
`Montag,  9. Mai 1994, 02:59:54 Uhr PDT' instead of the usual
`Mon May  9 02:59:54 PDT 1994'.

The locale files are in /usr/lib/locale, using the same format as in
Solaris 2.3 (which I assume is fairly standard, at least among SVR4-ish
hosts).  If locale files are absent or are not of the proper form,
traditional behavior is assumed.  I invented a strftime format `%+',
which expands to the default format used by `date'; this makes it
easier to put all the locale-relevant stuff in strftime.c, and also
lets the user type fun commands like `date "+|%+|"'.

This patch also extends strftime to use the format "%c" if its format
argument is (char*)0; this is the documented behavior in Solaris 2.3.

This patch assumes the Posix/GCC cleanup patch I sent a while back.

===================================================================
RCS file: RCS/Makefile,v
retrieving revision 1994.7.1.1
retrieving revision 1994.7.1.2
diff -c -r1994.7.1.1 -r1994.7.1.2
*** Makefile	1994/05/05 19:10:21	1994.7.1.1
--- Makefile	1994/05/09 09:47:26	1994.7.1.2
***************
*** 82,87 ****
--- 82,88 ----
  #  -DHAVE_ADJTIME=0 if `adjtime' does not exist (SVR0?)
  #  -DHAVE_LONG_DOUBLE if your compiler supports the `long double' type
  #  -DHAVE_MKDIR=0 if the mkdir system call does not work (SVR0?)
+ #  -DHAVE_SETLOCALE=0 if the setlocale function does not work (SVR3)
  #  -DHAVE_SETTIMEOFDAY=0 if settimeofday does not exist (SVR0?)
  #  -DHAVE_SETTIMEOFDAY=1 if settimeofday has just 1 arg (SVR4)
  #  -DHAVE_SETTIMEOFDAY=2 if settimeofday uses 2nd arg (4.3BSD)
***************
*** 89,94 ****
--- 90,96 ----
  #  -DHAVE_STDLIB_H=0 if `#include <stdlib.h>' does not work
  #  -DHAVE_UMASK=0 if `umask(0)' does not work
  #  -DHAVE_UNISTD=0 if `#include <unistd.h>' does not work
+ #  -DLOCALE_HOME=\"path\" if locales are in "path", not "/usr/lib/locale"
  #  -DNEED_STRCHR_DECL=1 if <string.h> does not declare strchr fully
  #  -DNEED_TIME_DECL=1 if <time.h> does not declare `time' fully
  #  -Dalloc_size_t=T if the malloc size argument is of type T, not size_t
===================================================================
RCS file: RCS/date.1,v
retrieving revision 1994.7
retrieving revision 1994.7.1.1
diff -c -r1994.7 -r1994.7.1.1
*** date.1	1992/11/04 18:08:06	1994.7
--- date.1	1994/05/09 09:47:50	1994.7.1.1
***************
*** 37,42 ****
--- 37,43 ----
  (or by the abbreviation for the time zone specified in the
  .B TZ
  environment variable if set).
+ The exact output format depends on the locale.
  .PP
  If a command-line argument starts with a plus sign
  .RB (` + '),
***************
*** 51,70 ****
  to be output in a particular way
  (or identify a special character to output):
  .nf
  .if t .in +.5i
  .if n .in +2
! .ta \w'%M\0\0'u +\w'Wed Mar  8 14:54:40 1989\0\0'u
  	Sample output	Explanation
! %a	Wed	Abbreviated weekday name
! %A	Wednesday	Full weekday name
! %b	Mar	Abbreviated month name
! %B	March	Full month name
! %c	03/08/89 14:54:40	Month/day/year Hour:minute:second
! %C	Wed Mar  8 14:54:40 1989	a la \fIasctime\^\fP(3)
  %d	08	Day of month (always two digits)
  %D	03/08/89	Month/day/year (eight characters)
  %e	 8	Day of month (leading zero blanked)
! %h	Mar	Abbreviated month name
  %H	14	24-hour-clock hour (two digits)
  %I	02	12-hour-clock hour (two digits)
  %j	067	Julian day number (three digits)
--- 52,72 ----
  to be output in a particular way
  (or identify a special character to output):
  .nf
+ .sp
  .if t .in +.5i
  .if n .in +2
! .ta \w'%M\0\0'u +\w'Wed Mar  8 14:54:40 EST 1989\0\0'u
  	Sample output	Explanation
! %a	Wed	Abbreviated weekday name*
! %A	Wednesday	Full weekday name*
! %b	Mar	Abbreviated month name*
! %B	March	Full month name*
! %c	Wed Mar 08 14:54:40 1989	Date and time*
! %C	19	Century
  %d	08	Day of month (always two digits)
  %D	03/08/89	Month/day/year (eight characters)
  %e	 8	Day of month (leading zero blanked)
! %h	Mar	Abbreviated month name*
  %H	14	24-hour-clock hour (two digits)
  %I	02	12-hour-clock hour (two digits)
  %j	067	Julian day number (three digits)
***************
*** 82,94 ****
  %U	10	Sunday-based week number (two digits)
  %w	3	Day number (one digit, Sunday is 0)
  %W	10	Monday-based week number (two digits)
! %x	03/08/89	Month/day/year (eight characters)
! %X	14:54:40	Hour:minute:second
  %y	89	Last two digits of year
  %Y	1989	Year in full
  %Z	EST	Time zone abbreviation
  .if t .in -.5i
  .if n .in -2
  .fi
  If a character other than one of those shown above appears after
  a percent sign in the format,
--- 84,99 ----
  %U	10	Sunday-based week number (two digits)
  %w	3	Day number (one digit, Sunday is 0)
  %W	10	Monday-based week number (two digits)
! %x	03/08/89	Date*
! %X	14:54:40	Time*
  %y	89	Last two digits of year
  %Y	1989	Year in full
  %Z	EST	Time zone abbreviation
+ %+	Wed Mar  8 14:54:40 EST 1989	Default output format*
  .if t .in -.5i
  .if n .in -2
+ * The exact output depends on the locale.
+ .sp
  .fi
  If a character other than one of those shown above appears after
  a percent sign in the format,
***************
*** 150,153 ****
--- 155,175 ----
  On BSD-based systems,
  the adjustment is made by changing the rate at which time advances;
  on System-V-based systems, the adjustment is made by changing the time.
+ .SH FILES
+ .ta \w'/usr/local/etc/zoneinfo/posixrules\0\0'u
+ /usr/lib/locale/\f2L\fP/LC_TIME	description of time locale \f2L\fP
+ .br
+ /usr/local/etc/zoneinfo	time zone information directory
+ .br
+ /usr/local/etc/zoneinfo/localtime	local time zone file
+ .br
+ /usr/local/etc/zoneinfo/posixrules	used with POSIX-style TZ's
+ .br
+ /usr/local/etc/zoneinfo/GMT	for UTC leap seconds
+ .sp
+ If
+ .B /usr/local/etc/zoneinfo/GMT
+ is absent,
+ UTC leap seconds are loaded from
+ .BR /usr/local/etc/zoneinfo/posixrules .
  .\" @(#)date.1	7.2
===================================================================
RCS file: RCS/date.c,v
retrieving revision 1994.6.1.1
retrieving revision 1994.6.1.2
diff -c -r1994.6.1.1 -r1994.6.1.2
*** date.c	1994/05/05 16:24:49	1994.6.1.1
--- date.c	1994/05/09 09:47:28	1994.6.1.2
***************
*** 40,45 ****
--- 40,49 ----
  #endif
  #include "utmp.h"	/* for OLD_TIME (or its absence) */
  
+ #if HAVE_SETLOCALE
+ #	include "locale.h"
+ #endif
+ 
  /*
  ** The two things date knows about time are. . .
  */
***************
*** 485,495 ****
  
  	(void) time(&now);
  	tm = *localtime(&now);
! 	if (format == NULL) {
! 		timeout(stdout, "%a %b ", &tm);
! 		(void) printf("%2d ", tm.tm_mday);
! 		timeout(stdout, "%X %Z %Y", &tm);
! 	} else	timeout(stdout, format, &tm);
  	(void) putchar('\n');
  	(void) fflush(stdout);
  	(void) fflush(stderr);
--- 489,498 ----
  
  	(void) time(&now);
  	tm = *localtime(&now);
! #if HAVE_SETLOCALE
! 	setlocale(LC_TIME, "");
! #endif
! 	timeout(stdout, format ? format : "%+", &tm);
  	(void) putchar('\n');
  	(void) fflush(stdout);
  	(void) fflush(stderr);
===================================================================
RCS file: RCS/private.h,v
retrieving revision 1994.5.1.1
retrieving revision 1994.5.1.2
diff -c -r1994.5.1.1 -r1994.5.1.2
*** private.h	1994/04/03 02:54:41	1994.5.1.1
--- private.h	1994/05/09 09:47:28	1994.5.1.2
***************
*** 30,35 ****
--- 30,38 ----
  #ifndef HAVE_MKDIR
  #define HAVE_MKDIR 1
  #endif
+ #ifndef HAVE_SETLOCALE
+ #define HAVE_SETLOCALE 1
+ #endif
  #ifndef HAVE_SETTIMEOFDAY
  #define HAVE_SETTIMEOFDAY 3
  #endif
***************
*** 48,53 ****
--- 51,60 ----
  #endif
  #ifndef NEED_TIME_DECL
  #define NEED_TIME_DECL 0
+ #endif
+ 
+ #ifndef LOCALE_HOME
+ #define LOCALE_HOME "/usr/lib/locale"
  #endif
  
  /*
===================================================================
RCS file: RCS/strftime.c,v
retrieving revision 1994.6
retrieving revision 1994.6.1.1
diff -c -r1994.6 -r1994.6.1.1
*** strftime.c	1994/05/05 15:47:58	1994.6
--- strftime.c	1994/05/09 09:48:02	1994.6.1.1
***************
*** 3,10 ****
  static char	elsieid[] = "@(#)strftime.c	7.19";
  /*
  ** Based on the UCB version with the ID appearing below.
! ** This is ANSIish only when time is treated identically in all locales and
! ** when "multibyte character == plain character".
  */
  #endif /* !defined NOID */
  #endif /* !defined lint */
--- 3,9 ----
  static char	elsieid[] = "@(#)strftime.c	7.19";
  /*
  ** Based on the UCB version with the ID appearing below.
! ** This is ANSIish only when "multibyte character == plain character".
  */
  #endif /* !defined NOID */
  #endif /* !defined lint */
***************
*** 35,60 ****
  #endif /* !defined LIBC_SCCS */
  
  #include "tzfile.h"
  
! static const char afmt[][4] = {
! 	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
  };
! static const char Afmt[][10] = {
! 	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
! 	"Saturday"
! };
! static const char bfmt[][4] = {
! 	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
! 	"Oct", "Nov", "Dec"
! };
! static const char Bfmt[][10] = {
! 	"January", "February", "March", "April", "May", "June", "July",
! 	"August", "September", "October", "November", "December"
  };
  
  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 *));
  
  size_t strftime P((char *, size_t, const char *, const struct tm *));
  
--- 34,98 ----
  #endif /* !defined LIBC_SCCS */
  
  #include "tzfile.h"
+ #include "fcntl.h"
  
! struct lc_time {
! 	const char *mon[12];
! 	const char *month[12];
! 	const char *wday[7];
! 	const char *weekday[7];
! 	const char *X_fmt;
! 	const char *x_fmt;
! 	const char *c_fmt;
! 	const char *am;
! 	const char *pm;
! 	const char *date_fmt;
  };
! 
! static const struct lc_time C_time_locale = {
! 	{
! 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
! 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
! 	}, {
! 		"January", "February", "March", "April", "May", "June",
! 		"July", "August", "September", "October", "November", "December"
! 	}, {
! 		"Sun", "Mon", "Tue", "Wed",
! 		"Thu", "Fri", "Sat"
! 	}, {
! 		"Sunday", "Monday", "Tuesday", "Wednesday",
! 		"Thursday", "Friday", "Saturday"
! 	},
! 
! 	/* X_fmt */
! 	"%H:%M:%S",
! 
! 	/*
! 	** x_fmt
! 	** Since the C language standard calls for
! 	** "date, using locale's date format," anything goes.
! 	** Using just numbers (as here) makes Quakers happier;
! 	** it's also compatible with SVR4.
! 	*/
! 	"%m/%d/%y",
! 
! 	/* c_fmt */
! 	"%a %b %d %H:%M:%S %Y",
! 
! 	"AM", "PM",
! 
! 	/* date_fmt */
! 	"%a %b %e %H:%M:%S %Z %Y"
  };
  
+ #if HAVE_SETLOCALE
+ #	include <locale.h>
+ #endif
+ 
  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 *, struct lc_time *));
! static const struct lc_time *_loc P((struct lc_time *));
  
  size_t strftime P((char *, size_t, const char *, const struct tm *));
  
***************
*** 68,75 ****
  	const struct tm *t;
  {
  	char *p;
  
! 	p = _fmt(format, t, s, s + maxsize);
  	if (p == s + maxsize)
  		return 0;
  	*p = '\0';
--- 106,117 ----
  	const struct tm *t;
  {
  	char *p;
+ 	struct lc_time localebuf;
  
! 	if (!format)
! 		format = "%c";
! 	localebuf.mon[0] = 0;
! 	p = _fmt(format, t, s, s + maxsize, &localebuf);
  	if (p == s + maxsize)
  		return 0;
  	*p = '\0';
***************
*** 77,87 ****
  }
  
  static char *
! _fmt(format, t, pt, ptlim)
  	const char *format;
  	const struct tm *t;
  	char *pt;
  	const char *ptlim;
  {
  	for (; *format; ++format) {
  		if (*format == '%') {
--- 119,130 ----
  }
  
  static char *
! _fmt(format, t, pt, ptlim, locale)
  	const char *format;
  	const struct tm *t;
  	char *pt;
  	const char *ptlim;
+ 	struct lc_time *locale;
  {
  	for (; *format; ++format) {
  		if (*format == '%') {
***************
*** 90,114 ****
  			case '\0':
  				--format;
  				break;
  			case 'A':
  				pt = _add((t->tm_wday < 0 || t->tm_wday > 6) ?
! 					"?" : Afmt[t->tm_wday], pt, ptlim);
  				continue;
  			case 'a':
  				pt = _add((t->tm_wday < 0 || t->tm_wday > 6) ?
! 					"?" : afmt[t->tm_wday], pt, ptlim);
  				continue;
  			case 'B':
  				pt = _add((t->tm_mon < 0 || t->tm_mon > 11) ?
! 					"?" : Bfmt[t->tm_mon], pt, ptlim);
  				continue;
  			case 'b':
  			case 'h':
  				pt = _add((t->tm_mon < 0 || t->tm_mon > 11) ?
! 					"?" : bfmt[t->tm_mon], pt, ptlim);
  				continue;
  			case 'c':
! 				pt = _fmt("%D %X", t, pt, ptlim);
  				continue;
  			case 'C':
  				/*
--- 133,166 ----
  			case '\0':
  				--format;
  				break;
+ 			case '+':
+ 				pt = _fmt(_loc(locale)->date_fmt, t,
+ 					pt, ptlim, locale);
+ 				continue;
  			case 'A':
  				pt = _add((t->tm_wday < 0 || t->tm_wday > 6) ?
! 					"?" : _loc(locale)->weekday[t->tm_wday],
! 					pt, ptlim);
  				continue;
  			case 'a':
  				pt = _add((t->tm_wday < 0 || t->tm_wday > 6) ?
! 					"?" : _loc(locale)->wday[t->tm_wday],
! 					pt, ptlim);
  				continue;
  			case 'B':
  				pt = _add((t->tm_mon < 0 || t->tm_mon > 11) ?
! 					"?" : _loc(locale)->month[t->tm_mon],
! 					pt, ptlim);
  				continue;
  			case 'b':
  			case 'h':
  				pt = _add((t->tm_mon < 0 || t->tm_mon > 11) ?
! 					"?" : _loc(locale)->mon[t->tm_mon],
! 					pt, ptlim);
  				continue;
  			case 'c':
! 				pt = _fmt(_loc(locale)->c_fmt, t,
! 					pt, ptlim, locale);
  				continue;
  			case 'C':
  				/*
***************
*** 122,148 ****
  					"%02d", pt, ptlim);
  				continue;
  			case 'D':
! 				pt = _fmt("%m/%d/%y", t, pt, ptlim);
! 				continue;
! 			case 'x':
! 				/*
! 				** Version 3.0 of strftime from Arnold Robbins
! 				** (arnold at skeeve.atl.ga.us) does the
! 				** equivalent of...
! 				**	_fmt("%a %b %e %Y");
! 				** ...for %x; since the X3J11 C language
! 				** standard calls for "date, using locale's
! 				** date format," anything goes.  Using just
! 				** numbers (as here) makes Quakers happier.
! 				** Word from Paul Eggert (eggert at twinsun.com)
! 				** is that %Y-%m-%d is the ISO standard date
! 				** format, specified in ISO 2014 and later
! 				** ISO 8601:1988, with a summary available in
! 				** pub/doc/ISO/english/ISO8601.ps.Z on
! 				** ftp.uni-erlangen.de.
! 				** (ado, 5/30/93)
! 				*/
! 				pt = _fmt("%m/%d/%y", t, pt, ptlim);
  				continue;
  			case 'd':
  				pt = _conv(t->tm_mday, "%02d", pt, ptlim);
--- 174,180 ----
  					"%02d", pt, ptlim);
  				continue;
  			case 'D':
! 				pt = _fmt("%m/%d/%y", t, pt, ptlim, locale);
  				continue;
  			case 'd':
  				pt = _conv(t->tm_mday, "%02d", pt, ptlim);
***************
*** 224,240 ****
  					pt, ptlim);
  				continue;
  			case 'R':
! 				pt = _fmt("%H:%M", t, pt, ptlim);
  				continue;
  			case 'r':
! 				pt = _fmt("%I:%M:%S %p", t, pt, ptlim);
  				continue;
  			case 'S':
  				pt = _conv(t->tm_sec, "%02d", pt, ptlim);
  				continue;
  			case 'T':
! 			case 'X':
! 				pt = _fmt("%H:%M:%S", t, pt, ptlim);
  				continue;
  			case 't':
  				pt = _add("\t", pt, ptlim);
--- 256,271 ----
  					pt, ptlim);
  				continue;
  			case 'R':
! 				pt = _fmt("%H:%M", t, pt, ptlim, locale);
  				continue;
  			case 'r':
! 				pt = _fmt("%I:%M:%S %p", t, pt, ptlim, locale);
  				continue;
  			case 'S':
  				pt = _conv(t->tm_sec, "%02d", pt, ptlim);
  				continue;
  			case 'T':
! 				pt = _fmt("%H:%M:%S", t, pt, ptlim, locale);
  				continue;
  			case 't':
  				pt = _add("\t", pt, ptlim);
***************
*** 321,327 ****
  				** "date as dd-bbb-YYYY"
  				** (ado, 5/24/93)
  				*/
! 				pt = _fmt("%e-%b-%Y", t, pt, ptlim);
  				continue;
  			case 'W':
  				pt = _conv((t->tm_yday + 7 -
--- 352,358 ----
  				** "date as dd-bbb-YYYY"
  				** (ado, 5/24/93)
  				*/
! 				pt = _fmt("%e-%b-%Y", t, pt, ptlim, locale);
  				continue;
  			case 'W':
  				pt = _conv((t->tm_yday + 7 -
***************
*** 332,337 ****
--- 363,376 ----
  			case 'w':
  				pt = _conv(t->tm_wday, "%d", pt, ptlim);
  				continue;
+ 			case 'X':
+ 				pt = _fmt(_loc(locale)->X_fmt, t,
+ 					pt, ptlim, locale);
+ 				continue;
+ 			case 'x':
+ 				pt = _fmt(_loc(locale)->x_fmt, t,
+ 					pt, ptlim, locale);
+ 				continue;
  			case 'y':
  				pt = _conv((t->tm_year + TM_YEAR_BASE) % 100,
  					"%02d", pt, ptlim);
***************
*** 390,393 ****
--- 429,526 ----
  	while (pt < ptlim && (*pt = *str++) != '\0')
  		++pt;
  	return pt;
+ }
+ 
+ static const struct lc_time *
+ _loc(locale)
+ 	struct lc_time *locale;
+ {
+ 	static const char locale_home[] = LOCALE_HOME;
+ 	static const char lc_time[] = "LC_TIME";
+ 	static char *locale_buf;
+ 	static char locale_buf_C[] = "C";
+ 
+ 	int fd;
+ 	char *lbuf, *name, *p;
+ 	const char **ap, *plim;
+ 	char filename[FILENAME_MAX];
+ 	struct stat st;
+ 	size_t namesize, bufsize;
+ 
+ 	/* Use locale->mon[0] to signal whether locale is already set up.  */
+ 	if (locale->mon[0])
+ 		return locale;
+ 
+ #if HAVE_SETLOCALE
+ 	name = setlocale(LC_TIME, (char *)0);
+ #else
+ 	if (!(name = getenv("LC_ALL")) || !*name)
+ 		if (!(name = getenv(lc_time)) || !*name)
+ 			name = getenv("LANG");
+ #endif
+ 	if (!name || !*name)
+ 		goto no_locale;
+ 
+ 	/* If the locale name is the same as our cache, use the cache.  */
+ 	lbuf = locale_buf;
+ 	if (lbuf  &&  strcmp(name, lbuf) == 0) {
+ 		p = lbuf;
+ 		for (ap=(const char **)locale; ap<(const char **)(locale + 1); ap++)
+ 			*ap = p += strlen(p) + 1;
+ 		return locale;
+ 	}
+ 
+ 	/* Slurp the locale file into the cache.  */
+ 	namesize = strlen(name) + 1;
+ 	if (sizeof(filename) < sizeof(locale_home) + namesize + sizeof(lc_time))
+ 		goto no_locale;
+ 	sprintf(filename, "%s/%s/%s", locale_home, name, lc_time);
+ 	fd = open(filename, O_RDONLY, 0);
+ 	if (fd < 0)
+ 		goto no_locale;
+ 	if (fstat(fd, &st) != 0)
+ 		goto bad_locale;
+ 	if (st.st_size <= 0)
+ 		goto bad_locale;
+ 	bufsize = namesize + st.st_size;
+ 	locale_buf = 0;
+ 	lbuf =
+ 		!lbuf || lbuf==locale_buf_C
+ 		? malloc(bufsize)
+ 		: realloc(lbuf, bufsize);
+ 	if (!lbuf)
+ 		goto bad_locale;
+ 	strcpy(lbuf, name);
+ 	p = lbuf + namesize;
+ 	plim = p + st.st_size;
+ 	if (read(fd, p, (size_t)st.st_size) != st.st_size)
+ 		goto bad_lbuf;
+ 	if (close(fd) != 0)
+ 		goto bad_lbuf;
+ 
+ 	/* Parse the locale file into *locale.  */
+ 	if (plim[-1] != '\n')
+ 		goto bad_lbuf;
+ 	for (ap=(const char **)locale; ap<(const char **)(locale + 1); ap++) {
+ 		if (p == plim)
+ 			goto bad_lbuf;
+ 		*ap = p;
+ 		while (*p != '\n')
+ 			p++;
+ 		*p++ = 0;
+ 	}
+ 
+ 	/* Record the successful parse in the cache.  */
+ 	locale_buf = lbuf;
+ 
+ 	return locale;
+ 
+       bad_lbuf:
+ 	free(lbuf);
+       bad_locale:
+ 	(void) close(fd);
+       no_locale:
+ 	*locale = C_time_locale;
+ 	locale_buf = locale_buf_C;
+ 	return locale;
  }



More information about the tz mailing list