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