New version of the query tool attached. This one has a lot more options
for searching for zones. It lets you ask questions like:
$ olson areas -Cru # where is Russia?
Asia
Europe
$ olson offsets -ICST # what does "CST" mean?
-06
-05
+08
+09:30
+10:30
$ olson zones -Cau -IEST # which zone might EST be in Australia?
Australia/Brisbane
Australia/Currie
Australia/Hobart
Australia/Lindeman
Australia/Melbourne
Australia/Sydney
I've got a todo list with a bunch more features that it ought to
support searching on, but I think I'm going to handle them by a radical
refactoring that yields more of a query language than this collection
of options.
-zefram
#!perl
{ use 5.006; }
use warnings;
use strict;
use Date::ISO8601 0.000 qw(present_ymd);
use DateTime::TimeZone::Olson ();
use DateTime::TimeZone::SystemV ();
use DateTime::TimeZone::Tzfile ();
use Getopt::Std qw(getopts);
use Params::Classify qw(is_string);
use Time::OlsonTZ::Data ();
use Time::Unix qw(time);
our $VERSION = "0.000";
my $rdn_epoch_cjdn = 1721425;
my %command;
$command{version} = sub () {
getopts("", {}) or die "bad options\n";
die "bad arguments\n" if @ARGV;
print "modules:\n App::olson $VERSION\n";
foreach my $mod (qw(
DateTime::TimeZone::Olson
DateTime::TimeZone::SystemV
DateTime::TimeZone::Tzfile
Time::OlsonTZ::Data
)) {
no strict "refs";
print " $mod ${qq(${mod}::VERSION)}\n";
}
print "Olson database: @{[DateTime::TimeZone::Olson::olson_version]}\n";
};
{
package FakeUtcDateTime;
sub new {
my($class, $rdns) = @_;
return bless({ rdn => $rdns->[0], sod => $rdns->[1] }, $class);
}
sub utc_rd_values { ($_[0]->{rdn}, $_[0]->{sod}, 0) }
}
{
package FakeLocalDateTime;
sub new {
my($class, $rdns) = @_;
return bless({ rdn => $rdns->[0], sod => $rdns->[1] }, $class);
}
sub local_rd_values { ($_[0]->{rdn}, $_[0]->{sod}, 0) }
}
sub handle_exception($$) {
my($val, $err) = @_;
if($err =~ /\A
time\ [-:TZ0-9]+\ is\ not\ represented\ in\ the\ [!-~]+
\ timezone\ due\ to\ (zone\ disuse|)\b
/x) {
return $1 eq "zone disuse" ? "!" : "?";
} elsif($err ne "") {
return "*";
} else {
return $val;
}
}
sub is_exception($) {
return is_string($_[0]) && $_[0] =~ /\A[!?*]\z/;
}
sub rdns_offset($$) {
my($rdns, $offset) = @_;
return $rdns if is_exception($rdns);
return $offset if is_exception($offset);
my($rdn, $sod) = @$rdns;
$sod += $offset;
use integer;
my $doff = $sod < 0 ? -((86399-$sod) / 86400) : $sod / 86400;
$rdn += $doff;
$sod -= 86400*$doff;
return [$rdn, $sod];
}
sub present_rdns($) {
my($rdns) = @_;
return $rdns x 19 if is_exception($rdns);
my($rdn, $sod) = @$rdns;
use integer;
return present_ymd($rdn + $rdn_epoch_cjdn).
"T".sprintf("%02d:%02d:%02d", $sod/3600, $sod/60%60, $sod%60);
}
sub now_utc_dt() {
my $now_unixtime = time;
my $now_utc_rdn = 719163 + int($now_unixtime/86400);
my $now_utc_sod = $now_unixtime % 86400;
return FakeUtcDateTime->new([$now_utc_rdn, $now_utc_sod]);
}
my $current_utc_dt = now_utc_dt();
sub utc_to_local_dt($$) {
my($utcdt, $offset) = @_;
return $utcdt if is_exception($utcdt);
return $offset if is_exception($offset);
return FakeLocalDateTime->new(
rdns_offset([$utcdt->utc_rd_values], $offset));
}
sub local_to_utc_dt($$) {
my($lcldt, $offset) = @_;
return $lcldt if is_exception($lcldt);
return $offset if is_exception($offset);
return FakeUtcDateTime->new(
rdns_offset([$lcldt->utc_rd_values], -$offset));
}
sub present_utc_dt($) {
my($dt) = @_;
return present_rdns(is_exception($dt) ? $dt : [$dt->utc_rd_values])."Z";
}
sub present_local_dt($) {
my($dt) = @_;
return present_rdns(is_exception($dt) ? $dt : [$dt->local_rd_values]);
}
sub present_offset($) {
my($offset) = @_;
return $offset x 3 if is_exception($offset);
my $sign = $offset < 0 ? "-" : "+";
$offset = abs($offset);
use integer;
my $disp = sprintf("%s%02d:%02d:%02d", $sign, $offset/3600,
$offset/60%60, $offset%60);
$disp =~ s/(?::00)+\z//;
return $disp;
}
sub present_initialism($) {
my($initialism) = @_;
return is_exception($initialism) ? $initialism x 3 : $initialism;
}
sub present_isdst($) {
my($isdst) = @_;
return is_exception($isdst) ? $isdst : $isdst ? "+" : "-";
}
{ my $areas; sub areas() { $areas ||= do {
my %areas;
foreach my $country (
values %{DateTime::TimeZone::Olson::olson_country_selection()}
) {
foreach my $region (values %{$country->{regions}}) {
$areas{$1} = undef
if $region->{timezone_name} =~ m#\A([^/]+)/#;
}
}
\%areas;
} } }
my $offset_rx = qr/[-+][0-9]{2}
(?:[0-5][0-9](?:[0-5][0-9])?|:[0-5][0-9](?::[0-5][0-9])?)?
/x;
sub parse_offset($) {
my($string) = @_;
my($sign, $h, $m, $s) = ($string =~ /\A
([-+])([0-9]{2})(?::?([0-5][0-9])(:?[0-5][0-9])?)?
\z/x);
die "unexpected malformed offset" unless defined $sign;
$m = 0 unless defined $m;
$s = 0 unless defined $s;
return (3600*$h + 60*$m + $s) * ($sign eq "-" ? -1 : +1);
}
my $zone_match_options = "A:C:I:O:";
sub zone_matcher($) {
my($opts) = @_;
my @match;
if(exists $opts->{A}) {
my %areas = map { (lc($_) => undef) } keys %{areas()};
if($opts->{A} eq "!") {
my $areas = join("|", map { "\Q$_\E" } keys %areas);
my $arearx = qr#\A($areas)/#oi;
push @match, sub ($) { $_[0] !~ $arearx };
} elsif($opts->{A} =~ /\A[!-~]+\z/ &&
exists($areas{lc($opts->{A})})) {
my $area = $opts->{A};
my $arearx = qr#\A\Q$area\E/#i;
push @match, sub ($) { $_[0] =~ $arearx };
} else {
die "no such area `@{[$opts->{A}]}' ".
"(try `$0 areas` to list them)\n";
}
}
if(exists $opts->{C}) {
my $sel = DateTime::TimeZone::Olson::olson_country_selection();
if($opts->{C} eq "!") {
my %zones = map {
map { ($_->{timezone_name} => undef) }
values %{$_->{regions}}
} values %$sel;
push @match, sub ($) { !exists($zones{$_[0]}) };
} elsif($opts->{C} !~ /\A[A-Za-z]{2}\z/) {
die "malformed country code\n";
} elsif($sel->{uc($opts->{C})}) {
my %zones = map { ($_->{timezone_name} => undef) }
values %{$sel->{uc($opts->{C})}->{regions}};
push @match, sub ($) { exists $zones{$_[0]} };
} else {
die "no such country `@{[uc($opts->{C})]}' ".
"(try `$0 countries` to list them)\n";
}
}
if(exists $opts->{I}) {
unless($opts->{I} =~ /\A[A-Za-z0-9\+\-]{3,}\z/) {
die "malformed initialism\n";
}
my $initialism = $opts->{I};
push @match, sub ($) {
my $zone = DateTime::TimeZone::Olson::olson_tz($_[0]);
my $i = eval {
local $SIG{__DIE__};
$zone->short_name_for_datetime($current_utc_dt);
};
return defined($i) && $i eq $initialism;
};
}
if(exists $opts->{O}) {
unless($opts->{O} =~ /\A$offset_rx\z/o) {
die "malformed offset\n";
}
my $offset = parse_offset($opts->{O});
push @match, sub ($) {
my $zone = DateTime::TimeZone::Olson::olson_tz($_[0]);
my $o = eval {
local $SIG{__DIE__};
$zone->offset_for_datetime($current_utc_dt);
};
return defined($o) && $o == $offset;
};
}
return sub ($) { !(grep { !$_->($_[0]) } @match) };
}
$command{zones} = sub () {
my %opts;
getopts("${zone_match_options}tido", \%opts) or die "bad options\n";
die "bad arguments\n" if @ARGV;
my $zone_matches = zone_matcher(\%opts);
my $links = DateTime::TimeZone::Olson::olson_links;
foreach my $zname (
sort keys %{DateTime::TimeZone::Olson::olson_all_names()}
) {
next unless $zone_matches->($zname);
my @parts;
my $zone = $opts{t} || $opts{i} || $opts{d} || $opts{o} ?
DateTime::TimeZone::Olson::olson_tz($zname) : undef;
my $offset = $opts{t} || $opts{d} || $opts{o} ?
handle_exception(eval {
local $SIG{__DIE__};
$zone->offset_for_datetime($current_utc_dt);
}, $@)
: undef;
if($opts{t}) {
push @parts, present_local_dt(
utc_to_local_dt($current_utc_dt, $offset));
}
if($opts{i}) {
push @parts, sprintf("%-6s", present_initialism(
handle_exception(eval {
local $SIG{__DIE__};
$zone->short_name_for_datetime(
$current_utc_dt);
}, $@)
));
}
if($opts{d}) {
push @parts, present_isdst(handle_exception(eval {
local $SIG{__DIE__};
$zone->is_dst_for_datetime($current_utc_dt) ? 1
: 0;
}, $@));
}
if($opts{o}) {
push @parts, sprintf("%-9s", present_offset($offset));
}
push @parts, $zname;
push @parts, "(-> $links->{$zname})" if exists $links->{$zname};
print join(" ", @parts), "\n";
}
};
$command{areas} = sub () {
my %opts;
getopts("${zone_match_options}", \%opts) or die "bad options\n";
die "bad arguments\n" if @ARGV;
my $zone_matches = zone_matcher(\%opts);
foreach my $area (sort keys %{areas()}) {
my $arearx = qr#\A\Q$area\E/#;
next unless grep { $_ =~ $arearx && $zone_matches->($_) } keys
%{DateTime::TimeZone::Olson::olson_all_names()};
print "$area\n";
}
};
$command{countries} = sub () {
my %opts;
getopts("${zone_match_options}", \%opts) or die "bad options\n";
die "bad arguments\n" if @ARGV;
my $zone_matches = zone_matcher(\%opts);
foreach my $country (sort {
$a->{olson_name} cmp $b->{olson_name}
} values %{DateTime::TimeZone::Olson::olson_country_selection()}) {
next unless grep { $zone_matches->($_->{timezone_name}) }
values %{$country->{regions}};
print $country->{alpha2_code}, " ", $country->{olson_name},
"\n";
}
};
$command{initialisms} = sub () {
my %opts;
getopts("${zone_match_options}", \%opts) or die "bad options\n";
die "bad arguments\n" if @ARGV;
my $zone_matches = zone_matcher(\%opts);
my %initialisms;
foreach my $zname (keys
%{DateTime::TimeZone::Olson::olson_all_names()}) {
next unless $zone_matches->($zname);
$initialisms{do {
my $zone = DateTime::TimeZone::Olson::olson_tz($zname);
handle_exception(eval {
local $SIG{__DIE__};
$zone->short_name_for_datetime($current_utc_dt);
}, $@);
}} = undef;
}
print present_initialism($_), "\n" foreach sort {
my($aa, $bb) = ($a, $b);
foreach my $o ($aa, $bb) {
$o = "\x00" if $o eq "*";
$o = "\x01" if $o eq "!";
$o = "\x02" if $o eq "?";
}
$aa cmp $bb;
} keys %initialisms;
};
$command{offsets} = sub () {
my %opts;
getopts("${zone_match_options}", \%opts) or die "bad options\n";
die "bad arguments\n" if @ARGV;
my $zone_matches = zone_matcher(\%opts);
my %offsets;
foreach my $zname (keys
%{DateTime::TimeZone::Olson::olson_all_names()}) {
next unless $zone_matches->($zname);
$offsets{do {
my $zone = DateTime::TimeZone::Olson::olson_tz($zname);
handle_exception(eval {
local $SIG{__DIE__};
0+$zone->offset_for_datetime($current_utc_dt);
}, $@)
}} = undef;
}
print present_offset($_), "\n" foreach sort {
my($aa, $bb) = ($a, $b);
foreach my $o ($aa, $bb) {
$o = -102*3600 if $o eq "*";
$o = -101*3600 if $o eq "!";
$o = -100*3600 if $o eq "?";
}
$aa <=> $bb;
} keys %offsets;
};
getopts("", {}) or die "bad options\n";
die "no subcommand specified\n" unless @ARGV;
($command{shift(@ARGV)} || sub () { die "unrecognised subcommand\n" })->();
exit 0;
=head1 NAME
olson - query the Olson timezone database
=head1 SYNOPSIS
olson areas
olson countries
olson initialisms
olson offsets
olson version
olson zones
=head1 DESCRIPTION
This program provides various ways of extracting information from
the Olson timezone database. It can be used to assist selection of
a timezone, and for other purposes. The type of query to perform is
determined by a subcommand, specified as the first command-line argument.
Some subcommands take options, in which case the options should appear
after the subcommand.
=head1 ZONE SELECTION OPTIONS
Several subcommands take options that restrict listings based on certain
features of timezones. When listing timezones, the listing is simply
restricted to timezones that match the criteria specified by these
options. When listing things other than timezones (such as countries),
the listing is restricted to those things that are associated with at
least one timezone matching the criteria. The options are:
=over
=item B<-A> I<area>
Requires the timezone name to start with the specified area name
(specified case-insensitively). If "C<!>" is specified, requires the
timezone name to not start with any area name. (The no-area timezones
include both non-geographical timezones and many historical aliases for
timezones now named in the area-based scheme.)
=item B<-C> I<country>
Requires the timezone to be associated with the country with the specified
ISO 3166 alpha-2 code (specified case-insensitively). If "C<!>" is
specified, requires the timezone to be not associated with any country.
(Timezones are associated with countries for the purpose of selecting a
timezone having already selected a country. The no-country timezones
therefore include not only non-geographical timezones but also many
historical aliases for timezones that can objectively be associated with
a particular country.)
=item B<-I> I<initialism>
Requires the timezone to be currently referred to by the specified
initialism (specified case-sensitively). (A timezone's initialism may
be fixed, or may vary over the course of each year to signal DST changes.)
=item B<-O> I<offset>
Requires the timezone to be currently using the specified offset from UT.
The offset must begin with a sign and two-digit number of hours. It may
optionally include a two-digit number of minutes, and if it does then
it may also include a two-digit number of seconds. Hours, minutes,
and seconds may be either separated by colons or run together.
=back
=head1 SUBCOMMANDS
=over
=item B<areas>
Lists the top-level areas (continents and oceans) that are used for rough
geographical categorisation of geographical timezones. These names
are the first part of the hierarchical geographical timezone names.
The names are sorted alphabetically.
The range of areas to list can be restricted to those containing
particular kinds of timezone by using the zone selection options
described above.
=item B<countries>
Lists the countries that are used for selection of timezones based
on political geography. For each country, the list shows its ISO
3166 alpha-2 code and a short version of its name. The list is sorted
alphabetically by name, and the names are optimised to make this a good
way to list them.
The range of countries to list can be restricted to those containing
particular kinds of timezone by using the zone selection options
described above.
=item B<initialisms>
Lists the initialisms that are currently used to refer to timezones.
(A timezone's initialism may be fixed, or may vary over the course of
each year to signal DST changes.) The list is sorted in ASCII order.
The range of initialisms to list can be restricted to those referring
to particular kinds of timezone by using the zone selection options
described above.
=item B<offsets>
Lists the offsets from UT that are currently used by timezones. The list
is sorted numerically, most negative (westernmost) first.
The range of offsets to list can be restricted to those used by particular
kinds of timezone by using the zone selection options described above.
=item B<version>
Indicates which version is being used of the Olson database and of the
relevant Perl modules. This information is vital in any bug report.
=item B<zones>
Lists timezones.
The list is sorted in ASCII order of timezone name.
For each timezone, what the list shows depends on options.
By default the listing shows the name, and if the name is an alias
then the target of the alias is also shown.
The options controlling the format of the list are:
=over
=item B<-d>
For each listed timezone, indicate whether it is currently observing DST.
A "C<+>" character indicates DST, and a "C<->" character indicates
not DST.
=item B<-i>
For each listed timezone, show the initialism currently used to refer
to it. (The initialism may be fixed for the timezone, or may vary over
the course of each year to signal DST changes.)
=item B<-o>
For each listed timezone, show its current offset from UT.
=item B<-t>
For each listed timezone, show its current local time.
=back
The range of timezones to list can be restricted by using the zone
selection options described above.
=back
=head1 SEE ALSO
L<DateTime::TimeZone::Olson>,
L<Time::OlsonTZ::Data>