From d1a2048bade189cf42a159a8bd9cec1295666f7e Mon Sep 17 00:00:00 2001
From: Paul Eggert <eggert@cs.ucla.edu>
Date: Sun, 21 Aug 2016 14:24:15 -0700
Subject: [PROPOSED PATCH 1/3] New option -i for zdump

* NEWS, zdump.8: Document -i.
* zdump.c (TYPE_BIT, TYPE_SIGNED, INT_STRLEN_MAXIMUM):
New macros, copied from private.h.
(xmalloc): New function.
(tzalloc, saveabbr): Use it to simplify code.
(usage): Document -i, and document other options better.
(main): Support -i.
(hunt): Don't output here; rely on caller to do it, so that
different output formats can be used.  Use same pattern
as in the revised 'main' function.
(gmtoff): New arg T.  All callers changed.
(format_local_time, format_utc_offset, format_quoted_string)
(istrftime, showtrans): New functions, to support -i.
---
 NEWS    |   4 +
 zdump.8 | 143 +++++++++++++++++++++++-
 zdump.c | 384 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 3 files changed, 458 insertions(+), 73 deletions(-)

diff --git a/NEWS b/NEWS
index e8fabe7..8f7d98d 100644
--- a/NEWS
+++ b/NEWS
@@ -22,6 +22,10 @@ Unreleased, experimental changes
     stamps on the reference platform.  (Thanks to Alexander Belopolsky
     for reporting the bug and suggesting a way forward.)
 
+    zdump has a new -i option to generate transitions in a
+    more-compact but still human-readable format.  This option is
+    experimental, and the output format may change in future versions.
+
   Changes affecting documentation and commentary
 
     tzfile.5 now documents the new restriction on POSIX TZ-like
diff --git a/zdump.8 b/zdump.8
index db73f49..16ec454 100644
--- a/zdump.8
+++ b/zdump.8
@@ -9,6 +9,13 @@ zdump \- time zone dumper
 .I zonename
 \&... ]
 .SH DESCRIPTION
+.ie '\(lq'' .ds lq \&"\"
+.el .ds lq \(lq\"
+.ie '\(rq'' .ds rq \&"\"
+.el .ds rq \(rq\"
+.de q
+\\$3\*(lq\\$1\*(rq\\$2
+..
 .ie \n(.g .ds - \f(CW-\fP
 .el ds - \-
 .I Zdump
@@ -21,7 +28,17 @@ These options are available:
 .BI "\*-\*-version"
 Output version information and exit.
 .TP
+.B \*-i
+.I "(This option is experimental: its behavior may change in future versions.)"
+Output a description of time intervals.  For each
+.I zonename
+on the command line, output an interval-format description of the
+zone.  See
+.q "INTERVAL FORMAT"
+below.
+.TP
 .B \*-v
+Output a verbose description of time intervals.
 For each
 .I zonename
 on the command line,
@@ -52,7 +69,7 @@ This generates output that is easier to compare to that of
 implementations with different time representations.
 .TP
 .BI "\*-c " [loyear,]hiyear
-Cut off verbose output at the given year(s).
+Cut off interval output at the given year(s).
 Cutoff times are computed using the proleptic Gregorian calendar with year 0
 and with Universal Time (UT) ignoring leap seconds.
 The lower bound is exclusive and the upper is inclusive; for example, a
@@ -64,7 +81,7 @@ The default cutoff is
 .BR \*-500,2500 .
 .TP
 .BI "\*-t " [lotime,]hitime
-Cut off verbose output at the given time(s),
+Cut off interval output at the given time(s),
 given in decimal seconds since 1970-01-01 00:00:00
 Coordinated Universal Time (UTC).
 The
@@ -73,19 +90,133 @@ determines whether the count includes leap seconds.
 As with
 .BR \*-c ,
 the cutoff's lower bound is exclusive and its upper bound is inclusive.
+.SH "INTERVAL FORMAT"
+.I "This format is experimental: it may change in future versions."
+.PP
+The interval format is a compact text representation that is intended
+to be both human- and machine-readable.  It consists of a first line
+.q "TZ=\fIstring\fP"
+where
+.I string
+is a double-quoted string giving the zone name, a second line
+.q "\*- \*- \fIinterval\fP"
+describing the time interval before the first transition if any, and
+zero or more following lines
+.q "\fIdate time interval\fP",
+one line for each transition time and following interval.  Fields are
+separated by single spaces.
+.PP
+Dates are in
+.IR yyyy - mm - dd
+format and times are in 24-hour
+.IR hh : mm : ss
+format where
+.IR hh <24.
+Times are in local time immediately after the transition.  A
+time interval description consists of a UT offset in signed
+.RI \(+- hhmmss
+format, a time zone abbreviation, and an isdst flag.  An abbreviation
+that equals the UT offset is omitted; other abbreviations are
+double-quoted strings unless they consist of one or more alphabetic
+characters.  An isdst flag is omitted for standard time, and otherwise
+is a decimal integer that is unsigned and positive (typically 1) for
+daylight saving time and negative for unknown.
+.PP
+In times and in UT offsets with absolute value less than 100 hours,
+the seconds are omitted if they are zero, and the minutes are also
+omitted if they are also zero.  Positive UT offsets are east of
+Greenwich.  The UT offset \*-00 denotes a UT placeholder in areas
+where the actual offset is unspecified; by convention, this occurs
+when the UT offset is zero and the time zone abbreviation begins with
+.q "\*-"
+or is
+.q "zzz".
+.PP
+In double-quoted strings, escape sequences represent unusual
+characters.  The escape sequences are \es for space, and \e", \e\e,
+\ef, \en, \er, \et, and \ev with their usual meaning in the C
+programming language.  E.g., the double-quoted string
+\*(lq"CET\es\e"\e\e"\*(rq represents the character sequence \*(lqCET
+"\e\*(rq.\""
+.PP
+.ne 9
+Here is an example:
+.nf
+.sp
+.if \n(.g .ft CW
+.if t .in +.5i
+.if n .in +2
+TZ="Pacific/Honolulu"
+- - -103126 LMT
+1896-01-13 12:01:26 -1030 HST
+1933-04-30 03 -0930 HDT 1
+1933-05-21 11 -1030 HST
+1942-02-09 03 -0930 HDT 1
+1945-09-30 01 -1030 HST
+1947-06-08 02:30 -10 HST
+.in
+.if \n(.g .ft
+.sp
+.fi
+Here, local time begins 10 hours, 31 minutes and 26 seconds west of
+UT, and is a standard time abbreviated LMT.  Immediately after the
+first transition, the date is 1896-01-13 and the time is 12:01:26, and
+the following time interval is 10.5 hours west of UT, a standard time
+abbreviated HST.  Immediately after the second transition, the date is
+1933-04-30 and the time is 03:00:00 and the following time interval is
+9.5 hours west of UT, is abbreviated HDT, and is daylight saving time.
+Immediately after the last transition the date is 1947-06-08 and the
+time is 02:30:00, and the following time interval is 10 hours west of
+UT, a standard time abbreviated HST.
+.PP
+.ne 10
+Here are excerpts from another example:
+.nf
+.sp
+.if \n(.g .ft CW
+.if t .in +.5i
+.if n .in +2
+TZ="Europe/Astrakhan"
+- - +031212 LMT
+1924-04-30 23:47:48 +03
+1930-06-21 01 +04
+1981-04-01 01 +05 1
+1981-09-30 23 +04
+\&...
+2014-10-26 01 +03
+2016-03-27 03 +04
+.in
+.if \n(.g .ft
+.sp
+.fi
+This time zone is east of UT, so its UT offsets are positive.  Also,
+many of its time zone abbreviations omitted since they duplicate the
+text of the UT offset.
+.PP
+If multiple zones are present, their representations are separated
+by empty lines.
 .SH LIMITATIONS
 Time discontinuities are found by sampling the results returned by localtime
 at twelve-hour intervals.
 This works in all real-world cases;
 one can construct artificial time zones for which this fails.
 .PP
-In the output, "UT" denotes the value returned by
+In the
+.B \*-v
+and
+.B \*-V
+output,
+.q "UT"
+denotes the value returned by
 .IR gmtime (3),
 which uses UTC for modern time stamps and some other UT flavor for
 time stamps that predate the introduction of UTC.
-No attempt is currently made to have the output use "UTC" for newer
-and "UT" for older time stamps,
-partly because the exact date of the introduction of UTC is problematic.
+No attempt is currently made to have the output use
+.q "UTC"
+for newer and
+.q "UT"
+for older time stamps, partly because the exact date of the
+introduction of UTC is problematic.
 .SH "SEE ALSO"
 newctime(3), tzfile(5), zic(8)
 .\" This file is in the public domain, so clarified as of
diff --git a/zdump.c b/zdump.c
index 64d90f6..c9d9409 100644
--- a/zdump.c
+++ b/zdump.c
@@ -139,6 +139,26 @@ typedef long intmax_t;
 # include <stdbool.h>
 #endif
 
+#ifndef TYPE_BIT
+#define TYPE_BIT(type)	(sizeof (type) * CHAR_BIT)
+#endif /* !defined TYPE_BIT */
+
+#ifndef TYPE_SIGNED
+#define TYPE_SIGNED(type) (((type) -1) < 0)
+#endif /* !defined TYPE_SIGNED */
+
+#ifndef INT_STRLEN_MAXIMUM
+/*
+** 302 / 1000 is log10(2.0) rounded up.
+** Subtract one for the sign bit if the type is signed;
+** add one for integer division truncation;
+** add one more for a minus sign if the type is signed.
+*/
+#define INT_STRLEN_MAXIMUM(type) \
+	((TYPE_BIT(type) - TYPE_SIGNED(type)) * 302 / 1000 + \
+	1 + TYPE_SIGNED(type))
+#endif /* !defined INT_STRLEN_MAXIMUM */
+
 #ifndef EXIT_SUCCESS
 #define EXIT_SUCCESS	0
 #endif /* !defined EXIT_SUCCESS */
@@ -268,6 +288,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;
 
@@ -305,6 +327,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
@@ -392,21 +427,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();
@@ -527,11 +554,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);
   }
@@ -552,10 +575,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);
@@ -567,7 +598,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;
@@ -577,11 +607,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;
@@ -603,9 +629,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:
@@ -617,7 +644,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;
@@ -674,7 +701,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]);
@@ -685,48 +714,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"[i == optind], &tm, t, ab, argv[i]);
+		    showtrans("- - %o%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 %L %o%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);
@@ -792,12 +839,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;
@@ -809,18 +855,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;
 }
 
@@ -864,13 +909,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 time stamp 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 {
@@ -909,7 +961,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);
@@ -920,6 +972,204 @@ 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 time stamps west of Greenwich; if the time
+   stamp represents an unknown UTC offset, use the format -00.  If the
+   hour needs more than two digits to represent, HH contains three or
+   more digits.  Otherwise, 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_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 time stamp 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
+   %o UTC offset as for format_utc_offset
+   %Q like " %Z D" where D is the isdst flag; except omit " D" if zero,
+      omit " %Z" if %Z=%o, and quote and escape %Z if it contains
+      nonalphabetics.  */
+
+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;
+
+  for (char const *p = f; ; p++)
+    if (*p == '%' && p[1] == '%')
+      p++;
+    else if (!*p
+	     || (*p == '%'
+		 && (p[1] == 'f' || p[1] == 'L'
+		     || p[1] == 'o' || 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 'o':
+	formatted_len = format_utc_offset(b, s, tm, t);
+	break;
+      case 'Q':
+	{
+	  char offbuf[INT_STRLEN_MAXIMUM(long) + sizeof "+mmss"];
+	  format_utc_offset(offbuf, sizeof offbuf, tm, t);
+	  if (strcmp(offbuf, ab) != 0) {
+	    char const *abp;
+	    size_t len;
+	    if (s <= 1)
+	      return false;
+	    *b++ = ' ', 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, " %d", tm->tm_isdst) : 0;
+	}
+	break;
+      }
+      if (! (0 <= formatted_len && formatted_len < s))
+	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)
 {
-- 
2.5.5

