#!perl -lw
#
# print out current time in all known time zones, also giving metadata
# about each time zone. usage:
#
# perl alltimezones.pl [unixtime]
#
# optionally, provide Unix time as a numeric argument to do that time
# instead of the current time.
#
# output:
#
# first output field: time offset from UTC.
# second output field: longitude with a period in front of it
# third output field: 0 if "canonical" time zone name, 1 if a "link" time zone.
# then a colon.
# then time, a colon, time zone name, then metadata about the time zone, including comments in time zone database, country, what it links to if it is a "link" time zone.
#
# the first three fields are intended to be used as sort keys then
# discarded, for example:
#
# perl alltimezones.pl | sort -k1g -k2g | perl -plwe 's/.*?://'
#
# Copyright 2022 Ken Takusagawa
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
use POSIX qw(strftime);
# the file exists in debian bullseye, not present in buster
$zifile=$ENV{TZDATAZIFILE};
$zifile='/usr/share/zoneinfo/tzdata.zi' unless defined$zifile;
open FI, $zifile or die;
while(){
if(/^Z\s+(\S+)/){
die if $mainzone{$1};
$mainzone{$1}=1;
} elsif(/^L\s+(\S+)\s+(\S+)$/){
die if $link{$2};
$link{$2}=$1;
}
}
#for(keys %z){print;}
for(keys %link){
die if$mainzone{$_};
#print;
}
open FI,'/usr/share/zoneinfo/zone1970.tab' or die;
while(){
chomp;
next if /^#/;
@F=split/\t/,$_;
$zone=$F[2];
#print "zone=$zone";
unless ($mainzone{$zone}){
# this happens if you use incompatible tzdata.zi and zone1970.tab
print STDERR "ignoring zone1970.tab $zone";
next;
}
die unless ($long1)=($F[1]=~/^[+-]\d+([+-]\d+)$/);
die if defined($longitude{$zone});
#print "got $lat1 $long1";
$longitude{$zone}=$long1;
@c=split/,/,$F[0];
for(@c){
push @{$countrycodes{$_}},$zone;
push @{$zonecountries{$zone}},$_;
}
if($F[3]){
die if $comment{$zone};
($zonesuffix)=($zone =~ m,([^/]*)$,);
unless($F[3] ne $zonesuffix){
#print "new ignore redundant comment $_";
} else {
$F[3] =~ s/$zonesuffix/~/g; # swung dash
$comment{$zone}=$F[3];
}
}
}
#print for(keys %countrycodes);
#for(keys%z){
# print"$_" unless defined($lat{$_}) and defined($long{$_});
#}
if (open FI,'/usr/share/zoneinfo/zone.tab'){
while(){
chomp;
next if /^#/;
@F=split /\t/,$_;
$zone=$F[2];
print STDERR "zone.tab not in zi: $_" unless $mainzone{$zone} or $link{$zone};
die unless ($long1)=($F[1]=~/^[+-]\d+([+-]\d+)$/);
die if defined($oldlongitude{$zone});
$oldlongitude{$zone}=$long1;
die if defined($oldcountry{$zone});
$oldcountry{$zone}=$F[0];
die if defined($oldcomment{$zone});
if(defined$F[3]){
($zonesuffix)=($zone =~ m,([^/]*)$,);
unless($F[3] ne $zonesuffix){
#print "old ignore redundant comment $_";
}
else {
if(defined($comment{$zone}) and $comment{$zone} ne $F[3]){
# as of Debian Bullseye (2021a), differences in comments are only in accented letters.
# 2021b and beyond (tested 2021e) introduced merged time zones, so a city's time zone could apply to a country it is not, e.g., America/Creston (Canada) -> America/Phoenix (US) . This makes the comments on Phoenix harder to understand.
#print "explore $_\nexploreold=($F[3])\nexplorenew=($comment{$zone})"
}
$F[3] =~ s/$zonesuffix/~/g; # swung dash
$oldcomment{$zone}=$F[3];
}
}
}
}
# these zones have no location information
# CST6CDT
# CET
# EET
# EST
# EST5EDT
# Etc/GMT
# Etc/GMT+1
# Etc/GMT+10
# Etc/GMT+11
# Etc/GMT+12
# Etc/GMT+2
# Etc/GMT+3
# Etc/GMT+4
# Etc/GMT+5
# Etc/GMT+6
# Etc/GMT+7
# Etc/GMT+8
# Etc/GMT+9
# Etc/GMT-1
# Etc/GMT-10
# Etc/GMT-11
# Etc/GMT-12
# Etc/GMT-13
# Etc/GMT-14
# Etc/GMT-2
# Etc/GMT-3
# Etc/GMT-4
# Etc/GMT-5
# Etc/GMT-6
# Etc/GMT-7
# Etc/GMT-8
# Etc/GMT-9
# Etc/UTC
# Factory
# HST
# MET
# MST
# MST7MDT
# PST8PDT
# WET
open FI,'/usr/share/zoneinfo/iso3166.tab' or die;
while(){
next if/^#/;
die unless /^(\S\S)\t(.+)/;
die if defined($codelookup{$1});
$codelookup{$1}=$2;
}
$now=$ARGV[0];
$now=time() unless defined$now;
sub getlongitude {
my$a=shift;
my$n;
if(defined($longitude{$a})){
$n=$longitude{$a};
}
# assigning longitudes to time zones is all around questionable, defended by wanting things to be roughly in the correct location
elsif ($a =~ m,^Etc/GMT([+-]\d+)$,) {
$n=0-$1;
$n*=(360/24); # 15 degrees per time zone
$n-=360 while($n>180); # because madness like GMT-14
$n+=360 while($n<-180); # currently unneeded
if($n>=0){
$n=sprintf("+%03d",$n);
} else {
$n=sprintf("%04d",$n);
}
#print "#n $n";
} elsif ($a eq 'PST8PDT') {
$n=$longitude{$link{'US/Pacific'}};
} elsif ($a eq 'MST7MDT') {
$n=$longitude{$link{'US/Mountain'}};
} elsif ($a eq 'MST') {
# no summer time, assigning location to the most famous place
$n=$longitude{$link{'US/Arizona'}};
} elsif ($a eq 'CST6CDT') {
$n=$longitude{$link{'US/Central'}};
} elsif ($a eq 'EST5EDT') {
$n=$longitude{$link{'US/Eastern'}};
} elsif ($a eq 'HST') {
$n=$longitude{$link{'US/Hawaii'}}; # questionable, because Alaska is sometimes Hawaii
} elsif ($a eq 'CET' or $a eq 'MET') {
# does summer time
$n="+015"
} elsif ($a eq 'EET') {
# does summer time
$n="+030"
} elsif ($a eq 'EST') {
# no summer time
$n="-075"
}else {
# everything else not assigned a longitude get assigned to 0.
# this includes incompatibities between tzdata.zi and zone1970.tab
$n="+00";
# WET does summer time
}
&makesortable($n);
}
sub makesortable {
my$n=shift;
$n =~ s/^(.)(.*)/${1}0.$2/; # make it something that sort -g can sort
# we need some sort of numeric sort because the negative sign essentially changes sort order to reverse
$n;
}
# almost the same as %c, but including AM/PM
$mainformat=":%a %d %b %Y %T %P %Z (%z)";
sub calldate {
my $z=shift;
my $format=shift;
#my $y=`env TZ=:$z date --date="\@$now"`;
# shelling out is 70 times slower
# colon indicates it is a file name in tzdata
$ENV{TZ}=":$z";
my@t=localtime($now);
strftime($format,$t[0],$t[1],$t[2],$t[3],$t[4],$t[5],$t[6],$t[7],$t[8]);
# passing $t[8] is necessary for %z to work
}
for(keys%mainzone){
if($comm=$comment{$_}){
$comm=" ($comm)";
} else {
$comm="";
}
$countrystring="";
for$country(@{$zonecountries{$_}}){
$countrystring.="[".$country.":".$codelookup{$country};
if(1==scalar(@{$countrycodes{$country}})){
$countrystring.=":entirety";
}
$countrystring.="]";
}
$countrystring=" ".$countrystring if $countrystring ne "";
$done=&calldate($_,$mainformat);
die if defined$save{$_};
$save{$_}=$done;
$offset=&calldate($_,"%z");
print "$offset ",&getlongitude($_)," 0 ",$done," : ",$_,$comm,$countrystring;
# extra field "0" for sorting: canonical comes first
}
for$l(keys%link){
$m=$link{$l};
die unless defined($mainzone{$m});
die unless defined($done=$save{$m});
$longi=$oldlongitude{$l};
$longi=&makesortable($longi) if defined$longi;
$longi=&getlongitude($m) unless defined$longi;
$countrystring=$oldcountry{$l};
unless(defined$countrystring){
$countrystring="";
} else {
$countrystring=" [$countrystring:".$codelookup{$countrystring}."]";
}
$comment=$oldcomment{$l};
unless(defined$comment){
$comment="";
}else{
$comment=" ($comment)";
}
$offset=&calldate($l,"%z");
print "$offset ",$longi," 1 ",$done," : ",$l,$comment,$countrystring," (->$m)";
}
# no longer done
sub docountries {
for$c(keys %countrycodes){
if(1==scalar(@{$countrycodes{$c}})){
$m=${$countrycodes{$c}}[0];
if(defined($name=$codelookup{$c})){
$name=" ($name)";
} else {
# currently all country codes have expansions, so this branch is not taken
$name="";
}
print "",&getlongitude($m)," 2 ",&calldate($m,$mainformat)," : ",".",lc$c,$name;
} else {
#print "country has multiple time zones $c";
# AQ AR AU BR CA CD CL CN CY DE EC ES FM GL ID KI KZ MH MN MX MY NZ PF PG PS PT RU TF UA UM US UZ VN
}
}
}