about summary refs log tree commit diff
path: root/timezone/zdump.c
diff options
context:
space:
mode:
Diffstat (limited to 'timezone/zdump.c')
-rw-r--r--timezone/zdump.c541
1 files changed, 305 insertions, 236 deletions
diff --git a/timezone/zdump.c b/timezone/zdump.c
index 063a2635ec..bf75800101 100644
--- a/timezone/zdump.c
+++ b/timezone/zdump.c
@@ -19,89 +19,7 @@
 # define USE_LTZ 1
 #endif
 
-#if USE_LTZ
-# include "private.h"
-#endif
-
-/* Enable tm_gmtoff and tm_zone on GNUish systems.  */
-#define _GNU_SOURCE 1
-/* Enable strtoimax on Solaris 10.  */
-#define __EXTENSIONS__ 1
-
-#include "stdio.h"	/* for stdout, stderr, perror */
-#include "string.h"	/* for strcpy */
-#include "sys/types.h"	/* for time_t */
-#include "time.h"	/* for struct tm */
-#include "stdlib.h"	/* for exit, malloc, atoi */
-#include "limits.h"	/* for CHAR_BIT, LLONG_MAX */
-#include <errno.h>
-
-/*
-** Substitutes for pre-C99 compilers.
-** Much of this section of code is stolen from private.h.
-*/
-
-#ifndef HAVE_STDINT_H
-# define HAVE_STDINT_H \
-    (199901 <= __STDC_VERSION__ \
-     || 2 < __GLIBC__ + (1 <= __GLIBC_MINOR__)	\
-     || __CYGWIN__)
-#endif
-#if HAVE_STDINT_H
-# include "stdint.h"
-#endif
-#ifndef HAVE_INTTYPES_H
-# define HAVE_INTTYPES_H HAVE_STDINT_H
-#endif
-#if HAVE_INTTYPES_H
-# include <inttypes.h>
-#endif
-
-#ifndef INT_FAST32_MAX
-# if INT_MAX >> 31 == 0
-typedef long int_fast32_t;
-# else
-typedef int int_fast32_t;
-# endif
-#endif
-
-/* Pre-C99 GCC compilers define __LONG_LONG_MAX__ instead of LLONG_MAX.  */
-#if !defined LLONG_MAX && defined __LONG_LONG_MAX__
-# define LLONG_MAX __LONG_LONG_MAX__
-#endif
-
-#ifndef INTMAX_MAX
-# ifdef LLONG_MAX
-typedef long long intmax_t;
-#  define strtoimax strtoll
-#  define INTMAX_MAX LLONG_MAX
-# else
-typedef long intmax_t;
-#  define strtoimax strtol
-#  define INTMAX_MAX LONG_MAX
-# endif
-#endif
-
-#ifndef PRIdMAX
-# if INTMAX_MAX == LLONG_MAX
-#  define PRIdMAX "lld"
-# else
-#  define PRIdMAX "ld"
-# endif
-#endif
-
-/* Infer TM_ZONE on systems where this information is known, but suppress
-   guessing if NO_TM_ZONE is defined.  Similarly for TM_GMTOFF.  */
-#if (defined __GLIBC__ \
-     || defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__ \
-     || (defined __APPLE__ && defined __MACH__))
-# if !defined TM_GMTOFF && !defined NO_TM_GMTOFF
-#  define TM_GMTOFF tm_gmtoff
-# endif
-# if !defined TM_ZONE && !defined NO_TM_ZONE
-#  define TM_ZONE tm_zone
-# endif
-#endif
+#include "private.h"
 
 #ifndef HAVE_LOCALTIME_R
 # define HAVE_LOCALTIME_R 1
@@ -131,62 +49,6 @@ typedef long intmax_t;
 #define MAX_STRING_LENGTH	1024
 #endif /* !defined MAX_STRING_LENGTH */
 
-#if __STDC_VERSION__ < 199901
-# define true 1
-# define false 0
-# define bool int
-#else
-# include <stdbool.h>
-#endif
-
-#ifndef EXIT_SUCCESS
-#define EXIT_SUCCESS	0
-#endif /* !defined EXIT_SUCCESS */
-
-#ifndef EXIT_FAILURE
-#define EXIT_FAILURE	1
-#endif /* !defined EXIT_FAILURE */
-
-#ifndef SECSPERMIN
-#define SECSPERMIN	60
-#endif /* !defined SECSPERMIN */
-
-#ifndef MINSPERHOUR
-#define MINSPERHOUR	60
-#endif /* !defined MINSPERHOUR */
-
-#ifndef SECSPERHOUR
-#define SECSPERHOUR	(SECSPERMIN * MINSPERHOUR)
-#endif /* !defined SECSPERHOUR */
-
-#ifndef HOURSPERDAY
-#define HOURSPERDAY	24
-#endif /* !defined HOURSPERDAY */
-
-#ifndef EPOCH_YEAR
-#define EPOCH_YEAR	1970
-#endif /* !defined EPOCH_YEAR */
-
-#ifndef TM_YEAR_BASE
-#define TM_YEAR_BASE	1900
-#endif /* !defined TM_YEAR_BASE */
-
-#ifndef DAYSPERNYEAR
-#define DAYSPERNYEAR	365
-#endif /* !defined DAYSPERNYEAR */
-
-#ifndef isleap
-#define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
-#endif /* !defined isleap */
-
-#ifndef isleap_sum
-/*
-** See tzfile.h for details on isleap_sum.
-*/
-#define isleap_sum(a, b)	isleap((a) % 400 + (b) % 400)
-#endif /* !defined isleap_sum */
-
-#define SECSPERDAY	((int_fast32_t) SECSPERHOUR * HOURSPERDAY)
 #define SECSPERNYEAR	(SECSPERDAY * DAYSPERNYEAR)
 #define SECSPERLYEAR	(SECSPERNYEAR + SECSPERDAY)
 #define SECSPER400YEARS	(SECSPERNYEAR * (intmax_t) (300 + 3)	\
@@ -201,49 +63,24 @@ typedef long intmax_t;
 */
 enum { SECSPER400YEARS_FITS = SECSPERLYEAR <= INTMAX_MAX / 400 };
 
-#ifndef HAVE_GETTEXT
-#define HAVE_GETTEXT 0
-#endif
 #if HAVE_GETTEXT
-#include "locale.h"	/* for setlocale */
-#include "libintl.h"
+#include <locale.h>	/* for setlocale */
 #endif /* HAVE_GETTEXT */
 
-#if 2 < __GNUC__ || (__GNUC__ == 2 && 96 <= __GNUC_MINOR__)
-# define ATTRIBUTE_PURE __attribute__ ((__pure__))
-#else
-# define ATTRIBUTE_PURE /* empty */
-#endif
-
-/*
-** For the benefit of GNU folk...
-** '_(MSGID)' uses the current locale's message library string for MSGID.
-** The default is to use gettext if available, and use MSGID otherwise.
-*/
-
-#ifndef _
-#if HAVE_GETTEXT
-#define _(msgid) gettext(msgid)
-#else /* !HAVE_GETTEXT */
-#define _(msgid) msgid
-#endif /* !HAVE_GETTEXT */
-#endif /* !defined _ */
-
-#if !defined TZ_DOMAIN && defined HAVE_GETTEXT
-# define TZ_DOMAIN "tz"
-#endif
-
 #if ! HAVE_LOCALTIME_RZ
 # undef  timezone_t
 # define timezone_t char **
 #endif
 
 extern char **	environ;
+
+#if !HAVE_POSIX_DECLS
 extern int	getopt(int argc, char * const argv[],
 			const char * options);
 extern char *	optarg;
 extern int	optind;
-extern char *	tzname[2];
+extern char *	tzname[];
+#endif
 
 /* The minimum and maximum finite time values.  */
 enum { atime_shift = CHAR_BIT * sizeof (time_t) - 2 };
@@ -266,6 +103,8 @@ static intmax_t	delta(struct tm *, struct tm *) ATTRIBUTE_PURE;
 static void dumptime(struct tm const *);
 static time_t hunt(timezone_t, char *, time_t, time_t);
 static void show(timezone_t, char *, time_t, bool);
+static void showtrans(char const *, struct tm const *, time_t, char const *,
+		      char const *);
 static const char *tformat(void);
 static time_t yeartot(intmax_t) ATTRIBUTE_PURE;
 
@@ -303,6 +142,19 @@ sumsize(size_t a, size_t b)
   return sum;
 }
 
+/* Return a pointer to a newly allocated buffer of size SIZE, exiting
+   on failure.  SIZE should be nonzero.  */
+static void *
+xmalloc(size_t size)
+{
+  void *p = malloc(size);
+  if (!p) {
+    perror(progname);
+    exit(EXIT_FAILURE);
+  }
+  return p;
+}
+
 #if ! HAVE_TZSET
 # undef tzset
 # define tzset zdump_tzset
@@ -390,21 +242,13 @@ tzalloc(char const *val)
 
     while (*e++)
       continue;
-    env = malloc(sumsize(sizeof *environ,
-			 (e - environ) * sizeof *environ));
-    if (! env) {
-      perror(progname);
-      exit(EXIT_FAILURE);
-    }
+    env = xmalloc(sumsize(sizeof *environ,
+			  (e - environ) * sizeof *environ));
     to = 1;
     for (e = environ; (env[to] = *e); e++)
       to += strncmp(*e, "TZ=", 3) != 0;
   }
-  env0 = malloc(sumsize(sizeof "TZ=", strlen(val)));
-  if (! env0) {
-    perror(progname);
-    exit(EXIT_FAILURE);
-  }
+  env0 = xmalloc(sumsize(sizeof "TZ=", strlen(val)));
   env[0] = strcat(strcpy(env0, "TZ="), val);
   environ = fakeenv = env;
   tzset();
@@ -525,11 +369,7 @@ saveabbr(char **buf, size_t *bufalloc, struct tm const *tmp)
 	 to avoid O(N**2) behavior on repeated calls.  */
       *bufalloc = sumsize(*bufalloc, ablen + 1);
 
-      *buf = malloc(*bufalloc);
-      if (! *buf) {
-	perror(progname);
-	exit(EXIT_FAILURE);
-      }
+      *buf = xmalloc(*bufalloc);
     }
     return strcpy(*buf, ab);
   }
@@ -550,10 +390,18 @@ static void
 usage(FILE * const stream, const int status)
 {
 	fprintf(stream,
-_("%s: usage: %s [--version] [--help] [-{vV}] [-{ct} [lo,]hi] zonename ...\n"
+_("%s: usage: %s OPTIONS ZONENAME ...\n"
+  "Options include:\n"
+  "  -c [L,]U   Start at year L (default -500), end before year U (default 2500)\n"
+  "  -t [L,]U   Start at time L, end before time U (in seconds since 1970)\n"
+  "  -i         List transitions briefly (format is experimental)\n" \
+  "  -v         List transitions verbosely\n"
+  "  -V         List transitions a bit less verbosely\n"
+  "  --help     Output this help\n"
+  "  --version  Output version info\n"
   "\n"
   "Report bugs to %s.\n"),
-		       progname, progname, REPORT_BUGS_TO);
+		progname, progname, REPORT_BUGS_TO);
 	if (status == EXIT_SUCCESS)
 	  close_file(stream);
 	exit(status);
@@ -565,7 +413,6 @@ main(int argc, char *argv[])
 	/* These are static so that they're initially zero.  */
 	static char *		abbrev;
 	static size_t		abbrevsize;
-	static struct tm	newtm;
 
 	register int		i;
 	register bool		vflag;
@@ -575,11 +422,7 @@ main(int argc, char *argv[])
 	register time_t		cutlotime;
 	register time_t		cuthitime;
 	time_t			now;
-	time_t			t;
-	time_t			newt;
-	struct tm		tm;
-	register struct tm *	tmp;
-	register struct tm *	newtmp;
+	bool iflag = false;
 
 	cutlotime = absolute_min_time;
 	cuthitime = absolute_max_time;
@@ -601,9 +444,10 @@ main(int argc, char *argv[])
 	vflag = Vflag = false;
 	cutarg = cuttimes = NULL;
 	for (;;)
-	  switch (getopt(argc, argv, "c:t:vV")) {
+	  switch (getopt(argc, argv, "c:it:vV")) {
 	  case 'c': cutarg = optarg; break;
 	  case 't': cuttimes = optarg; break;
+	  case 'i': iflag = true; break;
 	  case 'v': vflag = true; break;
 	  case 'V': Vflag = true; break;
 	  case -1:
@@ -615,7 +459,7 @@ main(int argc, char *argv[])
 	  }
  arg_processing_done:;
 
-	if (vflag | Vflag) {
+	if (iflag | vflag | Vflag) {
 		intmax_t	lo;
 		intmax_t	hi;
 		char *loend, *hiend;
@@ -672,7 +516,9 @@ main(int argc, char *argv[])
 		}
 	}
 	gmtzinit();
-	now = time(NULL);
+	INITIALIZE (now);
+	if (! (iflag | vflag | Vflag))
+	  now = time(NULL);
 	longest = 0;
 	for (i = optind; i < argc; i++) {
 	  size_t arglen = strlen(argv[i]);
@@ -683,48 +529,66 @@ main(int argc, char *argv[])
 	for (i = optind; i < argc; ++i) {
 		timezone_t tz = tzalloc(argv[i]);
 		char const *ab;
+		time_t t;
+		struct tm tm, newtm;
+		bool tm_ok;
 		if (!tz) {
 		  perror(argv[i]);
 		  return EXIT_FAILURE;
 		}
-		if (! (vflag | Vflag)) {
+		if (! (iflag | vflag | Vflag)) {
 			show(tz, argv[i], now, false);
 			tzfree(tz);
 			continue;
 		}
 		warned = false;
 		t = absolute_min_time;
-		if (!Vflag) {
+		if (! (iflag | Vflag)) {
 			show(tz, argv[i], t, true);
 			t += SECSPERDAY;
 			show(tz, argv[i], t, true);
 		}
 		if (t < cutlotime)
 			t = cutlotime;
-		tmp = my_localtime_rz(tz, &t, &tm);
-		if (tmp)
+		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
+		if (tm_ok) {
 		  ab = saveabbr(&abbrev, &abbrevsize, &tm);
+		  if (iflag) {
+		    showtrans("\nTZ=%f", &tm, t, ab, argv[i]);
+		    showtrans("-\t-\t%Q", &tm, t, ab, argv[i]);
+		  }
+		}
 		while (t < cuthitime) {
-			newt = ((t < absolute_max_time - SECSPERDAY / 2
-				 && t + SECSPERDAY / 2 < cuthitime)
-				? t + SECSPERDAY / 2
-				: cuthitime);
-			newtmp = localtime_rz(tz, &newt, &newtm);
-			if ((tmp == NULL || newtmp == NULL) ? (tmp != newtmp) :
-				(delta(&newtm, &tm) != (newt - t) ||
-				newtm.tm_isdst != tm.tm_isdst ||
-				strcmp(abbr(&newtm), ab) != 0)) {
-					newt = hunt(tz, argv[i], t, newt);
-					newtmp = localtime_rz(tz, &newt, &newtm);
-					if (newtmp)
-					  ab = saveabbr(&abbrev, &abbrevsize,
-							&newtm);
-			}
-			t = newt;
-			tm = newtm;
-			tmp = newtmp;
+		  time_t newt = ((t < absolute_max_time - SECSPERDAY / 2
+				  && t + SECSPERDAY / 2 < cuthitime)
+				 ? t + SECSPERDAY / 2
+				 : cuthitime);
+		  struct tm *newtmp = localtime_rz(tz, &newt, &newtm);
+		  bool newtm_ok = newtmp != NULL;
+		  if (! (tm_ok & newtm_ok
+			 ? (delta(&newtm, &tm) == newt - t
+			    && newtm.tm_isdst == tm.tm_isdst
+			    && strcmp(abbr(&newtm), ab) == 0)
+			 : tm_ok == newtm_ok)) {
+		    newt = hunt(tz, argv[i], t, newt);
+		    newtmp = localtime_rz(tz, &newt, &newtm);
+		    newtm_ok = newtmp != NULL;
+		    if (iflag)
+		      showtrans("%Y-%m-%d\t%L\t%Q", newtmp, newt,
+				newtm_ok ? abbr(&newtm) : NULL, argv[i]);
+		    else {
+		      show(tz, argv[i], newt - 1, true);
+		      show(tz, argv[i], newt, true);
+		    }
+		  }
+		  t = newt;
+		  tm_ok = newtm_ok;
+		  if (newtm_ok) {
+		    ab = saveabbr(&abbrev, &abbrevsize, &newtm);
+		    tm = newtm;
+		  }
 		}
-		if (!Vflag) {
+		if (! (iflag | Vflag)) {
 			t = absolute_max_time;
 			t -= SECSPERDAY;
 			show(tz, argv[i], t, true);
@@ -790,12 +654,11 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit)
 	char const *		ab;
 	time_t			t;
 	struct tm		lotm;
-	register struct tm *	lotmp;
 	struct tm		tm;
-	register struct tm *	tmp;
+	bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL;
+	bool tm_ok;
 
-	lotmp = my_localtime_rz(tz, &lot, &lotm);
-	if (lotmp)
+	if (lotm_ok)
 	  ab = saveabbr(&loab, &loabsize, &lotm);
 	for ( ; ; ) {
 		time_t diff = hit - lot;
@@ -807,18 +670,17 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit)
 			++t;
 		else if (t >= hit)
 			--t;
-		tmp = my_localtime_rz(tz, &t, &tm);
-		if ((lotmp == NULL || tmp == NULL) ? (lotmp == tmp) :
-			(delta(&tm, &lotm) == (t - lot) &&
-			tm.tm_isdst == lotm.tm_isdst &&
-			strcmp(abbr(&tm), ab) == 0)) {
-				lot = t;
-				lotm = tm;
-				lotmp = tmp;
+		tm_ok = my_localtime_rz(tz, &t, &tm) != NULL;
+		if (lotm_ok & tm_ok
+		    ? (delta(&tm, &lotm) == t - lot
+		       && tm.tm_isdst == lotm.tm_isdst
+		       && strcmp(abbr(&tm), ab) == 0)
+		    : lotm_ok == tm_ok) {
+		  lot = t;
+		  if (tm_ok)
+		    lotm = tm;
 		} else	hit = t;
 	}
-	show(tz, name, lot, true);
-	show(tz, name, hit, true);
 	return hit;
 }
 
@@ -862,13 +724,20 @@ adjusted_yday(struct tm const *a, struct tm const *b)
 
 /* If A is the broken-down local time and B the broken-down UTC for
    the same instant, return A's UTC offset in seconds, where positive
-   offsets are east of Greenwich.  On failure, return LONG_MIN.  */
+   offsets are east of Greenwich.  On failure, return LONG_MIN.
+
+   If T is nonnull, *T is the timestamp that corresponds to A; call
+   my_gmtime_r and use its result instead of B.  Otherwise, B is the
+   possibly nonnull result of an earlier call to my_gmtime_r.  */
 static long
-gmtoff(struct tm const *a, struct tm const *b)
+gmtoff(struct tm const *a, time_t *t, struct tm const *b)
 {
 #ifdef TM_GMTOFF
   return a->TM_GMTOFF;
 #else
+  struct tm tm;
+  if (t)
+    b = my_gmtime_r(t, &tm);
   if (! b)
     return LONG_MIN;
   else {
@@ -907,7 +776,7 @@ show(timezone_t tz, char *zone, time_t t, bool v)
 		if (*abbr(tmp) != '\0')
 			printf(" %s", abbr(tmp));
 		if (v) {
-			long off = gmtoff(tmp, gmtmp);
+			long off = gmtoff(tmp, NULL, gmtmp);
 			printf(" isdst=%d", tmp->tm_isdst);
 			if (off != LONG_MIN)
 			  printf(" gmtoff=%ld", off);
@@ -918,6 +787,206 @@ show(timezone_t tz, char *zone, time_t t, bool v)
 		abbrok(abbr(tmp), zone);
 }
 
+/* Store into BUF, of size SIZE, a formatted local time taken from *TM.
+   Use ISO 8601 format +HH:MM:SS.  Omit :SS if SS is zero, and omit
+   :MM too if MM is also zero.
+
+   Return the length of the resulting string.  If the string does not
+   fit, return the length that the string would have been if it had
+   fit; do not overrun the output buffer.  */
+static int
+format_local_time(char *buf, size_t size, struct tm const *tm)
+{
+  int ss = tm->tm_sec, mm = tm->tm_min, hh = tm->tm_hour;
+  return (ss
+	  ? snprintf(buf, size, "%02d:%02d:%02d", hh, mm, ss)
+	  : mm
+	  ? snprintf(buf, size, "%02d:%02d", hh, mm)
+	  : snprintf(buf, size, "%02d", hh));
+}
+
+/* Store into BUF, of size SIZE, a formatted UTC offset for the
+   localtime *TM corresponding to time T.  Use ISO 8601 format
+   +HHMMSS, or -HHMMSS for timestamps west of Greenwich; use the
+   format -00 for unknown UTC offsets.  If the hour needs more than
+   two digits to represent, extend the length of HH as needed.
+   Otherwise, omit SS if SS is zero, and omit MM too if MM is also
+   zero.
+
+   Return the length of the resulting string, or -1 if the result is
+   not representable as a string.  If the string does not fit, return
+   the length that the string would have been if it had fit; do not
+   overrun the output buffer.  */
+static int
+format_utc_offset(char *buf, size_t size, struct tm const *tm, time_t t)
+{
+  long off = gmtoff(tm, &t, NULL);
+  char sign = ((off < 0
+		|| (off == 0
+		    && (*abbr(tm) == '-' || strcmp(abbr(tm), "zzz") == 0)))
+	       ? '-' : '+');
+  long hh;
+  int mm, ss;
+  if (off < 0)
+    {
+      if (off == LONG_MIN)
+	return -1;
+      off = -off;
+    }
+  ss = off % 60;
+  mm = off / 60 % 60;
+  hh = off / 60 / 60;
+  return (ss || 100 <= hh
+	  ? snprintf(buf, size, "%c%02ld%02d%02d", sign, hh, mm, ss)
+	  : mm
+	  ? snprintf(buf, size, "%c%02ld%02d", sign, hh, mm)
+	  : snprintf(buf, size, "%c%02ld", sign, hh));
+}
+
+/* Store into BUF (of size SIZE) a quoted string representation of P.
+   If the representation's length is less than SIZE, return the
+   length; the representation is not null terminated.  Otherwise
+   return SIZE, to indicate that BUF is too small.  */
+static size_t
+format_quoted_string(char *buf, size_t size, char const *p)
+{
+  char *b = buf;
+  size_t s = size;
+  if (!s)
+    return size;
+  *b++ = '"', s--;
+  for (;;) {
+    char c = *p++;
+    if (s <= 1)
+      return size;
+    switch (c) {
+    default: *b++ = c, s--; continue;
+    case '\0': *b++ = '"', s--; return size - s;
+    case '"': case '\\': break;
+    case ' ': c = 's'; break;
+    case '\f': c = 'f'; break;
+    case '\n': c = 'n'; break;
+    case '\r': c = 'r'; break;
+    case '\t': c = 't'; break;
+    case '\v': c = 'v'; break;
+    }
+    *b++ = '\\', *b++ = c, s -= 2;
+  }
+}
+
+/* Store into BUF (of size SIZE) a timestamp formatted by TIME_FMT.
+   TM is the broken-down time, T the seconds count, AB the time zone
+   abbreviation, and ZONE_NAME the zone name.  Return true if
+   successful, false if the output would require more than SIZE bytes.
+   TIME_FMT uses the same format that strftime uses, with these
+   additions:
+
+   %f zone name
+   %L local time as per format_local_time
+   %Q like "U\t%Z\tD" where U is the UTC offset as for format_utc_offset
+      and D is the isdst flag; except omit D if it is zero, omit %Z if
+      it equals U, quote and escape %Z if it contains nonalphabetics,
+      and omit any trailing tabs.  */
+
+static bool
+istrftime(char *buf, size_t size, char const *time_fmt,
+	  struct tm const *tm, time_t t, char const *ab, char const *zone_name)
+{
+  char *b = buf;
+  size_t s = size;
+  char const *f = time_fmt, *p;
+
+  for (p = f; ; p++)
+    if (*p == '%' && p[1] == '%')
+      p++;
+    else if (!*p
+	     || (*p == '%'
+		 && (p[1] == 'f' || p[1] == 'L' || p[1] == 'Q'))) {
+      size_t formatted_len;
+      size_t f_prefix_len = p - f;
+      size_t f_prefix_copy_size = p - f + 2;
+      char fbuf[100];
+      bool oversized = sizeof fbuf <= f_prefix_copy_size;
+      char *f_prefix_copy = oversized ? xmalloc(f_prefix_copy_size) : fbuf;
+      memcpy(f_prefix_copy, f, f_prefix_len);
+      strcpy(f_prefix_copy + f_prefix_len, "X");
+      formatted_len = strftime(b, s, f_prefix_copy, tm);
+      if (oversized)
+	free(f_prefix_copy);
+      if (formatted_len == 0)
+	return false;
+      formatted_len--;
+      b += formatted_len, s -= formatted_len;
+      if (!*p++)
+	break;
+      switch (*p) {
+      case 'f':
+	formatted_len = format_quoted_string(b, s, zone_name);
+	break;
+      case 'L':
+	formatted_len = format_local_time(b, s, tm);
+	break;
+      case 'Q':
+	{
+	  bool show_abbr;
+	  int offlen = format_utc_offset(b, s, tm, t);
+	  if (! (0 <= offlen && offlen < s))
+	    return false;
+	  show_abbr = strcmp(b, ab) != 0;
+	  b += offlen, s -= offlen;
+	  if (show_abbr) {
+	    char const *abp;
+	    size_t len;
+	    if (s <= 1)
+	      return false;
+	    *b++ = '\t', s--;
+	    for (abp = ab; is_alpha(*abp); abp++)
+	      continue;
+	    len = (!*abp && *ab
+		   ? snprintf(b, s, "%s", ab)
+		   : format_quoted_string(b, s, ab));
+	    if (s <= len)
+	      return false;
+	    b += len, s -= len;
+	  }
+	  formatted_len = (tm->tm_isdst
+			   ? snprintf(b, s, &"\t\t%d"[show_abbr], tm->tm_isdst)
+			   : 0);
+	}
+	break;
+      }
+      if (s <= formatted_len)
+	return false;
+      b += formatted_len, s -= formatted_len;
+      f = p + 1;
+    }
+  *b = '\0';
+  return true;
+}
+
+/* Show a time transition.  */
+static void
+showtrans(char const *time_fmt, struct tm const *tm, time_t t, char const *ab,
+	  char const *zone_name)
+{
+  if (!tm) {
+    printf(tformat(), t);
+    putchar('\n');
+  } else {
+    char stackbuf[1000];
+    size_t size = sizeof stackbuf;
+    char *buf = stackbuf;
+    char *bufalloc = NULL;
+    while (! istrftime(buf, size, time_fmt, tm, t, ab, zone_name)) {
+      size = sumsize(size, size);
+      free(bufalloc);
+      buf = bufalloc = xmalloc(size);
+    }
+    puts(buf);
+    free(bufalloc);
+  }
+}
+
 static char const *
 abbr(struct tm const *tmp)
 {