Update from GNU libc.
authorJim Meyering <jim@meyering.net>
Tue, 7 May 1996 03:41:17 +0000 (03:41 +0000)
committerJim Meyering <jim@meyering.net>
Tue, 7 May 1996 03:41:17 +0000 (03:41 +0000)
lib/strftime.c

index 2b8f360..16a9bb2 100644 (file)
-/* strftime - custom formatting of date and/or time
-   Copyright (C) 1989, 1991, 1992 Free Software Foundation, Inc.
-
-   This program is free software; you can redistribute it and/or modify
-   it under the terms of the GNU General Public License as published by
-   the Free Software Foundation; either version 2, or (at your option)
-   any later version.
-
-   This program is distributed in the hope that it will be useful,
-   but WITHOUT ANY WARRANTY; without even the implied warranty of
-   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-   GNU General Public License for more details.
-
-   You should have received a copy of the GNU General Public License
-   along with this program; if not, write to the Free Software
-   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
-
-/* Note: this version of strftime lacks locale support,
-   but it is standalone.
-
-   Performs `%' substitutions similar to those in printf.  Except
-   where noted, substituted fields have a fixed size; numeric fields are
-   padded if necessary.  Padding is with zeros by default; for fields
-   that display a single number, padding can be changed or inhibited by
-   following the `%' with one of the modifiers described below.  Unknown
-   field specifiers are copied as normal characters.  All other
-   characters are copied to the output without change.
-
-   Supports a superset of the ANSI C field specifiers.
-
-   Literal character fields:
-   %   %
-   n   newline
-   t   tab
-
-   Numeric modifiers (a nonstandard extension):
-   -   do not pad the field
-   _   pad the field with spaces
-
-   Time fields:
-   %H  hour (00..23)
-   %I  hour (01..12)
-   %k  hour ( 0..23)
-   %l  hour ( 1..12)
-   %M  minute (00..59)
-   %p  locale's AM or PM
-   %r  time, 12-hour (hh:mm:ss [AP]M)
-   %R  time, 24-hour (hh:mm)
-   %s  time in seconds since 00:00:00, Jan 1, 1970 (a nonstandard extension)
-   %S  second (00..61)
-   %T  time, 24-hour (hh:mm:ss)
-   %X  locale's time representation (%H:%M:%S)
-   %z   RFC-822 style numeric timezone (-0500) (a nonstandard extension)
-   %Z  time zone (EDT), or nothing if no time zone is determinable
-
-   Date fields:
-   %a  locale's abbreviated weekday name (Sun..Sat)
-   %A  locale's full weekday name, variable length (Sunday..Saturday)
-   %b  locale's abbreviated month name (Jan..Dec)
-   %B  locale's full month name, variable length (January..December)
-   %c  locale's date and time (Sat Nov 04 12:02:33 EST 1989)
-   %C  century (00..99)
-   %d  day of month (01..31)
-   %e  day of month ( 1..31)
-   %D  date (mm/dd/yy)
-   %h  same as %b
-   %j  day of year (001..366)
-   %m  month (01..12)
-   %U  week number of year with Sunday as first day of week (00..53)
-   %V  FIXME
-   %w  day of week (0..6)
-   %W  week number of year with Monday as first day of week (00..53)
-   %x  locale's date representation (mm/dd/yy)
-   %y  last two digits of year (00..99)
-   %Y  year (1970...)
-
-   David MacKenzie <djm@gnu.ai.mit.edu> */
+/* Copyright (C) 1991, 92, 93, 94, 95, 96 Free Software Foundation, Inc.
+
+NOTE: The canonical source of this file is maintained with the GNU C Library.
+Bugs can be reported to bug-glibc@prep.ai.mit.edu.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; either version 2, or (at your option) any
+later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+USA.  */
 
 #ifdef HAVE_CONFIG_H
-#include <config.h>
+# include <config.h>
+#endif
+
+#ifdef _LIBC
+# define HAVE_LIMITS_H 1
+# define HAVE_MBLEN 1
+# define HAVE_TM_ZONE 1
+# define STDC_HEADERS 1
+# include <ansidecl.h>
+# include "../locale/localeinfo.h"
 #endif
 
 #include <stdio.h>
-#include <sys/types.h>
-#if defined(TM_IN_SYS_TIME) || (!defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME))
-#include <sys/time.h>
+#include <sys/types.h>         /* Some systems define `time_t' here.  */
+
+#ifdef TIME_WITH_SYS_TIME
+# include <sys/time.h>
+# include <time.h>
 #else
-#include <time.h>
+# ifdef HAVE_SYS_TIME_H
+#  include <sys/time.h>
+# else
+#  include <time.h>
+# endif
 #endif
 
-#ifndef STDC_HEADERS
-time_t mktime ();
+#if HAVE_MBLEN
+# include <ctype.h>
 #endif
 
-#if defined(HAVE_TZNAME)
-extern char *tzname[2];
+#if HAVE_LIMITS_H
+# include <limits.h>
 #endif
 
-/* Types of padding for numbers in date and time. */
-enum padding
-{
-  none, blank, zero
-};
-
-static char const* const days[] =
-{
-  "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
-};
-
-static char const * const months[] =
-{
-  "January", "February", "March", "April", "May", "June",
-  "July", "August", "September", "October", "November", "December"
-};
-
-/* Add character C to STRING and increment LENGTH,
-   unless LENGTH would exceed MAX. */
-
-#define add_char(c)                                                    \
-  do                                                                   \
-    {                                                                  \
-      if (length + 1 <= max)                                           \
-       string[length++] = (c);                                         \
-    }                                                                  \
-  while (0)
-
-/* Add a 2 digit number to STRING, padding if specified.
-   Return the number of characters added, up to MAX. */
-
-static int
-add_num2 (string, num, max, pad)
-     char *string;
-     int num;
-     int max;
-     enum padding pad;
-{
-  int top = num / 10;
-  int length = 0;
-
-  if (top == 0 && pad == blank)
-    add_char (' ');
-  else if (top != 0 || pad == zero)
-    add_char (top + '0');
-  add_char (num % 10 + '0');
-  return length;
-}
+#if STDC_HEADERS
+# include <stddef.h>
+# include <stdlib.h>
+# include <string.h>
+#else
+# define memcpy(d, s, n) bcopy (s, d, n)
+#endif
 
-/* Add a 3 digit number to STRING, padding if specified.
-   Return the number of characters added, up to MAX. */
+#ifndef __P
+#if defined (__GNUC__) || (defined (__STDC__) && __STDC__)
+#define __P(args) args
+#else
+#define __P(args) ()
+#endif  /* GCC.  */
+#endif  /* Not __P.  */
 
-static int
-add_num3 (string, num, max, pad)
-     char *string;
-     int num;
-     int max;
-     enum padding pad;
-{
-  int top = num / 100;
-  int mid = (num - top * 100) / 10;
-  int length = 0;
-
-  if (top == 0 && pad == blank)
-    add_char (' ');
-  else if (top != 0 || pad == zero)
-    add_char (top + '0');
-  if (mid == 0 && top == 0 && pad == blank)
-    add_char (' ');
-  else if (mid != 0 || top != 0 || pad == zero)
-    add_char (mid + '0');
-  add_char (num % 10 + '0');
-  return length;
-}
+#ifndef PTR
+#ifdef __STDC__
+#define PTR void *
+#else
+#define PTR char *
+#endif
+#endif
 
-/* Like strncpy except return the number of characters copied. */
+static unsigned int week __P((const struct tm *const, int, int));
+
+
+#define        add(n, f)                                                             \
+  do                                                                         \
+    {                                                                        \
+      i += (n);                                                                      \
+      if (i >= maxsize)                                                              \
+       return 0;                                                             \
+      else                                                                   \
+       if (p)                                                                \
+         {                                                                   \
+           f;                                                                \
+           p += (n);                                                         \
+         }                                                                   \
+    } while (0)
+#define        cpy(n, s)       add((n), memcpy((PTR) p, (PTR) (s), (n)))
+
+#ifdef _LIBC
+#define        fmt(n, args)    add((n), if (sprintf args != (n)) return 0)
+#else
+#define        fmt(n, args)    add((n), sprintf args; if (strlen (p) != (n)) return 0)
+#endif
 
-static int
-add_str (to, from, max)
-     char *to;
-     const char *from;
-     int max;
-{
-  int i;
 
-  for (i = 0; from[i] && i <= max; ++i)
-    to[i] = from[i];
-  return i;
-}
 
-static int
-add_num_time_t (string, max, num)
-     char *string;
-     int max;
-     time_t num;
+/* Return the week in the year specified by TP,
+   with weeks starting on STARTING_DAY.  */
+#ifdef __GNUC__
+inline
+#endif
+static unsigned int
+week (tp, starting_day, max_preceding)
+      const struct tm *const tp;
+      int starting_day;
+      int max_preceding;
 {
-  /* This buffer is large enough to hold the character representation
-     (including the trailing NUL) of any unsigned decimal quantity
-     whose binary representation fits in 128 bits.  */
-  char buf[40];
-  int length;
-
-  if (sizeof (num) > 16)
-    abort ();
-  sprintf (buf, "%lu", (unsigned long) num);
-  length = add_str (string, buf, max);
-  return length;
+  int wday, dl, base;
+
+  wday = tp->tm_wday - starting_day;
+  if (wday < 0)
+    wday += 7;
+
+  /* Set DL to the day in the year of the first day of the week
+     containing the day specified in TP.  */
+  dl = tp->tm_yday - wday;
+
+  /* For the computation following ISO 8601:1988 we set the number of
+     the week containing January 1st to 1 if this week has more than
+     MAX_PRECEDING days in the new year.  For ISO 8601 this number is
+     3, for the other representation it is 7 (i.e., not to be
+     fulfilled).  */
+  base = ((dl + 7) % 7) > max_preceding ? 1 : 0;
+
+  /* If DL is negative we compute the result as 0 unless we have to
+     compute it according ISO 8601.  In this case we have to return 53
+     or 1 if the week containing January 1st has less than 4 days in
+     the new year or not.  If DL is not negative we calculate the
+     number of complete weeks for our week (DL / 7) plus 1 (because
+     only for DL < 0 we are in week 0/53 and plus the number of the
+     first week computed in the last step.  */
+  return dl < 0 ? (dl < -max_preceding ? 53 : base)
+               : base + 1 + dl / 7;
 }
 
-/* Convert MINUTES_EAST into a string suitable for use as the RFC-822
-   timezone indicator.  Write no more than MAX bytes into STRING.
-    Return the number of bytes written into STRING.  */
+#ifndef _NL_CURRENT
+static char const weekday_name[][10] =
+  {
+    "Sunday", "Monday", "Tuesday", "Wednesday",
+    "Thursday", "Friday", "Saturday"
+  };
+static char const month_name[][10] =
+  {
+    "January", "February", "March", "April", "May", "June",
+    "July", "August", "September", "October", "November", "December"
+  };
+#endif
 
-static int
-add_num_tz (string, max, minutes_east)
-     char *string;
-     int max;
-     int minutes_east;
+/* Write information from TP into S according to the format
+   string FORMAT, writing no more that MAXSIZE characters
+   (including the terminating '\0') and returning number of
+   characters written.  If S is NULL, nothing will be written
+   anywhere, so to determine how many characters would be
+   written, use NULL for S and (size_t) UINT_MAX for MAXSIZE.  */
+size_t
+strftime (s, maxsize, format, tp)
+      char *s;
+      size_t maxsize;
+      const char *format;
+      register const struct tm *tp;
 {
-  int length;
+  int hour12 = tp->tm_hour;
+#ifdef _NL_CURRENT
+  const char *const a_wkday = _NL_CURRENT (LC_TIME, ABDAY_1 + tp->tm_wday);
+  const char *const f_wkday = _NL_CURRENT (LC_TIME, DAY_1 + tp->tm_wday);
+  const char *const a_month = _NL_CURRENT (LC_TIME, ABMON_1 + tp->tm_mon);
+  const char *const f_month = _NL_CURRENT (LC_TIME, MON_1 + tp->tm_mon);
+  const char *const ampm = _NL_CURRENT (LC_TIME,
+                                       hour12 > 11 ? PM_STR : AM_STR);
+  size_t aw_len = strlen(a_wkday);
+  size_t am_len = strlen(a_month);
+  size_t ap_len = strlen (ampm);
+#else
+  const char *const f_wkday = weekday_name[tp->tm_wday];
+  const char *const f_month = month_name[tp->tm_mon];
+  const char *const a_wkday = f_wkday;
+  const char *const a_month = f_month;
+  const char *const ampm = "AMPM" + 2 * (hour12 > 11);
+  size_t aw_len = 3;
+  size_t am_len = 3;
+  size_t ap_len = 2;
+#endif
+  size_t wkday_len = strlen(f_wkday);
+  size_t month_len = strlen(f_month);
+  const unsigned int y_week0 = week (tp, 0, 7);
+  const unsigned int y_week1 = week (tp, 1, 7);
+  const unsigned int y_week2 = week (tp, 1, 3);
+  const char *zone;
+  size_t zonelen;
+  register size_t i = 0;
+  register char *p = s;
+  register const char *f;
+  char number_fmt[5];
+
+  /* Initialize the buffer we will use for the sprintf format for numbers.  */
+  number_fmt[0] = '%';
+
+  zone = 0;
+#if HAVE_TM_ZONE
+  zone = (const char *) tp->tm_zone;
+#endif
+#if HAVE_TZNAME
+  if (!(zone && *zone) && tp->tm_isdst >= 0)
+    zone = tzname[tp->tm_isdst];
+#endif
+  if (!(zone && *zone))
+    zone = "???";
 
-  if (max < 1)
-    return 0;
+  zonelen = strlen (zone);
 
-  if (minutes_east < 0)
-    {
-      *string = '-';
-      minutes_east = -minutes_east;
-    }
+  if (hour12 > 12)
+    hour12 -= 12;
   else
-    *string = '+';
+    if (hour12 == 0) hour12 = 12;
 
-  length = 1 + add_num2 (&string[1], (minutes_east / 60) % 24, max - 1, zero);
-  length += add_num2 (&string[length], minutes_east % 60, max - length, zero);
+  for (f = format; *f != '\0'; ++f)
+    {
+      enum { pad_zero, pad_space, pad_none } pad; /* Padding for number.  */
+      unsigned int maxdigits;  /* Max digits for numeric format.  */
+      unsigned int number_value; /* Numeric value to be printed.  */
+      const char *subfmt;
 
-  return length;
-}
+#if HAVE_MBLEN
+      if (!isascii(*f))
+       {
+         /* Non-ASCII, may be a multibyte.  */
+         int len = mblen(f, strlen(f));
+         if (len > 0)
+           {
+             cpy(len, f);
+             continue;
+           }
+       }
+#endif
 
-/* Implement %U.  Return the week in the year of the time in TM,
-   with the weeks starting on Sundays.  */
+      if (*f != '%')
+       {
+         add(1, *p = *f);
+         continue;
+       }
 
-static int
-sun_week (tm)
-     const struct tm *tm;
-{
-  int dl;
+      /* Check for flags that can modify a number format.  */
+      ++f;
+      switch (*f)
+       {
+       case '_':
+         pad = pad_space;
+         ++f;
+         break;
+       case '-':
+         pad = pad_none;
+         ++f;
+         break;
+       default:
+         pad = pad_zero;
+         break;
+       }
 
-  /* %U Week of the year (Sunday as the first day of the week) as a decimal
-     number [00-53].  All days in a new year preceding the first Sunday are
-     considered to be in week 0.  */
+      /* Now do the specified format.  */
+      switch (*f)
+       {
+       case '\0':
+       case '%':
+         add(1, *p = *f);
+         break;
+
+       case 'a':
+         cpy(aw_len, a_wkday);
+         break;
+
+       case 'A':
+         cpy(wkday_len, f_wkday);
+         break;
+
+       case 'b':
+       case 'h':               /* GNU extension.  */
+         cpy(am_len, a_month);
+         break;
+
+       case 'B':
+         cpy(month_len, f_month);
+         break;
+
+       case 'c':
+#ifdef _NL_CURRENT
+         subfmt = _NL_CURRENT (LC_TIME, D_T_FMT);
+#else
+         subfmt = "%a %b %d %H:%M:%S %Z %Y";
+#endif
+       subformat:
+         {
+           size_t len = strftime (p, maxsize - i, subfmt, tp);
+           if (len == 0 && *subfmt)
+             return 0;
+           add(len, );
+         }
+         break;
+
+#define DO_NUMBER(digits, value) \
+         maxdigits = digits; number_value = value; goto do_number
+#define DO_NUMBER_NOPAD(digits, value) \
+         maxdigits = digits; number_value = value; goto do_number_nopad
+
+       case 'C':
+         DO_NUMBER (2, (1900 + tp->tm_year) / 100);
+
+       case 'x':
+#ifdef _NL_CURRENT
+         subfmt = _NL_CURRENT (LC_TIME, D_FMT);
+         goto subformat;
+#endif
+         /* Fall through.  */
+       case 'D':               /* GNU extension.  */
+         subfmt = "%m/%d/%y";
+         goto subformat;
 
-  dl = tm->tm_yday - tm->tm_wday;
-  return dl < 0 ? 0 : dl / 7 + 1;
-}
+       case 'd':
+         DO_NUMBER (2, tp->tm_mday);
 
-/* Implement %V.  Similar to mon_week (%W), but there is no 0'th week --
-   they're numbered [01-53].  And if the week containing January 1 has
-   four or more days in the new year, then it is considered week 1;
-   otherwise, it is week 53 of the previous year, and the next week is
-   week 1. (See the ISO 8601: 1988 standard.)  */
+       case 'e':               /* GNU extension: %d, but blank-padded.  */
+         DO_NUMBER_NOPAD (2, tp->tm_mday);
 
-static int
-mon_week_ISO (tm)
-     const struct tm *tm;
-{
-  int dl, n_days_before_first_monday;
-  int week_num;
+         /* All numeric formats set MAXDIGITS and NUMBER_VALUE and then
+            jump to one of these two labels.  */
 
-  n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
-  dl = tm->tm_yday - n_days_before_first_monday;
-  week_num = dl < 0 ? 0 : dl / 7 + 1;
-  if (n_days_before_first_monday >= 4)
-    {
-      week_num = (week_num + 1) % 54;
-      if (week_num == 0)
-       week_num = 1;
-    }
-  if (week_num == 0)
-    week_num = 53;
+       do_number_nopad:
+         /* Force `-' flag.  */
+         pad = pad_none;
 
-  return week_num;
-}
+       do_number:
+         {
+           /* Format the number according to the PAD flag.  */
 
-/* Implement %W.  Return the week in the year of the time in TM,
-   with the weeks starting on Mondays.  */
+           register char *nf = &number_fmt[1];
+           int printed;
 
-static int
-mon_week (tm)
-     const struct tm *tm;
-{
-  int dl, n_days_before_first_monday;
+           switch (pad)
+             {
+             case pad_zero:
+               *nf++ = '0';
+             case pad_space:
+               *nf++ = '0' + maxdigits;
+             case pad_none:
+               *nf++ = 'u';
+               *nf = '\0';
+             }
 
-  n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
-  dl = tm->tm_yday - n_days_before_first_monday;
-  return dl < 0 ? 0 : dl / 7 + 1;
-}
+#ifdef _LIBC
+           add (maxdigits, printed = sprintf (p, number_fmt, number_value));
+#else
+           add (maxdigits, sprintf (p, number_fmt, number_value);
+                printed = strlen (p));
+#endif
 
-#if !defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME)
-char *
-zone_name (tp)
-     struct tm *tp;
-{
-  char *timezone ();
-  struct timeval tv;
-  struct timezone tz;
+           break;
+         }
 
-  gettimeofday (&tv, &tz);
-  return timezone (tz.tz_minuteswest, tp->tm_isdst);
-}
-#endif
 
-/* Format the time given in TM according to FORMAT, and put the
-   results in STRING.
-   Return the number of characters (not including terminating null)
-   that were put into STRING, or 0 if the length would have
-   exceeded MAX. */
+       case 'H':
+         DO_NUMBER (2, tp->tm_hour);
 
-size_t
-strftime (string, max, format, tm)
-     char *string;
-     size_t max;
-     const char *format;
-     const struct tm *tm;
-{
-  enum padding pad;            /* Type of padding to apply. */
-  size_t length = 0;           /* Characters put in STRING so far. */
+       case 'I':
+         DO_NUMBER (2, hour12);
 
-  for (; *format && length < max; ++format)
-    {
-      if (*format != '%')
-       add_char (*format);
-      else
-       {
-         ++format;
-         /* Modifiers: */
-         if (*format == '-')
-           {
-             pad = none;
-             ++format;
-           }
-         else if (*format == '_')
-           {
-             pad = blank;
-             ++format;
-           }
-         else
-           pad = zero;
+       case 'k':               /* GNU extension.  */
+         DO_NUMBER_NOPAD (2, tp->tm_hour);
 
-         switch (*format)
-           {
-             /* Literal character fields: */
-           case 0:
-           case '%':
-             add_char ('%');
-             break;
-           case 'n':
-             add_char ('\n');
-             break;
-           case 't':
-             add_char ('\t');
-             break;
-           default:
-             add_char (*format);
-             break;
-
-             /* Time fields: */
-           case 'H':
-           case 'k':
-             length +=
-               add_num2 (&string[length], tm->tm_hour, max - length,
-                         *format == 'H' ? pad : blank);
-             break;
-           case 'I':
-           case 'l':
-             {
-               int hour12;
-
-               if (tm->tm_hour == 0)
-                 hour12 = 12;
-               else if (tm->tm_hour > 12)
-                 hour12 = tm->tm_hour - 12;
-               else
-                 hour12 = tm->tm_hour;
-               length +=
-                 add_num2 (&string[length], hour12, max - length,
-                           *format == 'I' ? pad : blank);
-             }
-             break;
-           case 'M':
-             length +=
-               add_num2 (&string[length], tm->tm_min, max - length, pad);
-             break;
-           case 'p':
-             if (tm->tm_hour < 12)
-               add_char ('A');
-             else
-               add_char ('P');
-             add_char ('M');
-             break;
-           case 'r':
-             length +=
-               strftime (&string[length], max - length, "%I:%M:%S %p", tm);
-             break;
-           case 'R':
-             length +=
-               strftime (&string[length], max - length, "%H:%M", tm);
-             break;
-
-           case 's':
-             {
-               struct tm writable_tm;
-               writable_tm = *tm;
-               length += add_num_time_t (&string[length], max - length,
-                                         mktime (&writable_tm));
-             }
-             break;
-
-           case 'S':
-             length +=
-               add_num2 (&string[length], tm->tm_sec, max - length, pad);
-             break;
-           case 'T':
-             length +=
-               strftime (&string[length], max - length, "%H:%M:%S", tm);
-             break;
-           case 'X':
-             length +=
-               strftime (&string[length], max - length, "%H:%M:%S", tm);
-             break;
-           case 'z':
-             {
-               time_t t;
-               struct tm tml, tmg;
-               int diff;
+       case 'l':               /* GNU extension.  */
+         DO_NUMBER_NOPAD (2, hour12);
 
-               tml = *tm;
-               t = mktime (&tml);
-               tml = *localtime (&t); /* Canonicalize the local time */
-               tmg = *gmtime (&t);
+       case 'j':
+         DO_NUMBER (3, 1 + tp->tm_yday);
 
-               /* Compute the difference */
+       case 'M':
+         DO_NUMBER (2, tp->tm_min);
 
-               diff = tml.tm_min - tmg.tm_min;
-               diff += 60 * (tml.tm_hour - tmg.tm_hour);
+       case 'm':
+         DO_NUMBER (2, tp->tm_mon + 1);
 
-               if (tml.tm_mon != tmg.tm_mon)
-                 {
-                   /* We assume no timezone differs from UTC by more than
-                      +- 23 hours.  This should be safe. */
-                   if (tmg.tm_mday == 1)
-                     tml.tm_mday = 0;
-                   else /* tml.tm_mday == 1 */
-                     tmg.tm_mday = 0;
-                 }
+       case 'n':               /* GNU extension.  */
+         add (1, *p = '\n');
+         break;
 
-               diff += 1440 * (tml.tm_mday - tmg.tm_mday);
+       case 'p':
+         cpy(ap_len, ampm);
+         break;
 
-               length += add_num_tz (&string[length], max - length, diff);
-             }
-             break;
-           case 'Z':
-#ifdef HAVE_TM_ZONE
-             length += add_str (&string[length], tm->tm_zone, max - length);
-#else
-#ifdef HAVE_TZNAME
-             if (tm->tm_isdst && tzname[1] && *tzname[1])
-               length += add_str (&string[length], tzname[1], max - length);
-             else
-               length += add_str (&string[length], tzname[0], max - length);
-#else
-             length += add_str (&string[length], zone_name (tm), max - length);
-#endif
+       case 'R':               /* GNU extension.  */
+         subfmt = "%H:%M";
+         goto subformat;
+
+       case 'r':               /* GNU extension.  */
+         subfmt = "%I:%M:%S %p";
+         goto subformat;
+
+       case 'S':
+         DO_NUMBER (2, tp->tm_sec);
+
+       case 'X':
+#ifdef _NL_CURRENT
+         subfmt = _NL_CURRENT (LC_TIME, T_FMT);
+         goto subformat;
 #endif
-             break;
-
-             /* Date fields: */
-           case 'a':
-             add_char (days[tm->tm_wday][0]);
-             add_char (days[tm->tm_wday][1]);
-             add_char (days[tm->tm_wday][2]);
-             break;
-           case 'A':
-             length +=
-               add_str (&string[length], days[tm->tm_wday], max - length);
-             break;
-           case 'b':
-           case 'h':
-             add_char (months[tm->tm_mon][0]);
-             add_char (months[tm->tm_mon][1]);
-             add_char (months[tm->tm_mon][2]);
-             break;
-           case 'B':
-             length +=
-               add_str (&string[length], months[tm->tm_mon], max - length);
-             break;
-           case 'c':
-             length +=
-               strftime (&string[length], max - length,
-                         "%a %b %d %H:%M:%S %Z %Y", tm);
-             break;
-           case 'C':
-             length +=
-               add_num2 (&string[length], (tm->tm_year + 1900) / 100,
-                         max - length, pad);
-             break;
-           case 'd':
-             length +=
-               add_num2 (&string[length], tm->tm_mday, max - length, pad);
-             break;
-           case 'e':
-             length +=
-               add_num2 (&string[length], tm->tm_mday, max - length, blank);
-             break;
-           case 'D':
-             length +=
-               strftime (&string[length], max - length, "%m/%d/%y", tm);
-             break;
-           case 'j':
-             length +=
-               add_num3 (&string[length], tm->tm_yday + 1, max - length, pad);
-             break;
-           case 'm':
-             length +=
-               add_num2 (&string[length], tm->tm_mon + 1, max - length, pad);
-             break;
-           case 'U':
-             length +=
-               add_num2 (&string[length], sun_week (tm), max - length, pad);
-             break;
-           case 'V':
-             length += add_num2 (&string[length], mon_week_ISO (tm),
-                                 max - length, pad);
-             break;
-           case 'w':
-             add_char (tm->tm_wday + '0');
-             break;
-           case 'W':
-             length +=
-               add_num2 (&string[length], mon_week (tm), max - length, pad);
-             break;
-           case 'x':
-             length +=
-               strftime (&string[length], max - length, "%m/%d/%y", tm);
-             break;
-           case 'y':
-             length +=
-               add_num2 (&string[length], tm->tm_year % 100,
-                         max - length, pad);
-             break;
-           case 'Y':
-             add_char ((tm->tm_year + 1900) / 1000 + '0');
-             length +=
-               add_num3 (&string[length],
-                         (1900 + tm->tm_year) % 1000, max - length, zero);
-             break;
-           }
+         /* Fall through.  */
+       case 'T':               /* GNU extenstion.  */
+         subfmt = "%H:%M:%S";
+         goto subformat;
+
+       case 't':               /* GNU extenstion.  */
+         add (1, *p = '\t');
+         break;
+
+       case 'U':
+         DO_NUMBER (2, y_week0);
+
+       case 'V':
+         DO_NUMBER (2, y_week2);
+
+       case 'W':
+         DO_NUMBER (2, y_week1);
+
+       case 'w':
+         DO_NUMBER (2, tp->tm_wday);
+
+       case 'Y':
+         DO_NUMBER (4, 1900 + tp->tm_year);
+
+       case 'y':
+         DO_NUMBER (2, tp->tm_year % 100);
+
+       case 'Z':
+         cpy(zonelen, zone);
+         break;
+
+       default:
+         /* Bad format.  */
+         break;
        }
     }
-  add_char (0);
-  return length - 1;
+
+  if (p)
+    *p = '\0';
+  return i;
 }