3 # $NetBSD: tzselect.ksh,v 1.9 2013/09/20 19:06:54 christos Exp $
7 REPORT_BUGS_TO
=tz@iana.org
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.
12 # Contributed by Paul Eggert.
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:
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/>
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:
30 # Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
31 # mawk <http://invisible-island.net/mawk/>
34 # Specify default values for environment variables if they are unset.
38 # Check for awk Posix compliance.
39 ($AWK -v x
=y
'BEGIN { exit 123 }') </dev
/null
>/dev
/null
2>&1
41 echo >&2 "$0: Sorry, your \`$AWK' program is not Posix compatible."
48 usage
="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
49 Select a time zone interactively.
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).
62 Display at most LIMIT locations when -c is used (default $location_limit).
65 Output version information.
70 Report bugs to $REPORT_BUGS_TO."
72 while getopts c
:n
:-: opt
78 location_limit
=$OPTARG ;;
82 exec echo "tzselect $PKGVERSION$TZVERSION" ;;
84 echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
86 echo >&2 "$0: try '$0 --help'"; exit 1 ;;
93 *) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
96 # Make sure the tables are readable.
97 TZ_COUNTRY_TABLE
=$TZDIR/iso3166.tab
98 TZ_ZONE_TABLE
=$TZDIR/zone.tab
99 for f
in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
102 echo >&2 "$0: time zone files are not set up correctly"
112 # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
113 case $
(echo 1 |
(select x
in x
; do break; done) 2>/dev
/null
) in
117 # Awk script to read a time zone table and output the same table,
118 # with each column preceded by its distance from 'here'.
122 while (getline <TZ_COUNTRY_TABLE)
125 country["US"] = "US" # Otherwise the strings get too long.
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]|$)/) {
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]|$)/) {
137 intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
138 min = degmin - intdeg * 100
139 deg = (intdeg * 60 + min) / 60
142 return deg * 0.017453292519943296
144 function convert_latitude(coord) {
145 match(coord, /..*[-+]/)
146 return convert_coord(substr(coord, 1, RLENGTH - 1))
148 function convert_longitude(coord) {
149 match(coord, /..*[-+]/)
150 return convert_coord(substr(coord, RLENGTH))
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)
164 coord_lat = convert_latitude(coord)
165 coord_long = convert_longitude(coord)
168 here_lat = convert_latitude($2)
169 here_long = convert_longitude($2)
170 line = $1 "\t" $2 "\t" $3 "\t" country[$1]
173 printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
177 # Begin the main loop. We come back here if the user wants to retry.
180 echo >&2 'Please identify a location' \
181 'so that time zone rules can be set correctly.'
192 # Ask the user for continent or ocean.
194 echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
199 entry = substr($3, 1, index($3, "/") - 1)
200 if (entry == "America")
202 if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
203 entry = entry " Ocean"
204 printf "'\''%s'\''\n", entry
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."
219 echo >&2 "Please enter a number in range.";;
222 Americas) continent=America;;
223 *" "*) continent=$(expr "$continent" : '\''\([^ ]*\)'\'')
235 # Ask the user for a Posix TZ string. Check that it conforms.
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.'
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
254 echo >&2 "\`$TZ' is not a conforming" \
255 'Posix time zone string.'
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.'
270 distance_table
=$
($AWK \
272 -v TZ_COUNTRY_TABLE
="$TZ_COUNTRY_TABLE" \
273 "$output_distances" <$TZ_ZONE_TABLE |
275 sed "${location_limit}q"
277 regions
=$
(echo "$distance_table" |
$AWK '
281 echo >&2 'Please select one of the following' \
283 echo >&2 'listed roughly in increasing order' \
284 "of distance from $coord".
285 select region
in $regions
288 '') echo >&2 'Please enter a number in range.';;
292 TZ
=$
(echo "$distance_table" |
$AWK -v region
="$region" '
294 $NF == region { print $4 }
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" \
304 $3 ~ ("^" continent "/") {
305 if (!cc_seen[$1]++) cc_list[++ccs] = $1
308 while (getline <TZ_COUNTRY_TABLE) {
309 if ($0 !~ /^#/) cc_name[$1] = $2
311 for (i = 1; i <= ccs; i++) {
313 if (cc_name[country]) {
314 country = cc_name[country]
319 ' <$TZ_ZONE_TABLE |
sort -f)
322 # If there's more than one country, ask the user which one.
325 echo >&2 'Please select a country' \
326 'whose clocks agree with yours.'
327 select country
in $countries
330 '') echo >&2 'Please enter a number in range.';;
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" \
350 while (getline <TZ_COUNTRY_TABLE) {
351 if ($0 !~ /^#/ && country == $2) {
357 $1 == cc { print $4 }
361 # If there's more than one region, ask the user which one.
364 echo >&2 'Please select one of the following' \
366 select region
in $regions
369 '') echo >&2 'Please enter a number in range.';;
380 # Determine TZ from country and region.
382 -v country
="$country" \
383 -v region
="$region" \
384 -v TZ_COUNTRY_TABLE
="$TZ_COUNTRY_TABLE" \
388 while (getline <TZ_COUNTRY_TABLE) {
389 if ($0 !~ /^#/ && country == $2) {
395 $1 == cc && $4 == region { print $3 }
399 # Make sure the corresponding zoneinfo file exists.
400 TZ_for_date
=$TZDIR/$TZ
402 echo >&2 "$0: time zone files are not set up correctly"
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.
413 for i
in 1 2 3 4 5 6 7 8
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]\)')
422 Local time is now: $TZdate.
423 Universal Time is now: $UTdate."
429 # Output TZ info and ask the user to confirm.
432 echo >&2 "The following information has been given:"
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'"
442 echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
443 echo >&2 "Is the above information OK?"
449 '') echo >&2 'Please enter 1 for Yes, or 2 for No.';;
461 *csh
) file=.login line
="setenv TZ '$TZ'";;
462 *) file=.profile line
="TZ='$TZ'; export TZ"
466 You can make this change permanent for yourself by appending the line
468 to the file '$file' in your home directory; then log out and log in again.
470 Here is that TZ value again, this time on standard output so that you
471 can use the $0 command in shell scripts:"