xref: /netbsd-src/lib/libc/time/tzselect.ksh (revision bfcefd56cfc74946ba0f251734a8426a0b91e115)
1#! /bin/bash
2#
3#	$NetBSD: tzselect.ksh,v 1.9 2013/09/20 19:06:54 christos Exp $
4#
5PKGVERSION='(tzcode) '
6TZVERSION=see_Makefile
7REPORT_BUGS_TO=tz@iana.org
8
9# Ask the user about the time zone, and output the resulting TZ value to stdout.
10# Interact with the user via stderr and stdin.
11
12# Contributed by Paul Eggert.
13
14# Porting notes:
15#
16# This script requires a Posix-like shell with the extension of a
17# 'select' statement.  The 'select' statement was introduced in the
18# Korn shell and is available in Bash and other shell implementations.
19# If your host lacks both Bash and the Korn shell, you can get their
20# source from one of these locations:
21#
22#	Bash <http://www.gnu.org/software/bash/bash.html>
23#	Korn Shell <http://www.kornshell.com/>
24#	Public Domain Korn Shell <http://www.cs.mun.ca/~michael/pdksh/>
25#
26# This script also uses several features of modern awk programs.
27# If your host lacks awk, or has an old awk that does not conform to Posix,
28# you can use either of the following free programs instead:
29#
30#	Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
31#	mawk <http://invisible-island.net/mawk/>
32
33
34# Specify default values for environment variables if they are unset.
35: ${AWK=awk}
36: ${TZDIR=$(pwd)}
37
38# Check for awk Posix compliance.
39($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
40[ $? = 123 ] || {
41	echo >&2 "$0: Sorry, your \`$AWK' program is not Posix compatible."
42	exit 1
43}
44
45coord=
46location_limit=10
47
48usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
49Select a time zone interactively.
50
51Options:
52
53  -c COORD
54    Instead of asking for continent and then country and then city,
55    ask for selection from time zones whose largest cities
56    are closest to the location with geographical coordinates COORD.
57    COORD should use ISO 6709 notation, for example, '-c +4852+00220'
58    for Paris (in degrees and minutes, North and East), or
59    '-c -35-058' for Buenos Aires (in degrees, South and West).
60
61  -n LIMIT
62    Display at most LIMIT locations when -c is used (default $location_limit).
63
64  --version
65    Output version information.
66
67  --help
68    Output this help.
69
70Report bugs to $REPORT_BUGS_TO."
71
72while getopts c:n:-: opt
73do
74    case $opt$OPTARG in
75    c*)
76	coord=$OPTARG ;;
77    n*)
78	location_limit=$OPTARG ;;
79    -help)
80	exec echo "$usage" ;;
81    -version)
82	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
83    -*)
84	echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
85    *)
86	echo >&2 "$0: try '$0 --help'"; exit 1 ;;
87    esac
88done
89
90shift $((OPTIND-1))
91case $# in
920) ;;
93*) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
94esac
95
96# Make sure the tables are readable.
97TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
98TZ_ZONE_TABLE=$TZDIR/zone.tab
99for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
100do
101	<$f || {
102		echo >&2 "$0: time zone files are not set up correctly"
103		exit 1
104	}
105done
106
107newline='
108'
109IFS=$newline
110
111
112# Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
113case $(echo 1 | (select x in x; do break; done) 2>/dev/null) in
114?*) PS3=
115esac
116
117# Awk script to read a time zone table and output the same table,
118# with each column preceded by its distance from 'here'.
119output_distances='
120  BEGIN {
121    FS = "\t"
122    while (getline <TZ_COUNTRY_TABLE)
123      if ($0 ~ /^[^#]/)
124        country[$1] = $2
125    country["US"] = "US" # Otherwise the strings get too long.
126  }
127  function convert_coord(coord, deg, min, ilen, sign, sec) {
128    if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
129      degminsec = coord
130      intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
131      minsec = degminsec - intdeg * 10000
132      intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
133      sec = minsec - intmin * 100
134      deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
135    } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
136      degmin = coord
137      intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
138      min = degmin - intdeg * 100
139      deg = (intdeg * 60 + min) / 60
140    } else
141      deg = coord
142    return deg * 0.017453292519943296
143  }
144  function convert_latitude(coord) {
145    match(coord, /..*[-+]/)
146    return convert_coord(substr(coord, 1, RLENGTH - 1))
147  }
148  function convert_longitude(coord) {
149    match(coord, /..*[-+]/)
150    return convert_coord(substr(coord, RLENGTH))
151  }
152  # Great-circle distance between points with given latitude and longitude.
153  # Inputs and output are in radians.  This uses the great-circle special
154  # case of the Vicenty formula for distances on ellipsoids.
155  function dist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
156    dlong = long2 - long1
157    x = cos (lat2) * sin (dlong)
158    y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
159    num = sqrt (x * x + y * y)
160    denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
161    return atan2(num, denom)
162  }
163  BEGIN {
164    coord_lat = convert_latitude(coord)
165    coord_long = convert_longitude(coord)
166  }
167  /^[^#]/ {
168    here_lat = convert_latitude($2)
169    here_long = convert_longitude($2)
170    line = $1 "\t" $2 "\t" $3 "\t" country[$1]
171    if (NF == 4)
172      line = line " - " $4
173    printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
174  }
175'
176
177# Begin the main loop.  We come back here if the user wants to retry.
178while
179
180	echo >&2 'Please identify a location' \
181		'so that time zone rules can be set correctly.'
182
183	continent=
184	country=
185	region=
186
187	case $coord in
188	?*)
189		continent=coord;;
190	'')
191
192	# Ask the user for continent or ocean.
193
194	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
195
196        quoted_continents=$(
197	  $AWK -F'\t' '
198	    /^[^#]/ {
199              entry = substr($3, 1, index($3, "/") - 1)
200              if (entry == "America")
201		entry = entry "s"
202              if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
203		entry = entry " Ocean"
204              printf "'\''%s'\''\n", entry
205            }
206          ' $TZ_ZONE_TABLE |
207	  sort -u |
208	  tr '\n' ' '
209	  echo ''
210	)
211
212	eval '
213	    select continent in '"$quoted_continents"' \
214		"coord - I want to use geographical coordinates." \
215		"TZ - I want to specify the time zone using the Posix TZ format."
216	    do
217		case $continent in
218		"")
219		    echo >&2 "Please enter a number in range.";;
220		?*)
221		    case $continent in
222		    Americas) continent=America;;
223		    *" "*) continent=$(expr "$continent" : '\''\([^ ]*\)'\'')
224		    esac
225		    break
226		esac
227	    done
228	'
229	esac
230
231	case $continent in
232	'')
233		exit 1;;
234	TZ)
235		# Ask the user for a Posix TZ string.  Check that it conforms.
236		while
237			echo >&2 'Please enter the desired value' \
238				'of the TZ environment variable.'
239			echo >&2 'For example, GST-10 is a zone named GST' \
240				'that is 10 hours ahead (east) of UTC.'
241			read TZ
242			$AWK -v TZ="$TZ" 'BEGIN {
243				tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
244				time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
245				offset = "[-+]?" time
246				date = "(J?[0-9]+|M[0-9]+\.[0-9]+\.[0-9]+)"
247				datetime = "," date "(/" time ")?"
248				tzpattern = "^(:.*|" tzname offset "(" tzname \
249				  "(" offset ")?(" datetime datetime ")?)?)$"
250				if (TZ ~ tzpattern) exit 1
251				exit 0
252			}'
253		do
254			echo >&2 "\`$TZ' is not a conforming" \
255				'Posix time zone string.'
256		done
257		TZ_for_date=$TZ;;
258	*)
259		case $continent in
260		coord)
261		    case $coord in
262		    '')
263			echo >&2 'Please enter coordinates' \
264				'in ISO 6709 notation.'
265			echo >&2 'For example, +4042-07403 stands for'
266			echo >&2 '40 degrees 42 minutes north,' \
267				'74 degrees 3 minutes west.'
268			read coord;;
269		    esac
270		    distance_table=$($AWK \
271			    -v coord="$coord" \
272			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
273			    "$output_distances" <$TZ_ZONE_TABLE |
274		      sort -n |
275		      sed "${location_limit}q"
276		    )
277		    regions=$(echo "$distance_table" | $AWK '
278		      BEGIN { FS = "\t" }
279		      { print $NF }
280		    ')
281		    echo >&2 'Please select one of the following' \
282			    'time zone regions,'
283		    echo >&2 'listed roughly in increasing order' \
284			    "of distance from $coord".
285		    select region in $regions
286		    do
287			case $region in
288			'') echo >&2 'Please enter a number in range.';;
289			?*) break;;
290			esac
291		    done
292		    TZ=$(echo "$distance_table" | $AWK -v region="$region" '
293		      BEGIN { FS="\t" }
294		      $NF == region { print $4 }
295		    ')
296		    ;;
297		*)
298		# Get list of names of countries in the continent or ocean.
299		countries=$($AWK -F'\t' \
300			-v continent="$continent" \
301			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
302		'
303			/^#/ { next }
304			$3 ~ ("^" continent "/") {
305				if (!cc_seen[$1]++) cc_list[++ccs] = $1
306			}
307			END {
308				while (getline <TZ_COUNTRY_TABLE) {
309					if ($0 !~ /^#/) cc_name[$1] = $2
310				}
311				for (i = 1; i <= ccs; i++) {
312					country = cc_list[i]
313					if (cc_name[country]) {
314					  country = cc_name[country]
315					}
316					print country
317				}
318			}
319		' <$TZ_ZONE_TABLE | sort -f)
320
321
322		# If there's more than one country, ask the user which one.
323		case $countries in
324		*"$newline"*)
325			echo >&2 'Please select a country' \
326				'whose clocks agree with yours.'
327			select country in $countries
328			do
329			    case $country in
330			    '') echo >&2 'Please enter a number in range.';;
331			    ?*) break
332			    esac
333			done
334
335			case $country in
336			'') exit 1
337			esac;;
338		*)
339			country=$countries
340		esac
341
342
343		# Get list of names of time zone rule regions in the country.
344		regions=$($AWK -F'\t' \
345			-v country="$country" \
346			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
347		'
348			BEGIN {
349				cc = country
350				while (getline <TZ_COUNTRY_TABLE) {
351					if ($0 !~ /^#/  &&  country == $2) {
352						cc = $1
353						break
354					}
355				}
356			}
357			$1 == cc { print $4 }
358		' <$TZ_ZONE_TABLE)
359
360
361		# If there's more than one region, ask the user which one.
362		case $regions in
363		*"$newline"*)
364			echo >&2 'Please select one of the following' \
365				'time zone regions.'
366			select region in $regions
367			do
368				case $region in
369				'') echo >&2 'Please enter a number in range.';;
370				?*) break
371				esac
372			done
373			case $region in
374			'') exit 1
375			esac;;
376		*)
377			region=$regions
378		esac
379
380		# Determine TZ from country and region.
381		TZ=$($AWK -F'\t' \
382			-v country="$country" \
383			-v region="$region" \
384			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
385		'
386			BEGIN {
387				cc = country
388				while (getline <TZ_COUNTRY_TABLE) {
389					if ($0 !~ /^#/  &&  country == $2) {
390						cc = $1
391						break
392					}
393				}
394			}
395			$1 == cc && $4 == region { print $3 }
396		' <$TZ_ZONE_TABLE)
397		esac
398
399		# Make sure the corresponding zoneinfo file exists.
400		TZ_for_date=$TZDIR/$TZ
401		<$TZ_for_date || {
402			echo >&2 "$0: time zone files are not set up correctly"
403			exit 1
404		}
405	esac
406
407
408	# Use the proposed TZ to output the current date relative to UTC.
409	# Loop until they agree in seconds.
410	# Give up after 8 unsuccessful tries.
411
412	extra_info=
413	for i in 1 2 3 4 5 6 7 8
414	do
415		TZdate=$(LANG=C TZ="$TZ_for_date" date)
416		UTdate=$(LANG=C TZ=UTC0 date)
417		TZsec=$(expr "$TZdate" : '.*:\([0-5][0-9]\)')
418		UTsec=$(expr "$UTdate" : '.*:\([0-5][0-9]\)')
419		case $TZsec in
420		$UTsec)
421			extra_info="
422Local time is now:	$TZdate.
423Universal Time is now:	$UTdate."
424			break
425		esac
426	done
427
428
429	# Output TZ info and ask the user to confirm.
430
431	echo >&2 ""
432	echo >&2 "The following information has been given:"
433	echo >&2 ""
434	case $country%$region%$coord in
435	?*%?*%)	echo >&2 "	$country$newline	$region";;
436	?*%%)	echo >&2 "	$country";;
437	%?*%?*) echo >&2 "	coord $coord$newline	$region";;
438	%%?*)	echo >&2 "	coord $coord";;
439	+)	echo >&2 "	TZ='$TZ'"
440	esac
441	echo >&2 ""
442	echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
443	echo >&2 "Is the above information OK?"
444
445	ok=
446	select ok in Yes No
447	do
448	    case $ok in
449	    '') echo >&2 'Please enter 1 for Yes, or 2 for No.';;
450	    ?*) break
451	    esac
452	done
453	case $ok in
454	'') exit 1;;
455	Yes) break
456	esac
457do coord=
458done
459
460case $SHELL in
461*csh) file=.login line="setenv TZ '$TZ'";;
462*) file=.profile line="TZ='$TZ'; export TZ"
463esac
464
465echo >&2 "
466You can make this change permanent for yourself by appending the line
467	$line
468to the file '$file' in your home directory; then log out and log in again.
469
470Here is that TZ value again, this time on standard output so that you
471can use the $0 command in shell scripts:"
472
473echo "$TZ"
474