Home | History | Annotate | Line # | Download | only in time
tzselect.ksh revision 1.23
      1   1.8  christos #! /bin/bash
      2   1.2     perry #
      3  1.18  christos # Ask the user about the time zone, and output the resulting TZ value to stdout.
      4  1.18  christos # Interact with the user via stderr and stdin.
      5  1.18  christos #
      6  1.23  christos #	$NetBSD: tzselect.ksh,v 1.23 2024/02/17 14:54:47 christos Exp $
      7   1.2     perry #
      8   1.8  christos PKGVERSION='(tzcode) '
      9   1.8  christos TZVERSION=see_Makefile
     10   1.8  christos REPORT_BUGS_TO=tz@iana.org
     11   1.5    kleink 
     12  1.15  christos # Contributed by Paul Eggert.  This file is in the public domain.
     13   1.1       jtc 
     14   1.1       jtc # Porting notes:
     15   1.1       jtc #
     16  1.10  christos # This script requires a Posix-like shell and prefers the extension of a
     17   1.8  christos # 'select' statement.  The 'select' statement was introduced in the
     18   1.8  christos # Korn shell and is available in Bash and other shell implementations.
     19   1.8  christos # If your host lacks both Bash and the Korn shell, you can get their
     20   1.8  christos # source from one of these locations:
     21   1.1       jtc #
     22  1.16  christos #	Bash <https://www.gnu.org/software/bash/>
     23   1.8  christos #	Korn Shell <http://www.kornshell.com/>
     24  1.19  christos #	MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm>
     25   1.1       jtc #
     26  1.19  christos # For portability to Solaris 10 /bin/sh (supported by Oracle through
     27  1.19  christos # January 2024) this script avoids some POSIX features and common
     28  1.19  christos # extensions, such as $(...) (which works sometimes but not others),
     29  1.19  christos # $((...)), ! CMD, ${#ID}, ${ID##PAT}, ${ID%%PAT}, and $10.
     30  1.19  christos 
     31  1.10  christos #
     32   1.1       jtc # This script also uses several features of modern awk programs.
     33   1.8  christos # If your host lacks awk, or has an old awk that does not conform to Posix,
     34   1.1       jtc # you can use either of the following free programs instead:
     35   1.1       jtc #
     36  1.16  christos #	Gawk (GNU awk) <https://www.gnu.org/software/gawk/>
     37  1.16  christos #	mawk <https://invisible-island.net/mawk/>
     38  1.21  christos #	nawk <https://github.com/onetrueawk/awk>
     39   1.1       jtc 
     40   1.1       jtc 
     41  1.23  christos # This script does not want path expansion.
     42  1.23  christos set -f
     43  1.23  christos 
     44   1.1       jtc # Specify default values for environment variables if they are unset.
     45   1.1       jtc : ${AWK=awk}
     46  1.23  christos : ${PWD=`pwd`}
     47  1.23  christos : ${TZDIR=$PWD}
     48   1.1       jtc 
     49  1.22  christos # Output one argument as-is to standard output, with trailing newline.
     50  1.14  christos # Safer than 'echo', which can mishandle '\' or leading '-'.
     51  1.14  christos say() {
     52  1.14  christos     printf '%s\n' "$1"
     53  1.14  christos }
     54  1.14  christos 
     55   1.1       jtc # Check for awk Posix compliance.
     56   1.1       jtc ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
     57   1.1       jtc [ $? = 123 ] || {
     58  1.14  christos 	say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
     59   1.1       jtc 	exit 1
     60   1.1       jtc }
     61   1.1       jtc 
     62   1.9  christos coord=
     63   1.9  christos location_limit=10
     64  1.11  christos zonetabtype=zone1970
     65   1.9  christos 
     66   1.9  christos usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
     67  1.18  christos Select a timezone interactively.
     68   1.6   mlelstv 
     69   1.9  christos Options:
     70   1.9  christos 
     71   1.9  christos   -c COORD
     72   1.9  christos     Instead of asking for continent and then country and then city,
     73   1.9  christos     ask for selection from time zones whose largest cities
     74   1.9  christos     are closest to the location with geographical coordinates COORD.
     75   1.9  christos     COORD should use ISO 6709 notation, for example, '-c +4852+00220'
     76   1.9  christos     for Paris (in degrees and minutes, North and East), or
     77   1.9  christos     '-c -35-058' for Buenos Aires (in degrees, South and West).
     78   1.9  christos 
     79   1.9  christos   -n LIMIT
     80   1.9  christos     Display at most LIMIT locations when -c is used (default $location_limit).
     81   1.9  christos 
     82   1.9  christos   --version
     83   1.9  christos     Output version information.
     84   1.9  christos 
     85   1.9  christos   --help
     86   1.9  christos     Output this help.
     87   1.9  christos 
     88   1.9  christos Report bugs to $REPORT_BUGS_TO."
     89   1.9  christos 
     90  1.10  christos # Ask the user to select from the function's arguments,
     91  1.10  christos # and assign the selected argument to the variable 'select_result'.
     92  1.22  christos # Exit on EOF or I/O error.  Use the shell's nicer 'select' builtin if
     93  1.22  christos # available, falling back on a portable substitute otherwise.
     94  1.10  christos if
     95  1.10  christos   case $BASH_VERSION in
     96  1.10  christos   ?*) : ;;
     97  1.10  christos   '')
     98  1.10  christos     # '; exit' should be redundant, but Dash doesn't properly fail without it.
     99  1.11  christos     (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
    100  1.10  christos   esac
    101  1.10  christos then
    102  1.10  christos   # Do this inside 'eval', as otherwise the shell might exit when parsing it
    103  1.10  christos   # even though it is never executed.
    104  1.10  christos   eval '
    105  1.10  christos     doselect() {
    106  1.10  christos       select select_result
    107  1.10  christos       do
    108  1.10  christos 	case $select_result in
    109  1.10  christos 	"") echo >&2 "Please enter a number in range." ;;
    110  1.10  christos 	?*) break
    111  1.10  christos 	esac
    112  1.10  christos       done || exit
    113  1.10  christos     }
    114  1.10  christos   '
    115  1.10  christos else
    116  1.10  christos   doselect() {
    117  1.10  christos     # Field width of the prompt numbers.
    118  1.23  christos     print_nargs_length="BEGIN {print length(\"$#\");}"
    119  1.23  christos     select_width=`$AWK "$print_nargs_length"`
    120  1.10  christos 
    121  1.10  christos     select_i=
    122  1.10  christos 
    123  1.10  christos     while :
    124  1.10  christos     do
    125  1.10  christos       case $select_i in
    126  1.10  christos       '')
    127  1.10  christos 	select_i=0
    128  1.10  christos 	for select_word
    129  1.10  christos 	do
    130  1.23  christos 	  select_i=`$AWK "BEGIN { print $select_i + 1 }"`
    131  1.10  christos 	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
    132  1.10  christos 	done ;;
    133  1.10  christos       *[!0-9]*)
    134  1.10  christos 	echo >&2 'Please enter a number in range.' ;;
    135  1.10  christos       *)
    136  1.10  christos 	if test 1 -le $select_i && test $select_i -le $#; then
    137  1.23  christos 	  shift `$AWK "BEGIN { print $select_i - 1 }"`
    138  1.10  christos 	  select_result=$1
    139  1.10  christos 	  break
    140  1.10  christos 	fi
    141  1.10  christos 	echo >&2 'Please enter a number in range.'
    142  1.10  christos       esac
    143  1.10  christos 
    144  1.10  christos       # Prompt and read input.
    145  1.10  christos       printf >&2 %s "${PS3-#? }"
    146  1.10  christos       read select_i || exit
    147  1.10  christos     done
    148  1.10  christos   }
    149  1.10  christos fi
    150  1.10  christos 
    151  1.12  christos while getopts c:n:t:-: opt
    152   1.9  christos do
    153   1.9  christos     case $opt$OPTARG in
    154   1.9  christos     c*)
    155   1.9  christos 	coord=$OPTARG ;;
    156   1.9  christos     n*)
    157   1.9  christos 	location_limit=$OPTARG ;;
    158  1.11  christos     t*) # Undocumented option, used for developer testing.
    159  1.11  christos 	zonetabtype=$OPTARG ;;
    160   1.9  christos     -help)
    161   1.9  christos 	exec echo "$usage" ;;
    162   1.9  christos     -version)
    163   1.9  christos 	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
    164   1.9  christos     -*)
    165  1.14  christos 	say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
    166   1.9  christos     *)
    167  1.14  christos 	say >&2 "$0: try '$0 --help'"; exit 1 ;;
    168   1.9  christos     esac
    169   1.9  christos done
    170   1.9  christos 
    171  1.23  christos shift `$AWK "BEGIN { print $OPTIND - 1 }"`
    172   1.9  christos case $# in
    173   1.9  christos 0) ;;
    174  1.14  christos *) say >&2 "$0: $1: unknown argument"; exit 1 ;;
    175   1.9  christos esac
    176   1.6   mlelstv 
    177   1.1       jtc # Make sure the tables are readable.
    178   1.1       jtc TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
    179  1.11  christos TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
    180   1.1       jtc for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
    181   1.1       jtc do
    182  1.11  christos 	<"$f" || {
    183  1.14  christos 		say >&2 "$0: time zone files are not set up correctly"
    184   1.1       jtc 		exit 1
    185   1.1       jtc 	}
    186   1.1       jtc done
    187   1.1       jtc 
    188  1.14  christos # If the current locale does not support UTF-8, convert data to current
    189  1.14  christos # locale's format if possible, as the shell aligns columns better that way.
    190  1.14  christos # Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
    191  1.19  christos $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' || {
    192  1.14  christos     { tmp=`(mktemp -d) 2>/dev/null` || {
    193  1.14  christos 	tmp=${TMPDIR-/tmp}/tzselect.$$ &&
    194  1.14  christos 	(umask 77 && mkdir -- "$tmp")
    195  1.14  christos     };} &&
    196  1.14  christos     trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
    197  1.14  christos     (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
    198  1.14  christos         2>/dev/null &&
    199  1.14  christos     TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
    200  1.14  christos     iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
    201  1.14  christos     TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
    202  1.19  christos }
    203  1.14  christos 
    204   1.1       jtc newline='
    205   1.1       jtc '
    206   1.1       jtc IFS=$newline
    207   1.1       jtc 
    208  1.22  christos # Awk script to output a country list.
    209  1.22  christos output_country_list='
    210  1.22  christos   BEGIN { FS = "\t" }
    211  1.22  christos   /^#$/ { next }
    212  1.22  christos   /^#[^@]/ { next }
    213  1.22  christos   {
    214  1.22  christos     commentary = $0 ~ /^#@/
    215  1.22  christos     if (commentary) {
    216  1.22  christos       col1ccs = substr($1, 3)
    217  1.22  christos       conts = $2
    218  1.22  christos     } else {
    219  1.22  christos       col1ccs = $1
    220  1.22  christos       conts = $3
    221  1.22  christos     }
    222  1.22  christos     ncc = split(col1ccs, cc, /,/)
    223  1.22  christos     ncont = split(conts, cont, /,/)
    224  1.22  christos     for (i = 1; i <= ncc; i++) {
    225  1.22  christos       elsewhere = commentary
    226  1.22  christos       for (ci = 1; ci <= ncont; ci++) {
    227  1.22  christos 	if (cont[ci] ~ continent_re) {
    228  1.22  christos 	  if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
    229  1.22  christos 	  elsewhere = 0
    230  1.22  christos 	}
    231  1.22  christos       }
    232  1.22  christos       if (elsewhere) {
    233  1.22  christos 	for (i = 1; i <= ncc; i++) {
    234  1.22  christos 	  cc_elsewhere[cc[i]] = 1
    235  1.22  christos 	}
    236  1.22  christos       }
    237  1.22  christos     }
    238  1.22  christos   }
    239  1.22  christos   END {
    240  1.22  christos 	  while (getline <TZ_COUNTRY_TABLE) {
    241  1.22  christos 		  if ($0 !~ /^#/) cc_name[$1] = $2
    242  1.22  christos 	  }
    243  1.22  christos 	  for (i = 1; i <= ccs; i++) {
    244  1.22  christos 		  country = cc_list[i]
    245  1.22  christos 		  if (cc_elsewhere[country]) continue
    246  1.22  christos 		  if (cc_name[country]) {
    247  1.22  christos 		    country = cc_name[country]
    248  1.22  christos 		  }
    249  1.22  christos 		  print country
    250  1.22  christos 	  }
    251  1.22  christos   }
    252  1.22  christos '
    253   1.1       jtc 
    254   1.9  christos # Awk script to read a time zone table and output the same table,
    255  1.22  christos # with each row preceded by its distance from 'here'.
    256  1.22  christos # If output_times is set, each row is instead preceded by its local time
    257  1.22  christos # and any apostrophes are escaped for the shell.
    258  1.22  christos output_distances_or_times='
    259   1.9  christos   BEGIN {
    260   1.9  christos     FS = "\t"
    261  1.22  christos     if (!output_times) {
    262  1.22  christos       while (getline <TZ_COUNTRY_TABLE)
    263  1.22  christos 	if ($0 ~ /^[^#]/)
    264  1.22  christos 	  country[$1] = $2
    265  1.22  christos       country["US"] = "US" # Otherwise the strings get too long.
    266  1.22  christos     }
    267   1.9  christos   }
    268  1.12  christos   function abs(x) {
    269  1.12  christos     return x < 0 ? -x : x;
    270  1.12  christos   }
    271  1.12  christos   function min(x, y) {
    272  1.12  christos     return x < y ? x : y;
    273  1.12  christos   }
    274  1.12  christos   function convert_coord(coord, deg, minute, ilen, sign, sec) {
    275   1.9  christos     if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
    276   1.9  christos       degminsec = coord
    277   1.9  christos       intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
    278   1.9  christos       minsec = degminsec - intdeg * 10000
    279   1.9  christos       intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
    280   1.9  christos       sec = minsec - intmin * 100
    281   1.9  christos       deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
    282   1.9  christos     } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
    283   1.9  christos       degmin = coord
    284   1.9  christos       intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
    285  1.12  christos       minute = degmin - intdeg * 100
    286  1.12  christos       deg = (intdeg * 60 + minute) / 60
    287   1.9  christos     } else
    288   1.9  christos       deg = coord
    289   1.9  christos     return deg * 0.017453292519943296
    290   1.9  christos   }
    291   1.9  christos   function convert_latitude(coord) {
    292   1.9  christos     match(coord, /..*[-+]/)
    293   1.9  christos     return convert_coord(substr(coord, 1, RLENGTH - 1))
    294   1.9  christos   }
    295   1.9  christos   function convert_longitude(coord) {
    296   1.9  christos     match(coord, /..*[-+]/)
    297   1.9  christos     return convert_coord(substr(coord, RLENGTH))
    298   1.9  christos   }
    299   1.9  christos   # Great-circle distance between points with given latitude and longitude.
    300   1.9  christos   # Inputs and output are in radians.  This uses the great-circle special
    301   1.9  christos   # case of the Vicenty formula for distances on ellipsoids.
    302  1.12  christos   function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
    303   1.9  christos     dlong = long2 - long1
    304  1.13  christos     x = cos(lat2) * sin(dlong)
    305  1.13  christos     y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
    306  1.13  christos     num = sqrt(x * x + y * y)
    307  1.13  christos     denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
    308   1.9  christos     return atan2(num, denom)
    309   1.9  christos   }
    310  1.12  christos   # Parallel distance between points with given latitude and longitude.
    311  1.12  christos   # This is the product of the longitude difference and the cosine
    312  1.12  christos   # of the latitude of the point that is further from the equator.
    313  1.12  christos   # I.e., it considers longitudes to be further apart if they are
    314  1.12  christos   # nearer the equator.
    315  1.12  christos   function pardist(lat1, long1, lat2, long2) {
    316  1.13  christos     return abs(long1 - long2) * min(cos(lat1), cos(lat2))
    317  1.12  christos   }
    318  1.12  christos   # The distance function is the sum of the great-circle distance and
    319  1.12  christos   # the parallel distance.  It could be weighted.
    320  1.12  christos   function dist(lat1, long1, lat2, long2) {
    321  1.13  christos     return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
    322  1.12  christos   }
    323   1.9  christos   BEGIN {
    324   1.9  christos     coord_lat = convert_latitude(coord)
    325   1.9  christos     coord_long = convert_longitude(coord)
    326   1.9  christos   }
    327   1.9  christos   /^[^#]/ {
    328  1.22  christos     inline[inlines++] = $0
    329  1.22  christos     ncc = split($1, cc, /,/)
    330  1.22  christos     for (i = 1; i <= ncc; i++)
    331  1.22  christos       cc_used[cc[i]]++
    332  1.22  christos   }
    333  1.22  christos   END {
    334  1.22  christos    for (h = 0; h < inlines; h++) {
    335  1.22  christos     $0 = inline[h]
    336  1.11  christos     line = $1 "\t" $2 "\t" $3
    337  1.11  christos     sep = "\t"
    338  1.11  christos     ncc = split($1, cc, /,/)
    339  1.22  christos     split("", item_seen)
    340  1.22  christos     item_seen[""] = 1
    341  1.11  christos     for (i = 1; i <= ncc; i++) {
    342  1.22  christos       item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4
    343  1.22  christos       if (item_seen[item]++) continue
    344  1.22  christos       line = line sep item
    345  1.22  christos       sep = "; "
    346  1.22  christos     }
    347  1.22  christos     if (output_times) {
    348  1.22  christos       fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n"
    349  1.22  christos       gsub(/'\''/, "&\\\\&&", line)
    350  1.22  christos       printf fmt, $3, h, line
    351  1.22  christos     } else {
    352  1.22  christos       here_lat = convert_latitude($2)
    353  1.22  christos       here_long = convert_longitude($2)
    354  1.22  christos       printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
    355  1.11  christos     }
    356  1.22  christos    }
    357   1.9  christos   }
    358   1.9  christos '
    359   1.1       jtc 
    360   1.1       jtc # Begin the main loop.  We come back here if the user wants to retry.
    361   1.1       jtc while
    362   1.1       jtc 
    363   1.1       jtc 	echo >&2 'Please identify a location' \
    364   1.1       jtc 		'so that time zone rules can be set correctly.'
    365   1.1       jtc 
    366   1.1       jtc 	continent=
    367   1.1       jtc 	country=
    368   1.1       jtc 	region=
    369   1.1       jtc 
    370   1.9  christos 	case $coord in
    371   1.9  christos 	?*)
    372   1.9  christos 		continent=coord;;
    373   1.9  christos 	'')
    374   1.1       jtc 
    375   1.1       jtc 	# Ask the user for continent or ocean.
    376   1.1       jtc 
    377  1.22  christos 	echo >&2 'Please select a continent, ocean, "coord", "TZ", or "time".'
    378   1.1       jtc 
    379  1.10  christos         quoted_continents=`
    380  1.10  christos 	  $AWK '
    381  1.20  christos 	    function handle_entry(entry) {
    382  1.20  christos 	      entry = substr(entry, 1, index(entry, "/") - 1)
    383  1.20  christos 	      if (entry == "America")
    384  1.20  christos 	       entry = entry "s"
    385  1.20  christos 	      if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
    386  1.20  christos 	       entry = entry " Ocean"
    387  1.20  christos 	      printf "'\''%s'\''\n", entry
    388  1.20  christos 	    }
    389  1.10  christos 	    BEGIN { FS = "\t" }
    390   1.9  christos 	    /^[^#]/ {
    391  1.20  christos               handle_entry($3)
    392   1.9  christos             }
    393  1.20  christos 	    /^#@/ {
    394  1.20  christos 	      ncont = split($2, cont, /,/)
    395  1.20  christos 	      for (ci = 1; ci <= ncont; ci++) {
    396  1.20  christos 	        handle_entry(cont[ci])
    397  1.20  christos 	      }
    398  1.20  christos 	    }
    399  1.11  christos           ' <"$TZ_ZONE_TABLE" |
    400   1.9  christos 	  sort -u |
    401   1.9  christos 	  tr '\n' ' '
    402   1.9  christos 	  echo ''
    403  1.10  christos 	`
    404   1.9  christos 
    405   1.9  christos 	eval '
    406  1.10  christos 	    doselect '"$quoted_continents"' \
    407   1.9  christos 		"coord - I want to use geographical coordinates." \
    408  1.23  christos 		"TZ - I want to specify the timezone using a POSIX.1-2017 TZ string." \
    409  1.22  christos 		"time - I know local time already."
    410  1.10  christos 	    continent=$select_result
    411  1.10  christos 	    case $continent in
    412  1.10  christos 	    Americas) continent=America;;
    413  1.23  christos 	    *)
    414  1.23  christos 		# Get the first word of $continent.  Path expansion is disabled
    415  1.23  christos 		# so this works even with "*", which should not happen.
    416  1.23  christos 		IFS=" "
    417  1.23  christos 		for continent in $continent ""; do break; done
    418  1.23  christos 		IFS=$newline;;
    419  1.10  christos 	    esac
    420   1.9  christos 	'
    421   1.9  christos 	esac
    422   1.9  christos 
    423   1.1       jtc 	case $continent in
    424   1.9  christos 	TZ)
    425  1.23  christos 		# Ask the user for a POSIX.1-2017 TZ string.  Check that it conforms.
    426   1.1       jtc 		while
    427   1.1       jtc 			echo >&2 'Please enter the desired value' \
    428   1.1       jtc 				'of the TZ environment variable.'
    429  1.18  christos 			echo >&2 'For example, AEST-10 is abbreviated' \
    430  1.18  christos 				'AEST and is 10 hours'
    431  1.17  christos 			echo >&2 'ahead (east) of Greenwich,' \
    432  1.17  christos 				'with no daylight saving time.'
    433   1.1       jtc 			read TZ
    434   1.1       jtc 			$AWK -v TZ="$TZ" 'BEGIN {
    435  1.15  christos 				tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
    436  1.15  christos 				time = "(2[0-4]|[0-1]?[0-9])" \
    437  1.15  christos 				  "(:[0-5][0-9](:[0-5][0-9])?)?"
    438   1.1       jtc 				offset = "[-+]?" time
    439  1.15  christos 				mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
    440  1.15  christos 				jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
    441  1.15  christos 				  "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
    442  1.15  christos 				datetime = ",(" mdate "|" jdate ")(/" time ")?"
    443   1.1       jtc 				tzpattern = "^(:.*|" tzname offset "(" tzname \
    444   1.1       jtc 				  "(" offset ")?(" datetime datetime ")?)?)$"
    445   1.1       jtc 				if (TZ ~ tzpattern) exit 1
    446   1.1       jtc 				exit 0
    447   1.1       jtc 			}'
    448   1.1       jtc 		do
    449  1.23  christos 		    say >&2 "'$tz' is not a conforming POSIX.1-2017 timezone string."
    450   1.1       jtc 		done
    451   1.1       jtc 		TZ_for_date=$TZ;;
    452   1.1       jtc 	*)
    453   1.9  christos 		case $continent in
    454   1.9  christos 		coord)
    455   1.9  christos 		    case $coord in
    456   1.9  christos 		    '')
    457   1.9  christos 			echo >&2 'Please enter coordinates' \
    458   1.9  christos 				'in ISO 6709 notation.'
    459   1.9  christos 			echo >&2 'For example, +4042-07403 stands for'
    460   1.9  christos 			echo >&2 '40 degrees 42 minutes north,' \
    461   1.9  christos 				'74 degrees 3 minutes west.'
    462   1.9  christos 			read coord;;
    463   1.9  christos 		    esac
    464  1.10  christos 		    distance_table=`$AWK \
    465   1.9  christos 			    -v coord="$coord" \
    466   1.9  christos 			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    467  1.22  christos 			    "$output_distances_or_times" <"$TZ_ZONE_TABLE" |
    468   1.9  christos 		      sort -n |
    469  1.23  christos 		      $AWK "{print} NR == $location_limit { exit }"
    470  1.10  christos 		    `
    471  1.22  christos 		    regions=`$AWK \
    472  1.22  christos 		      -v distance_table="$distance_table" '
    473  1.22  christos 		      BEGIN {
    474  1.22  christos 		        nlines = split(distance_table, line, /\n/)
    475  1.22  christos 			for (nr = 1; nr <= nlines; nr++) {
    476  1.22  christos 			  nf = split(line[nr], f, /\t/)
    477  1.22  christos 			  print f[nf]
    478  1.22  christos 			}
    479  1.22  christos 		      }
    480  1.10  christos 		    '`
    481  1.22  christos 		    echo >&2 'Please select one of the following timezones,'
    482   1.9  christos 		    echo >&2 'listed roughly in increasing order' \
    483   1.9  christos 			    "of distance from $coord".
    484  1.10  christos 		    doselect $regions
    485  1.10  christos 		    region=$select_result
    486  1.22  christos 		    TZ=`$AWK \
    487  1.22  christos 		      -v distance_table="$distance_table" \
    488  1.22  christos 		      -v region="$region" '
    489  1.22  christos 		      BEGIN {
    490  1.22  christos 		        nlines = split(distance_table, line, /\n/)
    491  1.22  christos 			for (nr = 1; nr <= nlines; nr++) {
    492  1.22  christos 			  nf = split(line[nr], f, /\t/)
    493  1.22  christos 			  if (f[nf] == region) {
    494  1.22  christos 			    print f[4]
    495  1.22  christos 			  }
    496  1.22  christos 			}
    497  1.22  christos 		      }
    498  1.10  christos 		    '`
    499   1.9  christos 		    ;;
    500   1.9  christos 		*)
    501  1.22  christos 		case $continent in
    502  1.22  christos 		time)
    503  1.22  christos 		  minute_format='%a %b %d %H:%M'
    504  1.22  christos 		  old_minute=`TZ=UTC0 date +"$minute_format"`
    505  1.22  christos 		  for i in 1 2 3
    506  1.22  christos 		  do
    507  1.22  christos 		    time_table_command=`
    508  1.22  christos 		      $AWK -v output_times=1 \
    509  1.22  christos 			  "$output_distances_or_times" <"$TZ_ZONE_TABLE"
    510  1.22  christos 		    `
    511  1.22  christos 		    time_table=`eval "$time_table_command"`
    512  1.22  christos 		    new_minute=`TZ=UTC0 date +"$minute_format"`
    513  1.22  christos 		    case $old_minute in
    514  1.22  christos 		    "$new_minute") break;;
    515  1.22  christos 		    esac
    516  1.22  christos 		    old_minute=$new_minute
    517  1.22  christos 		  done
    518  1.22  christos 		  echo >&2 "The system says Universal Time is $new_minute."
    519  1.22  christos 		  echo >&2 "Assuming that's correct, what is the local time?"
    520  1.22  christos 		  eval doselect `
    521  1.22  christos 		    say "$time_table" |
    522  1.22  christos 		    sort -k2n -k2,5 -k1n |
    523  1.22  christos 		    $AWK '{
    524  1.22  christos 		      line = $6 " " $7 " " $4 " " $5
    525  1.22  christos 		      if (line == oldline) next
    526  1.22  christos 		      oldline = line
    527  1.22  christos 		      gsub(/'\''/, "&\\\\&&", line)
    528  1.22  christos 		      printf "'\''%s'\''\n", line
    529  1.22  christos 		    }'
    530  1.22  christos 		  `
    531  1.22  christos 		  time=$select_result
    532  1.22  christos 		  zone_table=`
    533  1.22  christos 		    say "$time_table" |
    534  1.22  christos 		    $AWK -v time="$time" '{
    535  1.22  christos 		      if ($6 " " $7 " " $4 " " $5 == time) {
    536  1.22  christos 			sub(/[^\t]*\t/, "")
    537  1.22  christos 			print
    538  1.22  christos 		      }
    539  1.22  christos 		    }'
    540  1.22  christos 		  `
    541  1.22  christos 		  countries=`
    542  1.22  christos 		     say "$zone_table" |
    543  1.22  christos 		     $AWK \
    544  1.22  christos 			-v continent_re='' \
    545  1.22  christos 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    546  1.22  christos 			"$output_country_list" |
    547  1.22  christos 		     sort -f
    548  1.22  christos 		  `
    549  1.22  christos 		  ;;
    550  1.22  christos 		*)
    551  1.22  christos 		  zone_table=file
    552  1.22  christos 		  # Get list of names of countries in the continent or ocean.
    553  1.22  christos 		  countries=`$AWK \
    554  1.20  christos 			-v continent_re="^$continent/" \
    555   1.1       jtc 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    556  1.22  christos 			"$output_country_list" \
    557  1.22  christos 			<"$TZ_ZONE_TABLE" | sort -f
    558  1.22  christos 		  `;;
    559  1.22  christos 		esac
    560   1.1       jtc 
    561   1.1       jtc 		# If there's more than one country, ask the user which one.
    562   1.1       jtc 		case $countries in
    563   1.1       jtc 		*"$newline"*)
    564   1.9  christos 			echo >&2 'Please select a country' \
    565   1.9  christos 				'whose clocks agree with yours.'
    566  1.10  christos 			doselect $countries
    567  1.22  christos 			country_result=$select_result
    568  1.10  christos 			country=$select_result;;
    569   1.1       jtc 		*)
    570   1.1       jtc 			country=$countries
    571   1.1       jtc 		esac
    572   1.1       jtc 
    573   1.1       jtc 
    574  1.18  christos 		# Get list of timezones in the country.
    575  1.22  christos 		regions=`
    576  1.22  christos 		  case $zone_table in
    577  1.22  christos 		  file) cat -- "$TZ_ZONE_TABLE";;
    578  1.22  christos 		  *) say "$zone_table";;
    579  1.22  christos 		  esac |
    580  1.22  christos 		  $AWK \
    581   1.1       jtc 			-v country="$country" \
    582   1.1       jtc 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    583  1.22  christos 		    '
    584   1.1       jtc 			BEGIN {
    585  1.10  christos 				FS = "\t"
    586   1.1       jtc 				cc = country
    587   1.1       jtc 				while (getline <TZ_COUNTRY_TABLE) {
    588   1.1       jtc 					if ($0 !~ /^#/  &&  country == $2) {
    589   1.1       jtc 						cc = $1
    590   1.1       jtc 						break
    591   1.1       jtc 					}
    592   1.1       jtc 				}
    593   1.1       jtc 			}
    594  1.14  christos 			/^#/ { next }
    595  1.11  christos 			$1 ~ cc { print $4 }
    596  1.22  christos 		    '
    597  1.22  christos 		`
    598   1.1       jtc 
    599   1.1       jtc 
    600   1.1       jtc 		# If there's more than one region, ask the user which one.
    601   1.1       jtc 		case $regions in
    602   1.1       jtc 		*"$newline"*)
    603  1.18  christos 			echo >&2 'Please select one of the following timezones.'
    604  1.10  christos 			doselect $regions
    605  1.22  christos 			region=$select_result
    606   1.1       jtc 		esac
    607   1.1       jtc 
    608   1.1       jtc 		# Determine TZ from country and region.
    609  1.22  christos 		TZ=`
    610  1.22  christos 		  case $zone_table in
    611  1.22  christos 		  file) cat -- "$TZ_ZONE_TABLE";;
    612  1.22  christos 		  *) say "$zone_table";;
    613  1.22  christos 		  esac |
    614  1.22  christos 		  $AWK \
    615   1.1       jtc 			-v country="$country" \
    616   1.1       jtc 			-v region="$region" \
    617   1.1       jtc 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    618  1.22  christos 		    '
    619   1.1       jtc 			BEGIN {
    620  1.10  christos 				FS = "\t"
    621   1.1       jtc 				cc = country
    622   1.1       jtc 				while (getline <TZ_COUNTRY_TABLE) {
    623   1.1       jtc 					if ($0 !~ /^#/  &&  country == $2) {
    624   1.1       jtc 						cc = $1
    625   1.1       jtc 						break
    626   1.1       jtc 					}
    627   1.1       jtc 				}
    628   1.1       jtc 			}
    629  1.14  christos 			/^#/ { next }
    630  1.22  christos 			$1 ~ cc && ($4 == region || !region) { print $3 }
    631  1.22  christos 		    '
    632  1.22  christos 		`;;
    633   1.9  christos 		esac
    634   1.1       jtc 
    635   1.1       jtc 		# Make sure the corresponding zoneinfo file exists.
    636   1.1       jtc 		TZ_for_date=$TZDIR/$TZ
    637  1.11  christos 		<"$TZ_for_date" || {
    638  1.14  christos 			say >&2 "$0: time zone files are not set up correctly"
    639   1.1       jtc 			exit 1
    640   1.1       jtc 		}
    641   1.1       jtc 	esac
    642   1.1       jtc 
    643   1.1       jtc 
    644   1.1       jtc 	# Use the proposed TZ to output the current date relative to UTC.
    645   1.1       jtc 	# Loop until they agree in seconds.
    646   1.1       jtc 	# Give up after 8 unsuccessful tries.
    647   1.1       jtc 
    648   1.1       jtc 	extra_info=
    649   1.1       jtc 	for i in 1 2 3 4 5 6 7 8
    650   1.1       jtc 	do
    651  1.10  christos 		TZdate=`LANG=C TZ="$TZ_for_date" date`
    652  1.10  christos 		UTdate=`LANG=C TZ=UTC0 date`
    653  1.23  christos 		if $AWK '
    654  1.23  christos 		      function getsecs(d) {
    655  1.23  christos 			return match(d, /.*:[0-5][0-9]/) ? substr(d, RLENGTH - 1, 2) : ""
    656  1.23  christos 		      }
    657  1.23  christos 		      BEGIN { exit getsecs(ARGV[1]) != getsecs(ARGV[2]) }
    658  1.23  christos 		   ' ="$TZdate" ="$UTdate"
    659  1.23  christos 		then
    660   1.1       jtc 			extra_info="
    661  1.15  christos Selected time is now:	$TZdate.
    662   1.1       jtc Universal Time is now:	$UTdate."
    663   1.1       jtc 			break
    664  1.23  christos 		fi
    665   1.1       jtc 	done
    666   1.1       jtc 
    667   1.1       jtc 
    668   1.1       jtc 	# Output TZ info and ask the user to confirm.
    669   1.1       jtc 
    670   1.1       jtc 	echo >&2 ""
    671  1.22  christos 	echo >&2 "Based on the following information:"
    672   1.1       jtc 	echo >&2 ""
    673  1.22  christos 	case $time%$country_result%$region%$coord in
    674  1.22  christos 	?*%?*%?*%)
    675  1.22  christos 	  say >&2 "	$time$newline	$country_result$newline	$region";;
    676  1.22  christos 	?*%?*%%|?*%%?*%) say >&2 "	$time$newline	$country_result$region";;
    677  1.22  christos 	?*%%%) say >&2 "	$time";;
    678  1.22  christos 	%?*%?*%) say >&2 "	$country_result$newline	$region";;
    679  1.22  christos 	%?*%%)	say >&2 "	$country_result";;
    680  1.22  christos 	%%?*%?*) say >&2 "	coord $coord$newline	$region";;
    681  1.22  christos 	%%%?*)	say >&2 "	coord $coord";;
    682  1.14  christos 	*)	say >&2 "	TZ='$TZ'"
    683   1.1       jtc 	esac
    684  1.14  christos 	say >&2 ""
    685  1.22  christos 	say >&2 "TZ='$TZ' will be used.$extra_info"
    686  1.14  christos 	say >&2 "Is the above information OK?"
    687   1.1       jtc 
    688  1.10  christos 	doselect Yes No
    689  1.10  christos 	ok=$select_result
    690   1.1       jtc 	case $ok in
    691   1.1       jtc 	Yes) break
    692   1.1       jtc 	esac
    693   1.9  christos do coord=
    694   1.1       jtc done
    695   1.1       jtc 
    696   1.5    kleink case $SHELL in
    697   1.5    kleink *csh) file=.login line="setenv TZ '$TZ'";;
    698   1.5    kleink *) file=.profile line="TZ='$TZ'; export TZ"
    699   1.5    kleink esac
    700   1.5    kleink 
    701  1.15  christos test -t 1 && say >&2 "
    702   1.5    kleink You can make this change permanent for yourself by appending the line
    703   1.5    kleink 	$line
    704   1.5    kleink to the file '$file' in your home directory; then log out and log in again.
    705   1.5    kleink 
    706   1.5    kleink Here is that TZ value again, this time on standard output so that you
    707   1.5    kleink can use the $0 command in shell scripts:"
    708   1.5    kleink 
    709  1.14  christos say "$TZ"
    710