Imported Upstream version 2017c
[platform/upstream/tzdata.git] / 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.  This file is in the public domain.
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 <https://www.gnu.org/software/bash/>
21 #       Korn Shell <http://www.kornshell.com/>
22 #       MirBSD Korn Shell <https://www.mirbsd.org/mksh.htm>
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) <https://www.gnu.org/software/gawk/>
33 #       mawk <https://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 # Output one argument as-is to standard output.
41 # Safer than 'echo', which can mishandle '\' or leading '-'.
42 say() {
43     printf '%s\n' "$1"
44 }
45
46 # Check for awk Posix compliance.
47 ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
48 [ $? = 123 ] || {
49         say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
50         exit 1
51 }
52
53 coord=
54 location_limit=10
55 zonetabtype=zone1970
56
57 usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
58 Select a time zone interactively.
59
60 Options:
61
62   -c COORD
63     Instead of asking for continent and then country and then city,
64     ask for selection from time zones whose largest cities
65     are closest to the location with geographical coordinates COORD.
66     COORD should use ISO 6709 notation, for example, '-c +4852+00220'
67     for Paris (in degrees and minutes, North and East), or
68     '-c -35-058' for Buenos Aires (in degrees, South and West).
69
70   -n LIMIT
71     Display at most LIMIT locations when -c is used (default $location_limit).
72
73   --version
74     Output version information.
75
76   --help
77     Output this help.
78
79 Report bugs to $REPORT_BUGS_TO."
80
81 # Ask the user to select from the function's arguments,
82 # and assign the selected argument to the variable 'select_result'.
83 # Exit on EOF or I/O error.  Use the shell's 'select' builtin if available,
84 # falling back on a less-nice but portable substitute otherwise.
85 if
86   case $BASH_VERSION in
87   ?*) : ;;
88   '')
89     # '; exit' should be redundant, but Dash doesn't properly fail without it.
90     (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
91   esac
92 then
93   # Do this inside 'eval', as otherwise the shell might exit when parsing it
94   # even though it is never executed.
95   eval '
96     doselect() {
97       select select_result
98       do
99         case $select_result in
100         "") echo >&2 "Please enter a number in range." ;;
101         ?*) break
102         esac
103       done || exit
104     }
105
106     # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
107     case $BASH_VERSION in
108     [01].*)
109       case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
110       ?*) PS3=
111       esac
112     esac
113   '
114 else
115   doselect() {
116     # Field width of the prompt numbers.
117     select_width=`expr $# : '.*'`
118
119     select_i=
120
121     while :
122     do
123       case $select_i in
124       '')
125         select_i=0
126         for select_word
127         do
128           select_i=`expr $select_i + 1`
129           printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
130         done ;;
131       *[!0-9]*)
132         echo >&2 'Please enter a number in range.' ;;
133       *)
134         if test 1 -le $select_i && test $select_i -le $#; then
135           shift `expr $select_i - 1`
136           select_result=$1
137           break
138         fi
139         echo >&2 'Please enter a number in range.'
140       esac
141
142       # Prompt and read input.
143       printf >&2 %s "${PS3-#? }"
144       read select_i || exit
145     done
146   }
147 fi
148
149 while getopts c:n:t:-: opt
150 do
151     case $opt$OPTARG in
152     c*)
153         coord=$OPTARG ;;
154     n*)
155         location_limit=$OPTARG ;;
156     t*) # Undocumented option, used for developer testing.
157         zonetabtype=$OPTARG ;;
158     -help)
159         exec echo "$usage" ;;
160     -version)
161         exec echo "tzselect $PKGVERSION$TZVERSION" ;;
162     -*)
163         say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
164     *)
165         say >&2 "$0: try '$0 --help'"; exit 1 ;;
166     esac
167 done
168
169 shift `expr $OPTIND - 1`
170 case $# in
171 0) ;;
172 *) say >&2 "$0: $1: unknown argument"; exit 1 ;;
173 esac
174
175 # Make sure the tables are readable.
176 TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
177 TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
178 for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
179 do
180         <"$f" || {
181                 say >&2 "$0: time zone files are not set up correctly"
182                 exit 1
183         }
184 done
185
186 # If the current locale does not support UTF-8, convert data to current
187 # locale's format if possible, as the shell aligns columns better that way.
188 # Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
189 ! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' &&
190     { tmp=`(mktemp -d) 2>/dev/null` || {
191         tmp=${TMPDIR-/tmp}/tzselect.$$ &&
192         (umask 77 && mkdir -- "$tmp")
193     };} &&
194     trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
195     (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
196         2>/dev/null &&
197     TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
198     iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
199     TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
200
201 newline='
202 '
203 IFS=$newline
204
205
206 # Awk script to read a time zone table and output the same table,
207 # with each column preceded by its distance from 'here'.
208 output_distances='
209   BEGIN {
210     FS = "\t"
211     while (getline <TZ_COUNTRY_TABLE)
212       if ($0 ~ /^[^#]/)
213         country[$1] = $2
214     country["US"] = "US" # Otherwise the strings get too long.
215   }
216   function abs(x) {
217     return x < 0 ? -x : x;
218   }
219   function min(x, y) {
220     return x < y ? x : y;
221   }
222   function convert_coord(coord, deg, minute, ilen, sign, sec) {
223     if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
224       degminsec = coord
225       intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
226       minsec = degminsec - intdeg * 10000
227       intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
228       sec = minsec - intmin * 100
229       deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
230     } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
231       degmin = coord
232       intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
233       minute = degmin - intdeg * 100
234       deg = (intdeg * 60 + minute) / 60
235     } else
236       deg = coord
237     return deg * 0.017453292519943296
238   }
239   function convert_latitude(coord) {
240     match(coord, /..*[-+]/)
241     return convert_coord(substr(coord, 1, RLENGTH - 1))
242   }
243   function convert_longitude(coord) {
244     match(coord, /..*[-+]/)
245     return convert_coord(substr(coord, RLENGTH))
246   }
247   # Great-circle distance between points with given latitude and longitude.
248   # Inputs and output are in radians.  This uses the great-circle special
249   # case of the Vicenty formula for distances on ellipsoids.
250   function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
251     dlong = long2 - long1
252     x = cos(lat2) * sin(dlong)
253     y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
254     num = sqrt(x * x + y * y)
255     denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
256     return atan2(num, denom)
257   }
258   # Parallel distance between points with given latitude and longitude.
259   # This is the product of the longitude difference and the cosine
260   # of the latitude of the point that is further from the equator.
261   # I.e., it considers longitudes to be further apart if they are
262   # nearer the equator.
263   function pardist(lat1, long1, lat2, long2) {
264     return abs(long1 - long2) * min(cos(lat1), cos(lat2))
265   }
266   # The distance function is the sum of the great-circle distance and
267   # the parallel distance.  It could be weighted.
268   function dist(lat1, long1, lat2, long2) {
269     return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
270   }
271   BEGIN {
272     coord_lat = convert_latitude(coord)
273     coord_long = convert_longitude(coord)
274   }
275   /^[^#]/ {
276     here_lat = convert_latitude($2)
277     here_long = convert_longitude($2)
278     line = $1 "\t" $2 "\t" $3
279     sep = "\t"
280     ncc = split($1, cc, /,/)
281     for (i = 1; i <= ncc; i++) {
282       line = line sep country[cc[i]]
283       sep = ", "
284     }
285     if (NF == 4)
286       line = line " - " $4
287     printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
288   }
289 '
290
291 # Begin the main loop.  We come back here if the user wants to retry.
292 while
293
294         echo >&2 'Please identify a location' \
295                 'so that time zone rules can be set correctly.'
296
297         continent=
298         country=
299         region=
300
301         case $coord in
302         ?*)
303                 continent=coord;;
304         '')
305
306         # Ask the user for continent or ocean.
307
308         echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
309
310         quoted_continents=`
311           $AWK '
312             BEGIN { FS = "\t" }
313             /^[^#]/ {
314               entry = substr($3, 1, index($3, "/") - 1)
315               if (entry == "America")
316                 entry = entry "s"
317               if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
318                 entry = entry " Ocean"
319               printf "'\''%s'\''\n", entry
320             }
321           ' <"$TZ_ZONE_TABLE" |
322           sort -u |
323           tr '\n' ' '
324           echo ''
325         `
326
327         eval '
328             doselect '"$quoted_continents"' \
329                 "coord - I want to use geographical coordinates." \
330                 "TZ - I want to specify the time zone using the Posix TZ format."
331             continent=$select_result
332             case $continent in
333             Americas) continent=America;;
334             *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
335             esac
336         '
337         esac
338
339         case $continent in
340         TZ)
341                 # Ask the user for a Posix TZ string.  Check that it conforms.
342                 while
343                         echo >&2 'Please enter the desired value' \
344                                 'of the TZ environment variable.'
345                         echo >&2 'For example, GST-10 is a zone named GST' \
346                                 'that is 10 hours ahead (east) of UTC.'
347                         read TZ
348                         $AWK -v TZ="$TZ" 'BEGIN {
349                                 tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
350                                 time = "(2[0-4]|[0-1]?[0-9])" \
351                                   "(:[0-5][0-9](:[0-5][0-9])?)?"
352                                 offset = "[-+]?" time
353                                 mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
354                                 jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
355                                   "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
356                                 datetime = ",(" mdate "|" jdate ")(/" time ")?"
357                                 tzpattern = "^(:.*|" tzname offset "(" tzname \
358                                   "(" offset ")?(" datetime datetime ")?)?)$"
359                                 if (TZ ~ tzpattern) exit 1
360                                 exit 0
361                         }'
362                 do
363                     say >&2 "'$TZ' is not a conforming Posix time zone string."
364                 done
365                 TZ_for_date=$TZ;;
366         *)
367                 case $continent in
368                 coord)
369                     case $coord in
370                     '')
371                         echo >&2 'Please enter coordinates' \
372                                 'in ISO 6709 notation.'
373                         echo >&2 'For example, +4042-07403 stands for'
374                         echo >&2 '40 degrees 42 minutes north,' \
375                                 '74 degrees 3 minutes west.'
376                         read coord;;
377                     esac
378                     distance_table=`$AWK \
379                             -v coord="$coord" \
380                             -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
381                             "$output_distances" <"$TZ_ZONE_TABLE" |
382                       sort -n |
383                       sed "${location_limit}q"
384                     `
385                     regions=`say "$distance_table" | $AWK '
386                       BEGIN { FS = "\t" }
387                       { print $NF }
388                     '`
389                     echo >&2 'Please select one of the following' \
390                             'time zone regions,'
391                     echo >&2 'listed roughly in increasing order' \
392                             "of distance from $coord".
393                     doselect $regions
394                     region=$select_result
395                     TZ=`say "$distance_table" | $AWK -v region="$region" '
396                       BEGIN { FS="\t" }
397                       $NF == region { print $4 }
398                     '`
399                     ;;
400                 *)
401                 # Get list of names of countries in the continent or ocean.
402                 countries=`$AWK \
403                         -v continent="$continent" \
404                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
405                 '
406                         BEGIN { FS = "\t" }
407                         /^#/ { next }
408                         $3 ~ ("^" continent "/") {
409                             ncc = split($1, cc, /,/)
410                             for (i = 1; i <= ncc; i++)
411                                 if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
412                         }
413                         END {
414                                 while (getline <TZ_COUNTRY_TABLE) {
415                                         if ($0 !~ /^#/) cc_name[$1] = $2
416                                 }
417                                 for (i = 1; i <= ccs; i++) {
418                                         country = cc_list[i]
419                                         if (cc_name[country]) {
420                                           country = cc_name[country]
421                                         }
422                                         print country
423                                 }
424                         }
425                 ' <"$TZ_ZONE_TABLE" | sort -f`
426
427
428                 # If there's more than one country, ask the user which one.
429                 case $countries in
430                 *"$newline"*)
431                         echo >&2 'Please select a country' \
432                                 'whose clocks agree with yours.'
433                         doselect $countries
434                         country=$select_result;;
435                 *)
436                         country=$countries
437                 esac
438
439
440                 # Get list of names of time zone rule regions in the country.
441                 regions=`$AWK \
442                         -v country="$country" \
443                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
444                 '
445                         BEGIN {
446                                 FS = "\t"
447                                 cc = country
448                                 while (getline <TZ_COUNTRY_TABLE) {
449                                         if ($0 !~ /^#/  &&  country == $2) {
450                                                 cc = $1
451                                                 break
452                                         }
453                                 }
454                         }
455                         /^#/ { next }
456                         $1 ~ cc { print $4 }
457                 ' <"$TZ_ZONE_TABLE"`
458
459
460                 # If there's more than one region, ask the user which one.
461                 case $regions in
462                 *"$newline"*)
463                         echo >&2 'Please select one of the following' \
464                                 'time zone regions.'
465                         doselect $regions
466                         region=$select_result;;
467                 *)
468                         region=$regions
469                 esac
470
471                 # Determine TZ from country and region.
472                 TZ=`$AWK \
473                         -v country="$country" \
474                         -v region="$region" \
475                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
476                 '
477                         BEGIN {
478                                 FS = "\t"
479                                 cc = country
480                                 while (getline <TZ_COUNTRY_TABLE) {
481                                         if ($0 !~ /^#/  &&  country == $2) {
482                                                 cc = $1
483                                                 break
484                                         }
485                                 }
486                         }
487                         /^#/ { next }
488                         $1 ~ cc && $4 == region { print $3 }
489                 ' <"$TZ_ZONE_TABLE"`
490                 esac
491
492                 # Make sure the corresponding zoneinfo file exists.
493                 TZ_for_date=$TZDIR/$TZ
494                 <"$TZ_for_date" || {
495                         say >&2 "$0: time zone files are not set up correctly"
496                         exit 1
497                 }
498         esac
499
500
501         # Use the proposed TZ to output the current date relative to UTC.
502         # Loop until they agree in seconds.
503         # Give up after 8 unsuccessful tries.
504
505         extra_info=
506         for i in 1 2 3 4 5 6 7 8
507         do
508                 TZdate=`LANG=C TZ="$TZ_for_date" date`
509                 UTdate=`LANG=C TZ=UTC0 date`
510                 TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
511                 UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
512                 case $TZsec in
513                 $UTsec)
514                         extra_info="
515 Selected time is now:   $TZdate.
516 Universal Time is now:  $UTdate."
517                         break
518                 esac
519         done
520
521
522         # Output TZ info and ask the user to confirm.
523
524         echo >&2 ""
525         echo >&2 "The following information has been given:"
526         echo >&2 ""
527         case $country%$region%$coord in
528         ?*%?*%) say >&2 "       $country$newline        $region";;
529         ?*%%)   say >&2 "       $country";;
530         %?*%?*) say >&2 "       coord $coord$newline    $region";;
531         %%?*)   say >&2 "       coord $coord";;
532         *)      say >&2 "       TZ='$TZ'"
533         esac
534         say >&2 ""
535         say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
536         say >&2 "Is the above information OK?"
537
538         doselect Yes No
539         ok=$select_result
540         case $ok in
541         Yes) break
542         esac
543 do coord=
544 done
545
546 case $SHELL in
547 *csh) file=.login line="setenv TZ '$TZ'";;
548 *) file=.profile line="TZ='$TZ'; export TZ"
549 esac
550
551 test -t 1 && say >&2 "
552 You can make this change permanent for yourself by appending the line
553         $line
554 to the file '$file' in your home directory; then log out and log in again.
555
556 Here is that TZ value again, this time on standard output so that you
557 can use the $0 command in shell scripts:"
558
559 say "$TZ"