about summary refs log tree commit diff
path: root/timezone/tzselect.ksh
diff options
context:
space:
mode:
Diffstat (limited to 'timezone/tzselect.ksh')
-rwxr-xr-xtimezone/tzselect.ksh923
1 files changed, 590 insertions, 333 deletions
diff --git a/timezone/tzselect.ksh b/timezone/tzselect.ksh
index 18fce27e24..38941bbc55 100755
--- a/timezone/tzselect.ksh
+++ b/timezone/tzselect.ksh
@@ -10,7 +10,7 @@ REPORT_BUGS_TO=tz@iana.org
 
 # Porting notes:
 #
-# This script requires a Posix-like shell and prefers the extension of a
+# This script requires a POSIX-like shell and prefers the extension of a
 # 'select' statement.  The 'select' statement was introduced in the
 # Korn shell and is available in Bash and other shell implementations.
 # If your host lacks both Bash and the Korn shell, you can get their
@@ -18,35 +18,47 @@ REPORT_BUGS_TO=tz@iana.org
 #
 #	Bash <https://www.gnu.org/software/bash/>
 #	Korn Shell <http://www.kornshell.com/>
-#	MirBSD Korn Shell <https://www.mirbsd.org/mksh.htm>
+#	MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm>
 #
-# For portability to Solaris 9 /bin/sh this script avoids some POSIX
-# features and common extensions, such as $(...) (which works sometimes
-# but not others), $((...)), and $10.
+# For portability to Solaris 10 /bin/sh (supported by Oracle through
+# January 2027) this script avoids some POSIX features and common
+# extensions, such as $(...), $((...)), ! CMD, unquoted ^, ${#ID},
+# ${ID##PAT}, ${ID%%PAT}, and $10.  Although some of these constructs
+# work sometimes, it's simpler to avoid them entirely.
 #
-# This script also uses several features of modern awk programs.
-# If your host lacks awk, or has an old awk that does not conform to Posix,
-# you can use either of the following free programs instead:
+# This script also uses several features of POSIX awk.
+# If your host lacks awk, or has an old awk that does not conform to POSIX,
+# you can use any of the following free programs instead:
 #
 #	Gawk (GNU awk) <https://www.gnu.org/software/gawk/>
 #	mawk <https://invisible-island.net/mawk/>
+#	nawk <https://github.com/onetrueawk/awk>
+#
+# Because 'awk "VAR=VALUE" ...' and 'awk -v "VAR=VALUE" ...' are not portable
+# if VALUE contains \, ", or newline, awk scripts in this file use:
+#   awk 'BEGIN { VAR = substr(ARGV[1], 2); ARGV[1] = "" } ...' ="VALUE"
+# The substr avoids problems when VALUE is of the form X=Y and would be
+# misinterpreted as an assignment.
 
+# This script does not want path expansion.
+set -f
 
 # Specify default values for environment variables if they are unset.
 : ${AWK=awk}
-: ${TZDIR=`pwd`}
+: ${PWD=`pwd`}
+: ${TZDIR=$PWD}
 
-# Output one argument as-is to standard output.
+# Output one argument as-is to standard output, with trailing newline.
 # Safer than 'echo', which can mishandle '\' or leading '-'.
 say() {
-    printf '%s\n' "$1"
+  printf '%s\n' "$1"
 }
 
-# Check for awk Posix compliance.
-($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
+# Check for awk POSIX compliance.
+($AWK -v x=y 'BEGIN { exit 123 }') <>/dev/null >&0 2>&0
 [ $? = 123 ] || {
-	say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
-	exit 1
+  say >&2 "$0: Sorry, your '$AWK' program is not POSIX compatible."
+  exit 1
 }
 
 coord=
@@ -79,14 +91,14 @@ Report bugs to $REPORT_BUGS_TO."
 
 # Ask the user to select from the function's arguments,
 # and assign the selected argument to the variable 'select_result'.
-# Exit on EOF or I/O error.  Use the shell's 'select' builtin if available,
-# falling back on a less-nice but portable substitute otherwise.
+# Exit on EOF or I/O error.  Use the shell's nicer 'select' builtin if
+# available, falling back on a portable substitute otherwise.
 if
   case $BASH_VERSION in
-  ?*) : ;;
+  ?*) :;;
   '')
     # '; exit' should be redundant, but Dash doesn't properly fail without it.
-    (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
+    (eval 'set --; select x; do break; done; exit') <>/dev/null 2>&0
   esac
 then
   # Do this inside 'eval', as otherwise the shell might exit when parsing it
@@ -96,24 +108,17 @@ then
       select select_result
       do
 	case $select_result in
-	"") echo >&2 "Please enter a number in range." ;;
+	"") echo >&2 "Please enter a number in range.";;
 	?*) break
 	esac
       done || exit
     }
-
-    # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
-    case $BASH_VERSION in
-    [01].*)
-      case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
-      ?*) PS3=
-      esac
-    esac
   '
 else
   doselect() {
     # Field width of the prompt numbers.
-    select_width=`expr $# : '.*'`
+    print_nargs_length="BEGIN {print length(\"$#\");}"
+    select_width=`$AWK "$print_nargs_length"`
 
     select_i=
 
@@ -124,14 +129,14 @@ else
 	select_i=0
 	for select_word
 	do
-	  select_i=`expr $select_i + 1`
+	  select_i=`$AWK "BEGIN { print $select_i + 1 }"`
 	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
-	done ;;
+	done;;
       *[!0-9]*)
-	echo >&2 'Please enter a number in range.' ;;
+	echo >&2 'Please enter a number in range.';;
       *)
 	if test 1 -le $select_i && test $select_i -le $#; then
-	  shift `expr $select_i - 1`
+	  shift `$AWK "BEGIN { print $select_i - 1 }"`
 	  select_result=$1
 	  break
 	fi
@@ -147,70 +152,137 @@ fi
 
 while getopts c:n:t:-: opt
 do
-    case $opt$OPTARG in
-    c*)
-	coord=$OPTARG ;;
-    n*)
-	location_limit=$OPTARG ;;
-    t*) # Undocumented option, used for developer testing.
-	zonetabtype=$OPTARG ;;
-    -help)
-	exec echo "$usage" ;;
-    -version)
-	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
-    -*)
-	say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
-    *)
-	say >&2 "$0: try '$0 --help'"; exit 1 ;;
-    esac
+  case $opt$OPTARG in
+  c*)
+    coord=$OPTARG;;
+  n*)
+    location_limit=$OPTARG;;
+  t*) # Undocumented option, used for developer testing.
+    zonetabtype=$OPTARG;;
+  -help)
+    exec echo "$usage";;
+  -version)
+    exec echo "tzselect $PKGVERSION$TZVERSION";;
+  -*)
+    say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1;;
+  *)
+    say >&2 "$0: try '$0 --help'"; exit 1
+  esac
 done
 
-shift `expr $OPTIND - 1`
+shift `$AWK "BEGIN { print $OPTIND - 1 }"`
 case $# in
 0) ;;
-*) say >&2 "$0: $1: unknown argument"; exit 1 ;;
+*) say >&2 "$0: $1: unknown argument"; exit 1
 esac
 
-# Make sure the tables are readable.
-TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
-TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
-for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
-do
-	<"$f" || {
-		say >&2 "$0: time zone files are not set up correctly"
-		exit 1
-	}
-done
+# translit=true to try transliteration.
+# This is false if U+12345 CUNEIFORM SIGN URU TIMES KI has length 1
+# which means awk (and presumably the shell) do not need transliteration.
+if $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) == 1 }'; then
+  translit=true
+else
+  translit=false
+fi
 
-# If the current locale does not support UTF-8, convert data to current
-# locale's format if possible, as the shell aligns columns better that way.
-# Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
-! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' &&
-    { tmp=`(mktemp -d) 2>/dev/null` || {
-	tmp=${TMPDIR-/tmp}/tzselect.$$ &&
-	(umask 77 && mkdir -- "$tmp")
-    };} &&
-    trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
-    (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
-        2>/dev/null &&
-    TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
-    iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
-    TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
+# Read into shell variable $1 the contents of file $2.
+# Convert to the current locale's encoding if possible,
+# as the shell aligns columns better that way.
+# If GNU iconv's //TRANSLIT does not work, fall back on POSIXish iconv;
+# if that does not work, fall back on 'cat'.
+read_file() {
+  { $translit && {
+    eval "$1=\`(iconv -f UTF-8 -t //TRANSLIT) 2>/dev/null <\"\$2\"\`" ||
+    eval "$1=\`(iconv -f UTF-8) 2>/dev/null <\"\$2\"\`"
+  }; } ||
+  eval "$1=\`cat <\"\$2\"\`" || {
+    say >&2 "$0: time zone files are not set up correctly"
+    exit 1
+  }
+}
+read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab"
+read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab"
+TZ_ZONENOW_TABLE=
 
 newline='
 '
 IFS=$newline
 
+# Awk script to output a country list.
+output_country_list='
+  BEGIN {
+    continent_re = substr(ARGV[1], 2)
+    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+    ARGV[1] = ARGV[2] = ARGV[3] = ""
+    FS = "\t"
+    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+    for (iline = 1; iline <= nlines; iline++) {
+      $0 = line[iline]
+      commentary = $0 ~ /^#@/
+      if (commentary) {
+	if ($0 !~ /^#@/)
+	  continue
+	col1ccs = substr($1, 3)
+	conts = $2
+      } else {
+	col1ccs = $1
+	conts = $3
+      }
+      ncc = split(col1ccs, cc, /,/)
+      ncont = split(conts, cont, /,/)
+      for (i = 1; i <= ncc; i++) {
+	elsewhere = commentary
+	for (ci = 1; ci <= ncont; ci++) {
+	  if (cont[ci] ~ continent_re) {
+	    if (!cc_seen[cc[i]]++)
+	      cc_list[++ccs] = cc[i]
+	    elsewhere = 0
+	  }
+	}
+	if (elsewhere)
+	  for (i = 1; i <= ncc; i++)
+	    cc_elsewhere[cc[i]] = 1
+      }
+    }
+    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+    for (i = 1; i <= nlines; i++) {
+      $0 = line[i]
+      if ($0 !~ /^#/)
+	cc_name[$1] = $2
+    }
+    for (i = 1; i <= ccs; i++) {
+      country = cc_list[i]
+      if (cc_elsewhere[country])
+	continue
+      if (cc_name[country])
+	country = cc_name[country]
+      print country
+    }
+  }
+'
 
-# Awk script to read a time zone table and output the same table,
-# with each column preceded by its distance from 'here'.
-output_distances='
+# Awk script to process a time zone table and output the same table,
+# with each row preceded by its distance from 'here'.
+# If output_times is set, each row is instead preceded by its local time
+# and any apostrophes are escaped for the shell.
+output_distances_or_times='
   BEGIN {
+    coord = substr(ARGV[1], 2)
+    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+    ARGV[1] = ARGV[2] = ARGV[3] = ""
     FS = "\t"
-    while (getline <TZ_COUNTRY_TABLE)
-      if ($0 ~ /^[^#]/)
-        country[$1] = $2
-    country["US"] = "US" # Otherwise the strings get too long.
+    if (!output_times) {
+      nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+      for (i = 1; i <= nlines; i++) {
+	$0 = line[i]
+	if ($0 ~ /^#/)
+	  continue
+	country[$1] = $2
+      }
+      country["US"] = "US" # Otherwise the strings get too long.
+    }
   }
   function abs(x) {
     return x < 0 ? -x : x;
@@ -270,281 +342,466 @@ output_distances='
   BEGIN {
     coord_lat = convert_latitude(coord)
     coord_long = convert_longitude(coord)
-  }
-  /^[^#]/ {
-    here_lat = convert_latitude($2)
-    here_long = convert_longitude($2)
-    line = $1 "\t" $2 "\t" $3
-    sep = "\t"
-    ncc = split($1, cc, /,/)
-    for (i = 1; i <= ncc; i++) {
-      line = line sep country[cc[i]]
-      sep = ", "
+    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+    for (h = 1; h <= nlines; h++) {
+      $0 = line[h]
+      if ($0 ~ /^#/)
+	continue
+      inline[inlines++] = $0
+      ncc = split($1, cc, /,/)
+      for (i = 1; i <= ncc; i++)
+	cc_used[cc[i]]++
+    }
+    for (h = 0; h < inlines; h++) {
+      $0 = inline[h]
+      outline = $1 "\t" $2 "\t" $3
+      sep = "\t"
+      ncc = split($1, cc, /,/)
+      split("", item_seen)
+      item_seen[""] = 1
+      for (i = 1; i <= ncc; i++) {
+	item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4
+	if (item_seen[item]++)
+	  continue
+	outline = outline sep item
+	sep = "; "
+      }
+      if (output_times) {
+	fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n"
+	gsub(/'\''/, "&\\\\&&", outline)
+	printf fmt, $3, h, outline
+      } else {
+	here_lat = convert_latitude($2)
+	here_long = convert_longitude($2)
+	printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), \
+	  outline
+      }
     }
-    if (NF == 4)
-      line = line " - " $4
-    printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
   }
 '
 
 # Begin the main loop.  We come back here if the user wants to retry.
 while
 
-	echo >&2 'Please identify a location' \
-		'so that time zone rules can be set correctly.'
+  echo >&2 'Please identify a location' \
+    'so that time zone rules can be set correctly.'
 
-	continent=
-	country=
-	region=
+  continent=
+  country=
+  country_result=
+  region=
+  time=
+  TZ_ZONE_TABLE=$TZ_ZONETABTYPE_TABLE
 
-	case $coord in
-	?*)
-		continent=coord;;
-	'')
+  case $coord in
+  ?*)
+    continent=coord;;
+  '')
 
-	# Ask the user for continent or ocean.
+    # Ask the user for continent or ocean.
 
-	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
+    echo >&2 \
+      'Please select a continent, ocean, "coord", "TZ", "time", or "now".'
 
-        quoted_continents=`
+    quoted_continents=`
+      $AWK '
+	function handle_entry(entry) {
+	  entry = substr(entry, 1, index(entry, "/") - 1)
+	  if (entry == "America")
+	    entry = entry "s"
+	  if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
+	    entry = entry " Ocean"
+	  printf "'\''%s'\''\n", entry
+	}
+	BEGIN {
+	  TZ_ZONETABTYPE_TABLE = substr(ARGV[1], 2)
+	  ARGV[1] = ""
+	  FS = "\t"
+	  nlines = split(TZ_ZONETABTYPE_TABLE, line, /\n/)
+	  for (i = 1; i <= nlines; i++) {
+	    $0 = line[i]
+	    if ($0 ~ /^[^#]/)
+	      handle_entry($3)
+	    else if ($0 ~ /^#@/) {
+	      ncont = split($2, cont, /,/)
+	      for (ci = 1; ci <= ncont; ci++)
+		handle_entry(cont[ci])
+	    }
+	  }
+	}
+      ' ="$TZ_ZONETABTYPE_TABLE" |
+      sort -u |
+      tr '\n' ' '
+      echo ''
+    `
+
+    eval '
+      doselect '"$quoted_continents"' \
+	"coord - I want to use geographical coordinates." \
+	"TZ - I want to specify the timezone using a POSIX.1-2017 TZ string." \
+	"time - I know local time already." \
+	"now - Like \"time\", but configure only for timestamps from now on."
+      continent=$select_result
+      case $continent in
+      Americas) continent=America;;
+      *)
+	# Get the first word of $continent.  Path expansion is disabled
+	# so this works even with "*", which should not happen.
+	IFS=" "
+	for continent in $continent ""; do break; done
+	IFS=$newline;;
+      esac
+      case $zonetabtype,$continent in
+      zonenow,*) ;;
+      *,now)
+	${TZ_ZONENOW_TABLE:+:} read_file TZ_ZONENOW_TABLE "$TZDIR/zonenow.tab"
+	TZ_ZONE_TABLE=$TZ_ZONENOW_TABLE
+      esac
+    '
+  esac
+
+  case $continent in
+  TZ)
+    # Ask the user for a POSIX.1-2017 TZ string.  Check that it conforms.
+    check_POSIX_TZ_string='
+      BEGIN {
+	tz = substr(ARGV[1], 2)
+	ARGV[1] = ""
+	tzname = ("(<[[:alnum:]+-][[:alnum:]+-][[:alnum:]+-]+>" \
+		  "|[[:alpha:]][[:alpha:]][[:alpha:]]+)")
+	time = ("(2[0-4]|[0-1]?[0-9])" \
+		"(:[0-5][0-9](:[0-5][0-9])?)?")
+	offset = "[-+]?" time
+	mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
+	jdate = ("((J[1-9]|[0-9]|J?[1-9][0-9]" \
+		 "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])")
+	datetime = ",(" mdate "|" jdate ")(/" time ")?"
+	tzpattern = ("^(:.*|" tzname offset "(" tzname \
+		     "(" offset ")?(" datetime datetime ")?)?)$")
+	exit tz ~ tzpattern
+      }
+    '
+
+    while
+      echo >&2 'Please enter the desired value' \
+	'of the TZ environment variable.'
+      echo >&2 'For example, AEST-10 is abbreviated' \
+	'AEST and is 10 hours'
+      echo >&2 'ahead (east) of Greenwich,' \
+	'with no daylight saving time.'
+      read tz
+      $AWK "$check_POSIX_TZ_string" ="$tz"
+    do
+      say >&2 "'$tz' is not a conforming POSIX.1-2017 timezone string."
+    done
+    TZ_for_date=$tz;;
+  *)
+    case $continent in
+    coord)
+      case $coord in
+      '')
+	echo >&2 'Please enter coordinates' \
+	  'in ISO 6709 notation.'
+	echo >&2 'For example, +4042-07403 stands for'
+	echo >&2 '40 degrees 42 minutes north,' \
+	  '74 degrees 3 minutes west.'
+	read coord
+      esac
+      distance_table=`
+	$AWK \
+	  "$output_distances_or_times" \
+	  ="$coord" ="$TZ_COUNTRY_TABLE" ="$TZ_ZONE_TABLE" |
+	sort -n |
+	$AWK "{print} NR == $location_limit { exit }"
+      `
+      regions=`
+	$AWK '
+	  BEGIN {
+	    distance_table = substr(ARGV[1], 2)
+	    ARGV[1] = ""
+	    nlines = split(distance_table, line, /\n/)
+	    for (nr = 1; nr <= nlines; nr++) {
+	      nf = split(line[nr], f, /\t/)
+	      print f[nf]
+	    }
+	  }
+	' ="$distance_table"
+      `
+      echo >&2 'Please select one of the following timezones,'
+      echo >&2 'listed roughly in increasing order' \
+	"of distance from $coord".
+      doselect $regions
+      region=$select_result
+      tz=`
+	$AWK '
+	  BEGIN {
+	    distance_table = substr(ARGV[1], 2)
+	    region = substr(ARGV[2], 2)
+	    ARGV[1] = ARGV[2] = ""
+	    nlines = split(distance_table, line, /\n/)
+	    for (nr = 1; nr <= nlines; nr++) {
+	      nf = split(line[nr], f, /\t/)
+	      if (f[nf] == region)
+		print f[4]
+	    }
+	  }
+	' ="$distance_table" ="$region"
+      `;;
+    *)
+      case $continent in
+      now|time)
+	minute_format='%a %b %d %H:%M'
+	old_minute=`TZ=UTC0 date +"$minute_format"`
+	for i in 1 2 3
+	do
+	  time_table_command=`
+	    $AWK \
+	      -v output_times=1 \
+	      "$output_distances_or_times" \
+	      = = ="$TZ_ZONE_TABLE"
+	  `
+	  time_table=`eval "$time_table_command"`
+	  new_minute=`TZ=UTC0 date +"$minute_format"`
+	  case $old_minute in
+	  "$new_minute") break
+	  esac
+	  old_minute=$new_minute
+	done
+	echo >&2 "The system says Universal Time is $new_minute."
+	echo >&2 "Assuming that's correct, what is the local time?"
+	sorted_table=`say "$time_table" | sort -k2n -k2,5 -k1n` || {
+	  say >&2 "$0: cannot sort time table"
+	  exit 1
+	}
+	eval doselect `
 	  $AWK '
-	    BEGIN { FS = "\t" }
-	    /^[^#]/ {
-              entry = substr($3, 1, index($3, "/") - 1)
-              if (entry == "America")
-		entry = entry "s"
-              if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
-		entry = entry " Ocean"
-              printf "'\''%s'\''\n", entry
-            }
-          ' <"$TZ_ZONE_TABLE" |
-	  sort -u |
-	  tr '\n' ' '
-	  echo ''
+	    BEGIN {
+	      sorted_table = substr(ARGV[1], 2)
+	      ARGV[1] = ""
+	      nlines = split(sorted_table, line, /\n/)
+	      for (i = 1; i <= nlines; i++) {
+		$0 = line[i]
+		outline = $6 " " $7 " " $4 " " $5
+		if (outline == oldline)
+		  continue
+		oldline = outline
+		gsub(/'\''/, "&\\\\&&", outline)
+		printf "'\''%s'\''\n", outline
+	      }
+	    }
+	  ' ="$sorted_table"
 	`
-
-	eval '
-	    doselect '"$quoted_continents"' \
-		"coord - I want to use geographical coordinates." \
-		"TZ - I want to specify the timezone using the Posix TZ format."
-	    continent=$select_result
-	    case $continent in
-	    Americas) continent=America;;
-	    *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
-	    esac
-	'
-	esac
-
-	case $continent in
-	TZ)
-		# Ask the user for a Posix TZ string.  Check that it conforms.
-		while
-			echo >&2 'Please enter the desired value' \
-				'of the TZ environment variable.'
-			echo >&2 'For example, AEST-10 is abbreviated' \
-				'AEST and is 10 hours'
-			echo >&2 'ahead (east) of Greenwich,' \
-				'with no daylight saving time.'
-			read TZ
-			$AWK -v TZ="$TZ" 'BEGIN {
-				tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
-				time = "(2[0-4]|[0-1]?[0-9])" \
-				  "(:[0-5][0-9](:[0-5][0-9])?)?"
-				offset = "[-+]?" time
-				mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
-				jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
-				  "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
-				datetime = ",(" mdate "|" jdate ")(/" time ")?"
-				tzpattern = "^(:.*|" tzname offset "(" tzname \
-				  "(" offset ")?(" datetime datetime ")?)?)$"
-				if (TZ ~ tzpattern) exit 1
-				exit 0
-			}'
-		do
-		    say >&2 "'$TZ' is not a conforming Posix timezone string."
-		done
-		TZ_for_date=$TZ;;
-	*)
-		case $continent in
-		coord)
-		    case $coord in
-		    '')
-			echo >&2 'Please enter coordinates' \
-				'in ISO 6709 notation.'
-			echo >&2 'For example, +4042-07403 stands for'
-			echo >&2 '40 degrees 42 minutes north,' \
-				'74 degrees 3 minutes west.'
-			read coord;;
-		    esac
-		    distance_table=`$AWK \
-			    -v coord="$coord" \
-			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-			    "$output_distances" <"$TZ_ZONE_TABLE" |
-		      sort -n |
-		      sed "${location_limit}q"
-		    `
-		    regions=`say "$distance_table" | $AWK '
-		      BEGIN { FS = "\t" }
-		      { print $NF }
-		    '`
-		    echo >&2 'Please select one of the following timezones,' \
-		    echo >&2 'listed roughly in increasing order' \
-			    "of distance from $coord".
-		    doselect $regions
-		    region=$select_result
-		    TZ=`say "$distance_table" | $AWK -v region="$region" '
-		      BEGIN { FS="\t" }
-		      $NF == region { print $4 }
-		    '`
-		    ;;
-		*)
-		# Get list of names of countries in the continent or ocean.
-		countries=`$AWK \
-			-v continent="$continent" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN { FS = "\t" }
-			/^#/ { next }
-			$3 ~ ("^" continent "/") {
-			    ncc = split($1, cc, /,/)
-			    for (i = 1; i <= ncc; i++)
-				if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
-			}
-			END {
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/) cc_name[$1] = $2
-				}
-				for (i = 1; i <= ccs; i++) {
-					country = cc_list[i]
-					if (cc_name[country]) {
-					  country = cc_name[country]
-					}
-					print country
-				}
-			}
-		' <"$TZ_ZONE_TABLE" | sort -f`
-
-
-		# If there's more than one country, ask the user which one.
-		case $countries in
-		*"$newline"*)
-			echo >&2 'Please select a country' \
-				'whose clocks agree with yours.'
-			doselect $countries
-			country=$select_result;;
-		*)
-			country=$countries
-		esac
-
-
-		# Get list of timezones in the country.
-		regions=`$AWK \
-			-v country="$country" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN {
-				FS = "\t"
-				cc = country
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/  &&  country == $2) {
-						cc = $1
-						break
-					}
-				}
-			}
-			/^#/ { next }
-			$1 ~ cc { print $4 }
-		' <"$TZ_ZONE_TABLE"`
-
-
-		# If there's more than one region, ask the user which one.
-		case $regions in
-		*"$newline"*)
-			echo >&2 'Please select one of the following timezones.'
-			doselect $regions
-			region=$select_result;;
-		*)
-			region=$regions
-		esac
-
-		# Determine TZ from country and region.
-		TZ=`$AWK \
-			-v country="$country" \
-			-v region="$region" \
-			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
-		'
-			BEGIN {
-				FS = "\t"
-				cc = country
-				while (getline <TZ_COUNTRY_TABLE) {
-					if ($0 !~ /^#/  &&  country == $2) {
-						cc = $1
-						break
-					}
-				}
-			}
-			/^#/ { next }
-			$1 ~ cc && $4 == region { print $3 }
-		' <"$TZ_ZONE_TABLE"`
-		esac
-
-		# Make sure the corresponding zoneinfo file exists.
-		TZ_for_date=$TZDIR/$TZ
-		<"$TZ_for_date" || {
-			say >&2 "$0: time zone files are not set up correctly"
-			exit 1
+	time=$select_result
+	continent_re='^'
+	zone_table=`
+	  $AWK '
+	    BEGIN {
+	      time = substr(ARGV[1], 2)
+	      time_table = substr(ARGV[2], 2)
+	      ARGV[1] = ARGV[2] = ""
+	      nlines = split(time_table, line, /\n/)
+	      for (i = 1; i <= nlines; i++) {
+		$0 = line[i]
+		if ($6 " " $7 " " $4 " " $5 == time) {
+		  sub(/[^\t]*\t/, "")
+		  print
 		}
-	esac
+	      }
+	    }
+	  ' ="$time" ="$time_table"
+	`
+	countries=`
+	  $AWK \
+	    "$output_country_list" \
+	    ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
+	  sort -f
+	`
+	;;
+      *)
+	continent_re="^$continent/"
+	zone_table=$TZ_ZONE_TABLE
+      esac
 
+      # Get list of names of countries in the continent or ocean.
+      countries=`
+	$AWK \
+	  "$output_country_list" \
+	  ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
+	sort -f
+      `
+      # If all zone table entries have comments, and there are
+      # at most 22 entries, asked based on those comments.
+      # This fits the prompt onto old-fashioned 24-line screens.
+      regions=`
+	$AWK '
+	  BEGIN {
+	    TZ_ZONE_TABLE = substr(ARGV[1], 2)
+	    ARGV[1] = ""
+	    FS = "\t"
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^[^#]/ && !missing_comment) {
+		if ($4)
+		  comment[++inlines] = $4
+		else
+		  missing_comment = 1
+	      }
+	    }
+	    if (!missing_comment && inlines <= 22)
+	      for (i = 1; i <= inlines; i++)
+		print comment[i]
+	  }
+	' ="$zone_table"
+      `
+
+      # If there's more than one country, ask the user which one.
+      case $countries in
+      *"$newline"*)
+	echo >&2 'Please select a country' \
+	  'whose clocks agree with yours.'
+	doselect $countries
+	country_result=$select_result
+	country=$select_result;;
+      *)
+	country=$countries
+      esac
 
-	# Use the proposed TZ to output the current date relative to UTC.
-	# Loop until they agree in seconds.
-	# Give up after 8 unsuccessful tries.
 
-	extra_info=
-	for i in 1 2 3 4 5 6 7 8
-	do
-		TZdate=`LANG=C TZ="$TZ_for_date" date`
-		UTdate=`LANG=C TZ=UTC0 date`
-		TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
-		UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
-		case $TZsec in
-		$UTsec)
-			extra_info="
-Selected time is now:	$TZdate.
-Universal Time is now:	$UTdate."
-			break
-		esac
-	done
+      # Get list of timezones in the country.
+      regions=`
+	$AWK '
+	  BEGIN {
+	    country = substr(ARGV[1], 2)
+	    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
+	    TZ_ZONE_TABLE = substr(ARGV[3], 2)
+	    ARGV[1] = ARGV[2] = ARGV[3] = ""
+	    FS = "\t"
+	    cc = country
+	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 !~ /^#/  &&  country == $2) {
+		cc = $1
+		break
+	      }
+	    }
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^#/)
+		continue
+	      if ($1 ~ cc)
+		print $4
+	    }
+	  }
+	' ="$country" ="$TZ_COUNTRY_TABLE" ="$zone_table"
+      `
+
+      # If there's more than one region, ask the user which one.
+      case $regions in
+      *"$newline"*)
+	echo >&2 'Please select one of the following timezones.'
+	doselect $regions
+	region=$select_result
+      esac
+
+      # Determine tz from country and region.
+      tz=`
+	$AWK '
+	  BEGIN {
+	    country = substr(ARGV[1], 2)
+	    region = substr(ARGV[2], 2)
+	    TZ_COUNTRY_TABLE = substr(ARGV[3], 2)
+	    TZ_ZONE_TABLE = substr(ARGV[4], 2)
+	    ARGV[1] = ARGV[2] = ARGV[3] = ARGV[4] = ""
+	    FS = "\t"
+	    cc = country
+	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 !~ /^#/  &&  country == $2) {
+		cc = $1
+		break
+	      }
+	    }
+	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
+	    for (i = 1; i <= nlines; i++) {
+	      $0 = line[i]
+	      if ($0 ~ /^#/)
+		continue
+	      if ($1 ~ cc && ($4 == region || !region))
+		print $3
+	    }
+	  }
+	' ="$country" ="$region" ="$TZ_COUNTRY_TABLE" ="$zone_table"
+      `
+    esac
 
+    # Make sure the corresponding zoneinfo file exists.
+    TZ_for_date=$TZDIR/$tz
+    <"$TZ_for_date" || {
+      say >&2 "$0: time zone files are not set up correctly"
+      exit 1
+    }
+  esac
 
-	# Output TZ info and ask the user to confirm.
 
-	echo >&2 ""
-	echo >&2 "The following information has been given:"
-	echo >&2 ""
-	case $country%$region%$coord in
-	?*%?*%)	say >&2 "	$country$newline	$region";;
-	?*%%)	say >&2 "	$country";;
-	%?*%?*) say >&2 "	coord $coord$newline	$region";;
-	%%?*)	say >&2 "	coord $coord";;
-	*)	say >&2 "	TZ='$TZ'"
-	esac
-	say >&2 ""
-	say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
-	say >&2 "Is the above information OK?"
-
-	doselect Yes No
-	ok=$select_result
-	case $ok in
-	Yes) break
-	esac
+  # Use the proposed TZ to output the current date relative to UTC.
+  # Loop until they agree in seconds.
+  # Give up after 8 unsuccessful tries.
+
+  extra_info=
+  for i in 1 2 3 4 5 6 7 8
+  do
+    TZdate=`LANG=C TZ="$TZ_for_date" date`
+    UTdate=`LANG=C TZ=UTC0 date`
+    if $AWK '
+	  function getsecs(d) {
+	    return match(d, /.*:[0-5][0-9]/) ? substr(d, RLENGTH - 1, 2) : ""
+	  }
+	  BEGIN { exit getsecs(ARGV[1]) != getsecs(ARGV[2]) }
+       ' ="$TZdate" ="$UTdate"
+    then
+      extra_info="
+Selected time is now:	$TZdate.
+Universal Time is now:	$UTdate."
+      break
+    fi
+  done
+
+
+  # Output TZ info and ask the user to confirm.
+
+  echo >&2 ""
+  echo >&2 "Based on the following information:"
+  echo >&2 ""
+  case $time%$country_result%$region%$coord in
+  ?*%?*%?*%)
+    say >&2 "	$time$newline	$country_result$newline	$region";;
+  ?*%?*%%|?*%%?*%) say >&2 "	$time$newline	$country_result$region";;
+  ?*%%%)	say >&2 "	$time";;
+  %?*%?*%)	say >&2 "	$country_result$newline	$region";;
+  %?*%%)	say >&2 "	$country_result";;
+  %%?*%?*)	say >&2 "	coord $coord$newline	$region";;
+  %%%?*)	say >&2 "	coord $coord";;
+  *)		say >&2 "	TZ='$tz'"
+  esac
+  say >&2 ""
+  say >&2 "TZ='$tz' will be used.$extra_info"
+  say >&2 "Is the above information OK?"
+
+  doselect Yes No
+  ok=$select_result
+  case $ok in
+  Yes) break
+  esac
 do coord=
 done
 
 case $SHELL in
-*csh) file=.login line="setenv TZ '$TZ'";;
-*) file=.profile line="TZ='$TZ'; export TZ"
+*csh) file=.login line="setenv TZ '$tz'";;
+*)    file=.profile line="TZ='$tz'; export TZ"
 esac
 
 test -t 1 && say >&2 "
@@ -555,4 +812,4 @@ to the file '$file' in your home directory; then log out and log in again.
 Here is that TZ value again, this time on standard output so that you
 can use the $0 command in shell scripts:"
 
-say "$TZ"
+say "$tz"