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