about summary refs log tree commit diff
path: root/timezone/tzselect.ksh
diff options
context:
space:
mode:
Diffstat (limited to 'timezone/tzselect.ksh')
-rw-r--r--timezone/tzselect.ksh366
1 files changed, 276 insertions, 90 deletions
diff --git a/timezone/tzselect.ksh b/timezone/tzselect.ksh
index 8e66b44273..9d7069116a 100644
--- a/timezone/tzselect.ksh
+++ b/timezone/tzselect.ksh
@@ -11,7 +11,7 @@ REPORT_BUGS_TO=tz@iana.org
 
 # Porting notes:
 #
-# This script requires a Posix-like shell with the extension of a
+# This script requires a Posix-like shell and prefers the extension of a
 # 'select' statement.  The 'select' statement was introduced in the
 # Korn shell and is available in Bash and other shell implementations.
 # If your host lacks both Bash and the Korn shell, you can get their
@@ -21,6 +21,10 @@ REPORT_BUGS_TO=tz@iana.org
 #	Korn Shell <http://www.kornshell.com/>
 #	Public Domain Korn Shell <http://www.cs.mun.ca/~michael/pdksh/>
 #
+# For portability to Solaris 9 /bin/sh this script avoids some POSIX
+# features and common extensions, such as $(...) (which works sometimes
+# but not others), $((...)), and $10.
+#
 # This script also uses several features of modern awk programs.
 # If your host lacks awk, or has an old awk that does not conform to Posix,
 # you can use either of the following free programs instead:
@@ -31,7 +35,7 @@ REPORT_BUGS_TO=tz@iana.org
 
 # Specify default values for environment variables if they are unset.
 : ${AWK=awk}
-: ${TZDIR=$(pwd)}
+: ${TZDIR=`pwd`}
 
 # Check for awk Posix compliance.
 ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
@@ -40,21 +44,125 @@ REPORT_BUGS_TO=tz@iana.org
 	exit 1
 }
 
-if [ "$1" = "--help" ]; then
-    cat <<EOF
-Usage: tzselect
+coord=
+location_limit=10
+
+usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
 Select a time zone interactively.
 
-Report bugs to $REPORT_BUGS_TO.
-EOF
-    exit
-elif [ "$1" = "--version" ]; then
-    cat <<EOF
-tzselect $PKGVERSION$TZVERSION
-EOF
-    exit
+Options:
+
+  -c COORD
+    Instead of asking for continent and then country and then city,
+    ask for selection from time zones whose largest cities
+    are closest to the location with geographical coordinates COORD.
+    COORD should use ISO 6709 notation, for example, '-c +4852+00220'
+    for Paris (in degrees and minutes, North and East), or
+    '-c -35-058' for Buenos Aires (in degrees, South and West).
+
+  -n LIMIT
+    Display at most LIMIT locations when -c is used (default $location_limit).
+
+  --version
+    Output version information.
+
+  --help
+    Output this help.
+
+Report bugs to $REPORT_BUGS_TO."
+
+# Ask the user to select from the function's arguments,
+# and assign the selected argument to the variable 'select_result'.
+# Exit on EOF or I/O error.  Use the shell's 'select' builtin if available,
+# falling back on a less-nice but portable substitute otherwise.
+if
+  case $BASH_VERSION in
+  ?*) : ;;
+  '')
+    # '; exit' should be redundant, but Dash doesn't properly fail without it.
+    (eval 'set --; select x; do break; done; exit') 2>/dev/null
+  esac
+then
+  # Do this inside 'eval', as otherwise the shell might exit when parsing it
+  # even though it is never executed.
+  eval '
+    doselect() {
+      select select_result
+      do
+	case $select_result in
+	"") echo >&2 "Please enter a number in range." ;;
+	?*) break
+	esac
+      done || exit
+    }
+
+    # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
+    case $BASH_VERSION in
+    [01].*)
+      case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
+      ?*) PS3=
+      esac
+    esac
+  '
+else
+  doselect() {
+    # Field width of the prompt numbers.
+    select_width=`expr $# : '.*'`
+
+    select_i=
+
+    while :
+    do
+      case $select_i in
+      '')
+	select_i=0
+	for select_word
+	do
+	  select_i=`expr $select_i + 1`
+	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
+	done ;;
+      *[!0-9]*)
+	echo >&2 'Please enter a number in range.' ;;
+      *)
+	if test 1 -le $select_i && test $select_i -le $#; then
+	  shift `expr $select_i - 1`
+	  select_result=$1
+	  break
+	fi
+	echo >&2 'Please enter a number in range.'
+      esac
+
+      # Prompt and read input.
+      printf >&2 %s "${PS3-#? }"
+      read select_i || exit
+    done
+  }
 fi
 
+while getopts c:n:-: opt
+do
+    case $opt$OPTARG in
+    c*)
+	coord=$OPTARG ;;
+    n*)
+	location_limit=$OPTARG ;;
+    -help)
+	exec echo "$usage" ;;
+    -version)
+	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
+    -*)
+	echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
+    *)
+	echo >&2 "$0: try '$0 --help'"; exit 1 ;;
+    esac
+done
+
+shift `expr $OPTIND - 1`
+case $# in
+0) ;;
+*) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
+esac
+
 # Make sure the tables are readable.
 TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
 TZ_ZONE_TABLE=$TZDIR/zone.tab
@@ -71,11 +179,65 @@ newline='
 IFS=$newline
 
 
-# Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
-case $(echo 1 | (select x in x; do break; done) 2>/dev/null) in
-?*) PS3=
-esac
-
+# Awk script to read a time zone table and output the same table,
+# with each column preceded by its distance from 'here'.
+output_distances='
+  BEGIN {
+    FS = "\t"
+    while (getline <TZ_COUNTRY_TABLE)
+      if ($0 ~ /^[^#]/)
+        country[$1] = $2
+    country["US"] = "US" # Otherwise the strings get too long.
+  }
+  function convert_coord(coord, deg, min, ilen, sign, sec) {
+    if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
+      degminsec = coord
+      intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
+      minsec = degminsec - intdeg * 10000
+      intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
+      sec = minsec - intmin * 100
+      deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
+    } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
+      degmin = coord
+      intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
+      min = degmin - intdeg * 100
+      deg = (intdeg * 60 + min) / 60
+    } else
+      deg = coord
+    return deg * 0.017453292519943296
+  }
+  function convert_latitude(coord) {
+    match(coord, /..*[-+]/)
+    return convert_coord(substr(coord, 1, RLENGTH - 1))
+  }
+  function convert_longitude(coord) {
+    match(coord, /..*[-+]/)
+    return convert_coord(substr(coord, RLENGTH))
+  }
+  # Great-circle distance between points with given latitude and longitude.
+  # Inputs and output are in radians.  This uses the great-circle special
+  # case of the Vicenty formula for distances on ellipsoids.
+  function dist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
+    dlong = long2 - long1
+    x = cos (lat2) * sin (dlong)
+    y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
+    num = sqrt (x * x + y * y)
+    denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
+    return atan2(num, denom)
+  }
+  BEGIN {
+    coord_lat = convert_latitude(coord)
+    coord_long = convert_longitude(coord)
+  }
+  /^[^#]/ {
+    here_lat = convert_latitude($2)
+    here_long = convert_longitude($2)
+    line = $1 "\t" $2 "\t" $3 "\t" country[$1]
+    if (NF == 4)
+      line = line " - " $4
+    printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
+  }
+'
 
 # Begin the main loop.  We come back here if the user wants to retry.
 while
@@ -87,39 +249,46 @@ while
 	country=
 	region=
 
+	case $coord in
+	?*)
+		continent=coord;;
+	'')
 
 	# Ask the user for continent or ocean.
 
-	echo >&2 'Please select a continent or ocean.'
-
-	select continent in \
-	    Africa \
-	    Americas \
-	    Antarctica \
-	    'Arctic Ocean' \
-	    Asia \
-	    'Atlantic Ocean' \
-	    Australia \
-	    Europe \
-	    'Indian Ocean' \
-	    'Pacific Ocean' \
-	    'none - I want to specify the time zone using the Posix TZ format.'
-	do
+	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
+
+        quoted_continents=`
+	  $AWK '
+	    BEGIN { FS = "\t" }
+	    /^[^#]/ {
+              entry = substr($3, 1, index($3, "/") - 1)
+              if (entry == "America")
+		entry = entry "s"
+              if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
+		entry = entry " Ocean"
+              printf "'\''%s'\''\n", entry
+            }
+          ' $TZ_ZONE_TABLE |
+	  sort -u |
+	  tr '\n' ' '
+	  echo ''
+	`
+
+	eval '
+	    doselect '"$quoted_continents"' \
+		"coord - I want to use geographical coordinates." \
+		"TZ - I want to specify the time zone using the Posix TZ format."
+	    continent=$select_result
 	    case $continent in
-	    '')
-		echo >&2 'Please enter a number in range.';;
-	    ?*)
-		case $continent in
-		Americas) continent=America;;
-		*' '*) continent=$(expr "$continent" : '\([^ ]*\)')
-		esac
-		break
+	    Americas) continent=America;;
+	    *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
 	    esac
-	done
+	'
+	esac
+
 	case $continent in
-	'')
-		exit 1;;
-	none)
+	TZ)
 		# Ask the user for a Posix TZ string.  Check that it conforms.
 		while
 			echo >&2 'Please enter the desired value' \
@@ -144,11 +313,46 @@ while
 		done
 		TZ_for_date=$TZ;;
 	*)
+		case $continent in
+		coord)
+		    case $coord in
+		    '')
+			echo >&2 'Please enter coordinates' \
+				'in ISO 6709 notation.'
+			echo >&2 'For example, +4042-07403 stands for'
+			echo >&2 '40 degrees 42 minutes north,' \
+				'74 degrees 3 minutes west.'
+			read coord;;
+		    esac
+		    distance_table=`$AWK \
+			    -v coord="$coord" \
+			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
+			    "$output_distances" <$TZ_ZONE_TABLE |
+		      sort -n |
+		      sed "${location_limit}q"
+		    `
+		    regions=`echo "$distance_table" | $AWK '
+		      BEGIN { FS = "\t" }
+		      { print $NF }
+		    '`
+		    echo >&2 'Please select one of the following' \
+			    'time zone regions,'
+		    echo >&2 'listed roughly in increasing order' \
+			    "of distance from $coord".
+		    doselect $regions
+		    region=$select_result
+		    TZ=`echo "$distance_table" | $AWK -v region="$region" '
+		      BEGIN { FS="\t" }
+		      $NF == region { print $4 }
+		    '`
+		    ;;
+		*)
 		# Get list of names of countries in the continent or ocean.
-		countries=$($AWK -F'\t' \
+		countries=`$AWK \
 			-v continent="$continent" \
 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
 		'
+			BEGIN { FS = "\t" }
 			/^#/ { next }
 			$3 ~ ("^" continent "/") {
 				if (!cc_seen[$1]++) cc_list[++ccs] = $1
@@ -165,35 +369,28 @@ while
 					print country
 				}
 			}
-		' <$TZ_ZONE_TABLE | sort -f)
+		' <$TZ_ZONE_TABLE | sort -f`
 
 
 		# If there's more than one country, ask the user which one.
 		case $countries in
 		*"$newline"*)
-			echo >&2 'Please select a country.'
-			select country in $countries
-			do
-			    case $country in
-			    '') echo >&2 'Please enter a number in range.';;
-			    ?*) break
-			    esac
-			done
-
-			case $country in
-			'') exit 1
-			esac;;
+			echo >&2 'Please select a country' \
+				'whose clocks agree with yours.'
+			doselect $countries
+			country=$select_result;;
 		*)
 			country=$countries
 		esac
 
 
 		# Get list of names of time zone rule regions in the country.
-		regions=$($AWK -F'\t' \
+		regions=`$AWK \
 			-v country="$country" \
 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
 		'
 			BEGIN {
+				FS = "\t"
 				cc = country
 				while (getline <TZ_COUNTRY_TABLE) {
 					if ($0 !~ /^#/  &&  country == $2) {
@@ -203,7 +400,7 @@ while
 				}
 			}
 			$1 == cc { print $4 }
-		' <$TZ_ZONE_TABLE)
+		' <$TZ_ZONE_TABLE`
 
 
 		# If there's more than one region, ask the user which one.
@@ -211,27 +408,20 @@ while
 		*"$newline"*)
 			echo >&2 'Please select one of the following' \
 				'time zone regions.'
-			select region in $regions
-			do
-				case $region in
-				'') echo >&2 'Please enter a number in range.';;
-				?*) break
-				esac
-			done
-			case $region in
-			'') exit 1
-			esac;;
+			doselect $regions
+			region=$select_result;;
 		*)
 			region=$regions
 		esac
 
 		# Determine TZ from country and region.
-		TZ=$($AWK -F'\t' \
+		TZ=`$AWK \
 			-v country="$country" \
 			-v region="$region" \
 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
 		'
 			BEGIN {
+				FS = "\t"
 				cc = country
 				while (getline <TZ_COUNTRY_TABLE) {
 					if ($0 !~ /^#/  &&  country == $2) {
@@ -241,7 +431,8 @@ while
 				}
 			}
 			$1 == cc && $4 == region { print $3 }
-		' <$TZ_ZONE_TABLE)
+		' <$TZ_ZONE_TABLE`
+		esac
 
 		# Make sure the corresponding zoneinfo file exists.
 		TZ_for_date=$TZDIR/$TZ
@@ -259,10 +450,10 @@ while
 	extra_info=
 	for i in 1 2 3 4 5 6 7 8
 	do
-		TZdate=$(LANG=C TZ="$TZ_for_date" date)
-		UTdate=$(LANG=C TZ=UTC0 date)
-		TZsec=$(expr "$TZdate" : '.*:\([0-5][0-9]\)')
-		UTsec=$(expr "$UTdate" : '.*:\([0-5][0-9]\)')
+		TZdate=`LANG=C TZ="$TZ_for_date" date`
+		UTdate=`LANG=C TZ=UTC0 date`
+		TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
+		UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
 		case $TZsec in
 		$UTsec)
 			extra_info="
@@ -278,28 +469,23 @@ Universal Time is now:	$UTdate."
 	echo >&2 ""
 	echo >&2 "The following information has been given:"
 	echo >&2 ""
-	case $country+$region in
-	?*+?*)	echo >&2 "	$country$newline	$region";;
-	?*+)	echo >&2 "	$country";;
+	case $country%$region%$coord in
+	?*%?*%)	echo >&2 "	$country$newline	$region";;
+	?*%%)	echo >&2 "	$country";;
+	%?*%?*) echo >&2 "	coord $coord$newline	$region";;
+	%%?*)	echo >&2 "	coord $coord";;
 	+)	echo >&2 "	TZ='$TZ'"
 	esac
 	echo >&2 ""
 	echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
 	echo >&2 "Is the above information OK?"
 
-	ok=
-	select ok in Yes No
-	do
-	    case $ok in
-	    '') echo >&2 'Please enter 1 for Yes, or 2 for No.';;
-	    ?*) break
-	    esac
-	done
+	doselect Yes No
+	ok=$select_result
 	case $ok in
-	'') exit 1;;
 	Yes) break
 	esac
-do :
+do coord=
 done
 
 case $SHELL in