Fixed license declaration at spec file
[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.
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 # 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 = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
350                                 time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
351                                 offset = "[-+]?" time
352                                 date = "(J?[0-9]+|M[0-9]+\\.[0-9]+\\.[0-9]+)"
353                                 datetime = "," date "(/" time ")?"
354                                 tzpattern = "^(:.*|" tzname offset "(" tzname \
355                                   "(" offset ")?(" datetime datetime ")?)?)$"
356                                 if (TZ ~ tzpattern) exit 1
357                                 exit 0
358                         }'
359                 do
360                     say >&2 "'$TZ' is not a conforming Posix time zone string."
361                 done
362                 TZ_for_date=$TZ;;
363         *)
364                 case $continent in
365                 coord)
366                     case $coord in
367                     '')
368                         echo >&2 'Please enter coordinates' \
369                                 'in ISO 6709 notation.'
370                         echo >&2 'For example, +4042-07403 stands for'
371                         echo >&2 '40 degrees 42 minutes north,' \
372                                 '74 degrees 3 minutes west.'
373                         read coord;;
374                     esac
375                     distance_table=`$AWK \
376                             -v coord="$coord" \
377                             -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
378                             "$output_distances" <"$TZ_ZONE_TABLE" |
379                       sort -n |
380                       sed "${location_limit}q"
381                     `
382                     regions=`say "$distance_table" | $AWK '
383                       BEGIN { FS = "\t" }
384                       { print $NF }
385                     '`
386                     echo >&2 'Please select one of the following' \
387                             'time zone regions,'
388                     echo >&2 'listed roughly in increasing order' \
389                             "of distance from $coord".
390                     doselect $regions
391                     region=$select_result
392                     TZ=`say "$distance_table" | $AWK -v region="$region" '
393                       BEGIN { FS="\t" }
394                       $NF == region { print $4 }
395                     '`
396                     ;;
397                 *)
398                 # Get list of names of countries in the continent or ocean.
399                 countries=`$AWK \
400                         -v continent="$continent" \
401                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
402                 '
403                         BEGIN { FS = "\t" }
404                         /^#/ { next }
405                         $3 ~ ("^" continent "/") {
406                             ncc = split($1, cc, /,/)
407                             for (i = 1; i <= ncc; i++)
408                                 if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
409                         }
410                         END {
411                                 while (getline <TZ_COUNTRY_TABLE) {
412                                         if ($0 !~ /^#/) cc_name[$1] = $2
413                                 }
414                                 for (i = 1; i <= ccs; i++) {
415                                         country = cc_list[i]
416                                         if (cc_name[country]) {
417                                           country = cc_name[country]
418                                         }
419                                         print country
420                                 }
421                         }
422                 ' <"$TZ_ZONE_TABLE" | sort -f`
423
424
425                 # If there's more than one country, ask the user which one.
426                 case $countries in
427                 *"$newline"*)
428                         echo >&2 'Please select a country' \
429                                 'whose clocks agree with yours.'
430                         doselect $countries
431                         country=$select_result;;
432                 *)
433                         country=$countries
434                 esac
435
436
437                 # Get list of names of time zone rule regions in the country.
438                 regions=`$AWK \
439                         -v country="$country" \
440                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
441                 '
442                         BEGIN {
443                                 FS = "\t"
444                                 cc = country
445                                 while (getline <TZ_COUNTRY_TABLE) {
446                                         if ($0 !~ /^#/  &&  country == $2) {
447                                                 cc = $1
448                                                 break
449                                         }
450                                 }
451                         }
452                         /^#/ { next }
453                         $1 ~ cc { print $4 }
454                 ' <"$TZ_ZONE_TABLE"`
455
456
457                 # If there's more than one region, ask the user which one.
458                 case $regions in
459                 *"$newline"*)
460                         echo >&2 'Please select one of the following' \
461                                 'time zone regions.'
462                         doselect $regions
463                         region=$select_result;;
464                 *)
465                         region=$regions
466                 esac
467
468                 # Determine TZ from country and region.
469                 TZ=`$AWK \
470                         -v country="$country" \
471                         -v region="$region" \
472                         -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
473                 '
474                         BEGIN {
475                                 FS = "\t"
476                                 cc = country
477                                 while (getline <TZ_COUNTRY_TABLE) {
478                                         if ($0 !~ /^#/  &&  country == $2) {
479                                                 cc = $1
480                                                 break
481                                         }
482                                 }
483                         }
484                         /^#/ { next }
485                         $1 ~ cc && $4 == region { print $3 }
486                 ' <"$TZ_ZONE_TABLE"`
487                 esac
488
489                 # Make sure the corresponding zoneinfo file exists.
490                 TZ_for_date=$TZDIR/$TZ
491                 <"$TZ_for_date" || {
492                         say >&2 "$0: time zone files are not set up correctly"
493                         exit 1
494                 }
495         esac
496
497
498         # Use the proposed TZ to output the current date relative to UTC.
499         # Loop until they agree in seconds.
500         # Give up after 8 unsuccessful tries.
501
502         extra_info=
503         for i in 1 2 3 4 5 6 7 8
504         do
505                 TZdate=`LANG=C TZ="$TZ_for_date" date`
506                 UTdate=`LANG=C TZ=UTC0 date`
507                 TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
508                 UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
509                 case $TZsec in
510                 $UTsec)
511                         extra_info="
512 Local time is now:      $TZdate.
513 Universal Time is now:  $UTdate."
514                         break
515                 esac
516         done
517
518
519         # Output TZ info and ask the user to confirm.
520
521         echo >&2 ""
522         echo >&2 "The following information has been given:"
523         echo >&2 ""
524         case $country%$region%$coord in
525         ?*%?*%) say >&2 "       $country$newline        $region";;
526         ?*%%)   say >&2 "       $country";;
527         %?*%?*) say >&2 "       coord $coord$newline    $region";;
528         %%?*)   say >&2 "       coord $coord";;
529         *)      say >&2 "       TZ='$TZ'"
530         esac
531         say >&2 ""
532         say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
533         say >&2 "Is the above information OK?"
534
535         doselect Yes No
536         ok=$select_result
537         case $ok in
538         Yes) break
539         esac
540 do coord=
541 done
542
543 case $SHELL in
544 *csh) file=.login line="setenv TZ '$TZ'";;
545 *) file=.profile line="TZ='$TZ'; export TZ"
546 esac
547
548 say >&2 "
549 You can make this change permanent for yourself by appending the line
550         $line
551 to the file '$file' in your home directory; then log out and log in again.
552
553 Here is that TZ value again, this time on standard output so that you
554 can use the $0 command in shell scripts:"
555
556 say "$TZ"