xref: /freebsd/contrib/tzcode/tzselect.ksh (revision 62a52c15422470f97fc7b311d89c83f910bcc1b1)
1#!/bin/bash
2# Ask the user about the time zone, and output the resulting TZ value to stdout.
3# Interact with the user via stderr and stdin.
4
5PKGVERSION='(tzcode) '
6TZVERSION=see_Makefile
7REPORT_BUGS_TO=tz@iana.org
8
9# Contributed by Paul Eggert.  This file is in the public domain.
10
11# Porting notes:
12#
13# This script requires a POSIX-like shell and prefers the extension of a
14# 'select' statement.  The 'select' statement was introduced in the
15# Korn shell and is available in Bash and other shell implementations.
16# If your host lacks both Bash and the Korn shell, you can get their
17# source from one of these locations:
18#
19#	Bash <https://www.gnu.org/software/bash/>
20#	Korn Shell <http://www.kornshell.com/>
21#	MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm>
22#
23# For portability to Solaris 10 /bin/sh (supported by Oracle through
24# January 2027) this script avoids some POSIX features and common
25# extensions, such as $(...), $((...)), ! CMD, unquoted ^, ${#ID},
26# ${ID##PAT}, ${ID%%PAT}, and $10.  Although some of these constructs
27# work sometimes, it's simpler to avoid them entirely.
28#
29# This script also uses several features of POSIX awk.
30# If your host lacks awk, or has an old awk that does not conform to POSIX,
31# you can use any of the following free programs instead:
32#
33#	Gawk (GNU awk) <https://www.gnu.org/software/gawk/>
34#	mawk <https://invisible-island.net/mawk/>
35#	nawk <https://github.com/onetrueawk/awk>
36#
37# Because 'awk "VAR=VALUE" ...' and 'awk -v "VAR=VALUE" ...' are not portable
38# if VALUE contains \, ", or newline, awk scripts in this file use:
39#   awk 'BEGIN { VAR = substr(ARGV[1], 2); ARGV[1] = "" } ...' ="VALUE"
40# The substr avoids problems when VALUE is of the form X=Y and would be
41# misinterpreted as an assignment.
42
43# This script does not want path expansion.
44set -f
45
46# Specify default values for environment variables if they are unset.
47: ${AWK=awk}
48: ${PWD=`pwd`}
49: ${TZDIR=$PWD}
50
51# Output one argument as-is to standard output, with trailing newline.
52# Safer than 'echo', which can mishandle '\' or leading '-'.
53say() {
54  printf '%s\n' "$1"
55}
56
57# Check for awk POSIX compliance.
58($AWK -v x=y 'BEGIN { exit 123 }') <>/dev/null >&0 2>&0
59[ $? = 123 ] || {
60  say >&2 "$0: Sorry, your '$AWK' program is not POSIX compatible."
61  exit 1
62}
63
64coord=
65location_limit=10
66zonetabtype=zone1970
67
68usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
69Select a timezone interactively.
70
71Options:
72
73  -c COORD
74    Instead of asking for continent and then country and then city,
75    ask for selection from time zones whose largest cities
76    are closest to the location with geographical coordinates COORD.
77    COORD should use ISO 6709 notation, for example, '-c +4852+00220'
78    for Paris (in degrees and minutes, North and East), or
79    '-c -35-058' for Buenos Aires (in degrees, South and West).
80
81  -n LIMIT
82    Display at most LIMIT locations when -c is used (default $location_limit).
83
84  --version
85    Output version information.
86
87  --help
88    Output this help.
89
90Report bugs to $REPORT_BUGS_TO."
91
92# Ask the user to select from the function's arguments,
93# and assign the selected argument to the variable 'select_result'.
94# Exit on EOF or I/O error.  Use the shell's nicer 'select' builtin if
95# available, falling back on a portable substitute otherwise.
96if
97  case $BASH_VERSION in
98  ?*) :;;
99  '')
100    # '; exit' should be redundant, but Dash doesn't properly fail without it.
101    (eval 'set --; select x; do break; done; exit') <>/dev/null 2>&0
102  esac
103then
104  # Do this inside 'eval', as otherwise the shell might exit when parsing it
105  # even though it is never executed.
106  eval '
107    doselect() {
108      select select_result
109      do
110	case $select_result in
111	"") echo >&2 "Please enter a number in range.";;
112	?*) break
113	esac
114      done || exit
115    }
116  '
117else
118  doselect() {
119    # Field width of the prompt numbers.
120    print_nargs_length="BEGIN {print length(\"$#\");}"
121    select_width=`$AWK "$print_nargs_length"`
122
123    select_i=
124
125    while :
126    do
127      case $select_i in
128      '')
129	select_i=0
130	for select_word
131	do
132	  select_i=`$AWK "BEGIN { print $select_i + 1 }"`
133	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
134	done;;
135      *[!0-9]*)
136	echo >&2 'Please enter a number in range.';;
137      *)
138	if test 1 -le $select_i && test $select_i -le $#; then
139	  shift `$AWK "BEGIN { print $select_i - 1 }"`
140	  select_result=$1
141	  break
142	fi
143	echo >&2 'Please enter a number in range.'
144      esac
145
146      # Prompt and read input.
147      printf >&2 %s "${PS3-#? }"
148      read select_i || exit
149    done
150  }
151fi
152
153while getopts c:n:t:-: opt
154do
155  case $opt$OPTARG in
156  c*)
157    coord=$OPTARG;;
158  n*)
159    location_limit=$OPTARG;;
160  t*) # Undocumented option, used for developer testing.
161    zonetabtype=$OPTARG;;
162  -help)
163    exec echo "$usage";;
164  -version)
165    exec echo "tzselect $PKGVERSION$TZVERSION";;
166  -*)
167    say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1;;
168  *)
169    say >&2 "$0: try '$0 --help'"; exit 1
170  esac
171done
172
173shift `$AWK "BEGIN { print $OPTIND - 1 }"`
174case $# in
1750) ;;
176*) say >&2 "$0: $1: unknown argument"; exit 1
177esac
178
179# translit=true to try transliteration.
180# This is false if U+12345 CUNEIFORM SIGN URU TIMES KI has length 1
181# which means awk (and presumably the shell) do not need transliteration.
182if $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) == 1 }'; then
183  translit=true
184else
185  translit=false
186fi
187
188# Read into shell variable $1 the contents of file $2.
189# Convert to the current locale's encoding if possible,
190# as the shell aligns columns better that way.
191# If GNU iconv's //TRANSLIT does not work, fall back on POSIXish iconv;
192# if that does not work, fall back on 'cat'.
193read_file() {
194  { $translit && {
195    eval "$1=\`(iconv -f UTF-8 -t //TRANSLIT) 2>/dev/null <\"\$2\"\`" ||
196    eval "$1=\`(iconv -f UTF-8) 2>/dev/null <\"\$2\"\`"
197  }; } ||
198  eval "$1=\`cat <\"\$2\"\`" || {
199    say >&2 "$0: time zone files are not set up correctly"
200    exit 1
201  }
202}
203read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab"
204read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab"
205TZ_ZONENOW_TABLE=
206
207newline='
208'
209IFS=$newline
210
211# Awk script to output a country list.
212output_country_list='
213  BEGIN {
214    continent_re = substr(ARGV[1], 2)
215    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
216    TZ_ZONE_TABLE = substr(ARGV[3], 2)
217    ARGV[1] = ARGV[2] = ARGV[3] = ""
218    FS = "\t"
219    nlines = split(TZ_ZONE_TABLE, line, /\n/)
220    for (iline = 1; iline <= nlines; iline++) {
221      $0 = line[iline]
222      commentary = $0 ~ /^#@/
223      if (commentary) {
224	if ($0 !~ /^#@/)
225	  continue
226	col1ccs = substr($1, 3)
227	conts = $2
228      } else {
229	col1ccs = $1
230	conts = $3
231      }
232      ncc = split(col1ccs, cc, /,/)
233      ncont = split(conts, cont, /,/)
234      for (i = 1; i <= ncc; i++) {
235	elsewhere = commentary
236	for (ci = 1; ci <= ncont; ci++) {
237	  if (cont[ci] ~ continent_re) {
238	    if (!cc_seen[cc[i]]++)
239	      cc_list[++ccs] = cc[i]
240	    elsewhere = 0
241	  }
242	}
243	if (elsewhere)
244	  for (i = 1; i <= ncc; i++)
245	    cc_elsewhere[cc[i]] = 1
246      }
247    }
248    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
249    for (i = 1; i <= nlines; i++) {
250      $0 = line[i]
251      if ($0 !~ /^#/)
252	cc_name[$1] = $2
253    }
254    for (i = 1; i <= ccs; i++) {
255      country = cc_list[i]
256      if (cc_elsewhere[country])
257	continue
258      if (cc_name[country])
259	country = cc_name[country]
260      print country
261    }
262  }
263'
264
265# Awk script to process a time zone table and output the same table,
266# with each row preceded by its distance from 'here'.
267# If output_times is set, each row is instead preceded by its local time
268# and any apostrophes are escaped for the shell.
269output_distances_or_times='
270  BEGIN {
271    coord = substr(ARGV[1], 2)
272    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
273    TZ_ZONE_TABLE = substr(ARGV[3], 2)
274    ARGV[1] = ARGV[2] = ARGV[3] = ""
275    FS = "\t"
276    if (!output_times) {
277      nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
278      for (i = 1; i <= nlines; i++) {
279	$0 = line[i]
280	if ($0 ~ /^#/)
281	  continue
282	country[$1] = $2
283      }
284      country["US"] = "US" # Otherwise the strings get too long.
285    }
286  }
287  function abs(x) {
288    return x < 0 ? -x : x;
289  }
290  function min(x, y) {
291    return x < y ? x : y;
292  }
293  function convert_coord(coord, deg, minute, ilen, sign, sec) {
294    if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
295      degminsec = coord
296      intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
297      minsec = degminsec - intdeg * 10000
298      intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
299      sec = minsec - intmin * 100
300      deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
301    } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
302      degmin = coord
303      intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
304      minute = degmin - intdeg * 100
305      deg = (intdeg * 60 + minute) / 60
306    } else
307      deg = coord
308    return deg * 0.017453292519943296
309  }
310  function convert_latitude(coord) {
311    match(coord, /..*[-+]/)
312    return convert_coord(substr(coord, 1, RLENGTH - 1))
313  }
314  function convert_longitude(coord) {
315    match(coord, /..*[-+]/)
316    return convert_coord(substr(coord, RLENGTH))
317  }
318  # Great-circle distance between points with given latitude and longitude.
319  # Inputs and output are in radians.  This uses the great-circle special
320  # case of the Vicenty formula for distances on ellipsoids.
321  function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
322    dlong = long2 - long1
323    x = cos(lat2) * sin(dlong)
324    y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
325    num = sqrt(x * x + y * y)
326    denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
327    return atan2(num, denom)
328  }
329  # Parallel distance between points with given latitude and longitude.
330  # This is the product of the longitude difference and the cosine
331  # of the latitude of the point that is further from the equator.
332  # I.e., it considers longitudes to be further apart if they are
333  # nearer the equator.
334  function pardist(lat1, long1, lat2, long2) {
335    return abs(long1 - long2) * min(cos(lat1), cos(lat2))
336  }
337  # The distance function is the sum of the great-circle distance and
338  # the parallel distance.  It could be weighted.
339  function dist(lat1, long1, lat2, long2) {
340    return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
341  }
342  BEGIN {
343    coord_lat = convert_latitude(coord)
344    coord_long = convert_longitude(coord)
345    nlines = split(TZ_ZONE_TABLE, line, /\n/)
346    for (h = 1; h <= nlines; h++) {
347      $0 = line[h]
348      if ($0 ~ /^#/)
349	continue
350      inline[inlines++] = $0
351      ncc = split($1, cc, /,/)
352      for (i = 1; i <= ncc; i++)
353	cc_used[cc[i]]++
354    }
355    for (h = 0; h < inlines; h++) {
356      $0 = inline[h]
357      outline = $1 "\t" $2 "\t" $3
358      sep = "\t"
359      ncc = split($1, cc, /,/)
360      split("", item_seen)
361      item_seen[""] = 1
362      for (i = 1; i <= ncc; i++) {
363	item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4
364	if (item_seen[item]++)
365	  continue
366	outline = outline sep item
367	sep = "; "
368      }
369      if (output_times) {
370	fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n"
371	gsub(/'\''/, "&\\\\&&", outline)
372	printf fmt, $3, h, outline
373      } else {
374	here_lat = convert_latitude($2)
375	here_long = convert_longitude($2)
376	printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), \
377	  outline
378      }
379    }
380  }
381'
382
383# Begin the main loop.  We come back here if the user wants to retry.
384while
385
386  echo >&2 'Please identify a location' \
387    'so that time zone rules can be set correctly.'
388
389  continent=
390  country=
391  country_result=
392  region=
393  time=
394  TZ_ZONE_TABLE=$TZ_ZONETABTYPE_TABLE
395
396  case $coord in
397  ?*)
398    continent=coord;;
399  '')
400
401    # Ask the user for continent or ocean.
402
403    echo >&2 \
404      'Please select a continent, ocean, "coord", "TZ", "time", or "now".'
405
406    quoted_continents=`
407      $AWK '
408	function handle_entry(entry) {
409	  entry = substr(entry, 1, index(entry, "/") - 1)
410	  if (entry == "America")
411	    entry = entry "s"
412	  if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
413	    entry = entry " Ocean"
414	  printf "'\''%s'\''\n", entry
415	}
416	BEGIN {
417	  TZ_ZONETABTYPE_TABLE = substr(ARGV[1], 2)
418	  ARGV[1] = ""
419	  FS = "\t"
420	  nlines = split(TZ_ZONETABTYPE_TABLE, line, /\n/)
421	  for (i = 1; i <= nlines; i++) {
422	    $0 = line[i]
423	    if ($0 ~ /^[^#]/)
424	      handle_entry($3)
425	    else if ($0 ~ /^#@/) {
426	      ncont = split($2, cont, /,/)
427	      for (ci = 1; ci <= ncont; ci++)
428		handle_entry(cont[ci])
429	    }
430	  }
431	}
432      ' ="$TZ_ZONETABTYPE_TABLE" |
433      sort -u |
434      tr '\n' ' '
435      echo ''
436    `
437
438    eval '
439      doselect '"$quoted_continents"' \
440	"coord - I want to use geographical coordinates." \
441	"TZ - I want to specify the timezone using a POSIX.1-2017 TZ string." \
442	"time - I know local time already." \
443	"now - Like \"time\", but configure only for timestamps from now on."
444      continent=$select_result
445      case $continent in
446      Americas) continent=America;;
447      *)
448	# Get the first word of $continent.  Path expansion is disabled
449	# so this works even with "*", which should not happen.
450	IFS=" "
451	for continent in $continent ""; do break; done
452	IFS=$newline;;
453      esac
454      case $zonetabtype,$continent in
455      zonenow,*) ;;
456      *,now)
457	${TZ_ZONENOW_TABLE:+:} read_file TZ_ZONENOW_TABLE "$TZDIR/zonenow.tab"
458	TZ_ZONE_TABLE=$TZ_ZONENOW_TABLE
459      esac
460    '
461  esac
462
463  case $continent in
464  TZ)
465    # Ask the user for a POSIX.1-2017 TZ string.  Check that it conforms.
466    check_POSIX_TZ_string='
467      BEGIN {
468	tz = substr(ARGV[1], 2)
469	ARGV[1] = ""
470	tzname = ("(<[[:alnum:]+-][[:alnum:]+-][[:alnum:]+-]+>" \
471		  "|[[:alpha:]][[:alpha:]][[:alpha:]]+)")
472	time = ("(2[0-4]|[0-1]?[0-9])" \
473		"(:[0-5][0-9](:[0-5][0-9])?)?")
474	offset = "[-+]?" time
475	mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
476	jdate = ("((J[1-9]|[0-9]|J?[1-9][0-9]" \
477		 "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])")
478	datetime = ",(" mdate "|" jdate ")(/" time ")?"
479	tzpattern = ("^(:.*|" tzname offset "(" tzname \
480		     "(" offset ")?(" datetime datetime ")?)?)$")
481	exit tz ~ tzpattern
482      }
483    '
484
485    while
486      echo >&2 'Please enter the desired value' \
487	'of the TZ environment variable.'
488      echo >&2 'For example, AEST-10 is abbreviated' \
489	'AEST and is 10 hours'
490      echo >&2 'ahead (east) of Greenwich,' \
491	'with no daylight saving time.'
492      read tz
493      $AWK "$check_POSIX_TZ_string" ="$tz"
494    do
495      say >&2 "'$tz' is not a conforming POSIX.1-2017 timezone string."
496    done
497    TZ_for_date=$tz;;
498  *)
499    case $continent in
500    coord)
501      case $coord in
502      '')
503	echo >&2 'Please enter coordinates' \
504	  'in ISO 6709 notation.'
505	echo >&2 'For example, +4042-07403 stands for'
506	echo >&2 '40 degrees 42 minutes north,' \
507	  '74 degrees 3 minutes west.'
508	read coord
509      esac
510      distance_table=`
511	$AWK \
512	  "$output_distances_or_times" \
513	  ="$coord" ="$TZ_COUNTRY_TABLE" ="$TZ_ZONE_TABLE" |
514	sort -n |
515	$AWK "{print} NR == $location_limit { exit }"
516      `
517      regions=`
518	$AWK '
519	  BEGIN {
520	    distance_table = substr(ARGV[1], 2)
521	    ARGV[1] = ""
522	    nlines = split(distance_table, line, /\n/)
523	    for (nr = 1; nr <= nlines; nr++) {
524	      nf = split(line[nr], f, /\t/)
525	      print f[nf]
526	    }
527	  }
528	' ="$distance_table"
529      `
530      echo >&2 'Please select one of the following timezones,'
531      echo >&2 'listed roughly in increasing order' \
532	"of distance from $coord".
533      doselect $regions
534      region=$select_result
535      tz=`
536	$AWK '
537	  BEGIN {
538	    distance_table = substr(ARGV[1], 2)
539	    region = substr(ARGV[2], 2)
540	    ARGV[1] = ARGV[2] = ""
541	    nlines = split(distance_table, line, /\n/)
542	    for (nr = 1; nr <= nlines; nr++) {
543	      nf = split(line[nr], f, /\t/)
544	      if (f[nf] == region)
545		print f[4]
546	    }
547	  }
548	' ="$distance_table" ="$region"
549      `;;
550    *)
551      case $continent in
552      now|time)
553	minute_format='%a %b %d %H:%M'
554	old_minute=`TZ=UTC0 date +"$minute_format"`
555	for i in 1 2 3
556	do
557	  time_table_command=`
558	    $AWK \
559	      -v output_times=1 \
560	      "$output_distances_or_times" \
561	      = = ="$TZ_ZONE_TABLE"
562	  `
563	  time_table=`eval "$time_table_command"`
564	  new_minute=`TZ=UTC0 date +"$minute_format"`
565	  case $old_minute in
566	  "$new_minute") break
567	  esac
568	  old_minute=$new_minute
569	done
570	echo >&2 "The system says Universal Time is $new_minute."
571	echo >&2 "Assuming that's correct, what is the local time?"
572	sorted_table=`say "$time_table" | sort -k2n -k2,5 -k1n` || {
573	  say >&2 "$0: cannot sort time table"
574	  exit 1
575	}
576	eval doselect `
577	  $AWK '
578	    BEGIN {
579	      sorted_table = substr(ARGV[1], 2)
580	      ARGV[1] = ""
581	      nlines = split(sorted_table, line, /\n/)
582	      for (i = 1; i <= nlines; i++) {
583		$0 = line[i]
584		outline = $6 " " $7 " " $4 " " $5
585		if (outline == oldline)
586		  continue
587		oldline = outline
588		gsub(/'\''/, "&\\\\&&", outline)
589		printf "'\''%s'\''\n", outline
590	      }
591	    }
592	  ' ="$sorted_table"
593	`
594	time=$select_result
595	continent_re='^'
596	zone_table=`
597	  $AWK '
598	    BEGIN {
599	      time = substr(ARGV[1], 2)
600	      time_table = substr(ARGV[2], 2)
601	      ARGV[1] = ARGV[2] = ""
602	      nlines = split(time_table, line, /\n/)
603	      for (i = 1; i <= nlines; i++) {
604		$0 = line[i]
605		if ($6 " " $7 " " $4 " " $5 == time) {
606		  sub(/[^\t]*\t/, "")
607		  print
608		}
609	      }
610	    }
611	  ' ="$time" ="$time_table"
612	`
613	countries=`
614	  $AWK \
615	    "$output_country_list" \
616	    ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
617	  sort -f
618	`
619	;;
620      *)
621	continent_re="^$continent/"
622	zone_table=$TZ_ZONE_TABLE
623      esac
624
625      # Get list of names of countries in the continent or ocean.
626      countries=`
627	$AWK \
628	  "$output_country_list" \
629	  ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
630	sort -f
631      `
632      # If all zone table entries have comments, and there are
633      # at most 22 entries, asked based on those comments.
634      # This fits the prompt onto old-fashioned 24-line screens.
635      regions=`
636	$AWK '
637	  BEGIN {
638	    TZ_ZONE_TABLE = substr(ARGV[1], 2)
639	    ARGV[1] = ""
640	    FS = "\t"
641	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
642	    for (i = 1; i <= nlines; i++) {
643	      $0 = line[i]
644	      if ($0 ~ /^[^#]/ && !missing_comment) {
645		if ($4)
646		  comment[++inlines] = $4
647		else
648		  missing_comment = 1
649	      }
650	    }
651	    if (!missing_comment && inlines <= 22)
652	      for (i = 1; i <= inlines; i++)
653		print comment[i]
654	  }
655	' ="$zone_table"
656      `
657
658      # If there's more than one country, ask the user which one.
659      case $countries in
660      *"$newline"*)
661	echo >&2 'Please select a country' \
662	  'whose clocks agree with yours.'
663	doselect $countries
664	country_result=$select_result
665	country=$select_result;;
666      *)
667	country=$countries
668      esac
669
670
671      # Get list of timezones in the country.
672      regions=`
673	$AWK '
674	  BEGIN {
675	    country = substr(ARGV[1], 2)
676	    TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
677	    TZ_ZONE_TABLE = substr(ARGV[3], 2)
678	    ARGV[1] = ARGV[2] = ARGV[3] = ""
679	    FS = "\t"
680	    cc = country
681	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
682	    for (i = 1; i <= nlines; i++) {
683	      $0 = line[i]
684	      if ($0 !~ /^#/  &&  country == $2) {
685		cc = $1
686		break
687	      }
688	    }
689	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
690	    for (i = 1; i <= nlines; i++) {
691	      $0 = line[i]
692	      if ($0 ~ /^#/)
693		continue
694	      if ($1 ~ cc)
695		print $4
696	    }
697	  }
698	' ="$country" ="$TZ_COUNTRY_TABLE" ="$zone_table"
699      `
700
701      # If there's more than one region, ask the user which one.
702      case $regions in
703      *"$newline"*)
704	echo >&2 'Please select one of the following timezones.'
705	doselect $regions
706	region=$select_result
707      esac
708
709      # Determine tz from country and region.
710      tz=`
711	$AWK '
712	  BEGIN {
713	    country = substr(ARGV[1], 2)
714	    region = substr(ARGV[2], 2)
715	    TZ_COUNTRY_TABLE = substr(ARGV[3], 2)
716	    TZ_ZONE_TABLE = substr(ARGV[4], 2)
717	    ARGV[1] = ARGV[2] = ARGV[3] = ARGV[4] = ""
718	    FS = "\t"
719	    cc = country
720	    nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
721	    for (i = 1; i <= nlines; i++) {
722	      $0 = line[i]
723	      if ($0 !~ /^#/  &&  country == $2) {
724		cc = $1
725		break
726	      }
727	    }
728	    nlines = split(TZ_ZONE_TABLE, line, /\n/)
729	    for (i = 1; i <= nlines; i++) {
730	      $0 = line[i]
731	      if ($0 ~ /^#/)
732		continue
733	      if ($1 ~ cc && ($4 == region || !region))
734		print $3
735	    }
736	  }
737	' ="$country" ="$region" ="$TZ_COUNTRY_TABLE" ="$zone_table"
738      `
739    esac
740
741    # Make sure the corresponding zoneinfo file exists.
742    TZ_for_date=$TZDIR/$tz
743    <"$TZ_for_date" || {
744      say >&2 "$0: time zone files are not set up correctly"
745      exit 1
746    }
747  esac
748
749
750  # Use the proposed TZ to output the current date relative to UTC.
751  # Loop until they agree in seconds.
752  # Give up after 8 unsuccessful tries.
753
754  extra_info=
755  for i in 1 2 3 4 5 6 7 8
756  do
757    TZdate=`LANG=C TZ="$TZ_for_date" date`
758    UTdate=`LANG=C TZ=UTC0 date`
759    if $AWK '
760	  function getsecs(d) {
761	    return match(d, /.*:[0-5][0-9]/) ? substr(d, RLENGTH - 1, 2) : ""
762	  }
763	  BEGIN { exit getsecs(ARGV[1]) != getsecs(ARGV[2]) }
764       ' ="$TZdate" ="$UTdate"
765    then
766      extra_info="
767Selected time is now:	$TZdate.
768Universal Time is now:	$UTdate."
769      break
770    fi
771  done
772
773
774  # Output TZ info and ask the user to confirm.
775
776  echo >&2 ""
777  echo >&2 "Based on the following information:"
778  echo >&2 ""
779  case $time%$country_result%$region%$coord in
780  ?*%?*%?*%)
781    say >&2 "	$time$newline	$country_result$newline	$region";;
782  ?*%?*%%|?*%%?*%) say >&2 "	$time$newline	$country_result$region";;
783  ?*%%%)	say >&2 "	$time";;
784  %?*%?*%)	say >&2 "	$country_result$newline	$region";;
785  %?*%%)	say >&2 "	$country_result";;
786  %%?*%?*)	say >&2 "	coord $coord$newline	$region";;
787  %%%?*)	say >&2 "	coord $coord";;
788  *)		say >&2 "	TZ='$tz'"
789  esac
790  say >&2 ""
791  say >&2 "TZ='$tz' will be used.$extra_info"
792  say >&2 "Is the above information OK?"
793
794  doselect Yes No
795  ok=$select_result
796  case $ok in
797  Yes) break
798  esac
799do coord=
800done
801
802case $SHELL in
803*csh) file=.login line="setenv TZ '$tz'";;
804*)    file=.profile line="TZ='$tz'; export TZ"
805esac
806
807test -t 1 && say >&2 "
808You can make this change permanent for yourself by appending the line
809	$line
810to the file '$file' in your home directory; then log out and log in again.
811
812Here is that TZ value again, this time on standard output so that you
813can use the $0 command in shell scripts:"
814
815say "$tz"
816