require 5; use strict;
use Getopt::Long;
use vars qw(@FILES $YEAR $DATA_DIR $OUT $SEP @MONTH
$VERSION_YEAR $VERSION_SUFFIX $RAW_VERSION
$TZ_ALIAS $TZ_DEFAULT $URL $TXT_FILE $HTML_FILE $JAVA_FILE
$TZ_TXT_VERSION %ZONE_ID_TO_INDEX $END_MARKER
%COUNTRY_CODES);
require 'dumpvar.pl';
use tzparse;
use tzutil;
$TZ_TXT_VERSION = 4;
$TZ_ALIAS = 'tz.alias';
$TZ_DEFAULT = 'tz.default';
$URL = "ftp://elsie.nci.nih.gov/pub";
$SEP = ',';
$END_MARKER = 'end';
@FILES = qw(africa
antarctica
asia
australasia
backward
etcetera
europe
factory
northamerica
pacificnew
solar87
solar88
solar89
southamerica);
$YEAR = 1900+@{[localtime]}[5];
$DATA_DIR = shift;
if (!$DATA_DIR || ! -d $DATA_DIR) {
print STDERR "No data directory or invalid directory specified\n\n";
usage();
}
$TXT_FILE = '';
$HTML_FILE = '';
$JAVA_FILE = '';
while (@ARGV) {
local $_ = shift;
if (/\.java$/i) {
if ($JAVA_FILE) {
print STDERR "Error: Multiple java files specified\n";
usage();
}
$JAVA_FILE = $_;
} elsif (/\.html?$/i) {
if ($HTML_FILE) {
print STDERR "Error: Multiple html files specified\n";
usage();
}
$HTML_FILE = $_;
} elsif (/\.txt$/i) {
if ($TXT_FILE) {
print STDERR "Error: Multiple txt files specified\n";
usage();
}
$TXT_FILE = $_;
} else {
print STDERR "Error: Unexpected command line parameter \"$_\"\n";
usage();
}
}
if (!($TXT_FILE || $JAVA_FILE || $HTML_FILE)) {
print STDERR "Nothing to do! Please specify one or more output files.\n";
usage();
}
if ($DATA_DIR =~ /(tzdata(\d{4})(\w?))/) {
$RAW_VERSION = $1;
$VERSION_YEAR = $2;
$VERSION_SUFFIX = $3;
if ($YEAR != $VERSION_YEAR) {
print STDERR "WARNING: You appear to be building $VERSION_YEAR data. Don't you want to use current $YEAR data?\n\n";
}
$VERSION_SUFFIX =~ tr/a-z/A-Z/;
if ($VERSION_SUFFIX =~ /[A-Z]/) {
$VERSION_SUFFIX = ord($VERSION_SUFFIX) - ord('A') + 1;
} else {
if ($VERSION_SUFFIX) {
print STDERR "Warning: Ignoring version suffix '$VERSION_SUFFIX' for \"$DATA_DIR\"\n";
}
$VERSION_SUFFIX = 0;
}
print "Time zone version $RAW_VERSION = $VERSION_YEAR($VERSION_SUFFIX)\n";
} else {
print STDERR "The directory specified doesn't contain \"tzdataNNNNR\", so I can't tell what version the data is. Please rename the directory and try again.\n";
usage();
}
@MONTH = qw(jan feb mar apr may jun
jul aug sep oct nov dec);
main();
exit();
sub usage {
print STDERR "Usage: $0 data_dir [txt_out] [html_out] [java_out]\n\n";
print STDERR " data_dir contains the unpacked files from\n";
print STDERR " $URL/tzdataYYYYR,\n";
print STDERR " where YYYY is the year and R is the revision\n";
print STDERR " letter.\n";
print STDERR "\n";
print STDERR " Files that are expected to be present are:\n";
print STDERR " ", join(", ", @FILES), "\n";
print STDERR "\n";
print STDERR " [txt_out] optional name of .txt file to output\n";
print STDERR " [html_out] optional name of .htm|.html file to output\n";
print STDERR " [java_out] optional name of .java file to output\n";
exit 1;
}
sub main {
my (%ZONES, %RULES, @EQUIV, %LINKS, %COUNTRIES);
print "Reading";
foreach (@FILES) {
if (! -e "$DATA_DIR/$_") {
print STDERR "\nMissing file $DATA_DIR/$_\n\n";
usage();
}
print ".";
TZ::ParseFile("$DATA_DIR/$_", \%ZONES, \%RULES, \%LINKS, $YEAR);
}
print "done\n";
TZ::ParseZoneTab("$DATA_DIR/zone.tab", \%ZONES, \%LINKS);
local(*FILE);
open(FILE, "$DATA_DIR/iso3166.tab") or die "Can't open $DATA_DIR/iso3166.tab";
while (<FILE>) {
s/\ next unless (/\S/);
s/\s+$//;
if (/^([A-Z]{2})\s+(\S.*)/) {
$COUNTRY_CODES{$1} = $2; } else {
print STDERR "Ignoring $DATA_DIR/iso3166.tab line: $_";
}
}
close(FILE);
TZ::Postprocess(\%ZONES, \%RULES);
my $aliases = incorporateAliases($TZ_ALIAS, \%ZONES, \%LINKS);
print
"Read ", scalar keys %ZONES, " current zones and ",
scalar keys %RULES, " rules for $YEAR\n";
if (!exists $ZONES{GMT}) {
print "Adding GMT zone\n";
my %GMT = ('format' => 'GMT',
'gmtoff' => '0:00',
'rule' => $TZ::STANDARD,
'until' => '');
$ZONES{GMT} = \%GMT;
}
foreach my $z (keys %ZONES) {
assertInvariantChars($z);
}
my $offsetIndex = createOffsetIndex(\%ZONES, $TZ_DEFAULT);
TZ::FormZoneEquivalencyGroups(\%ZONES, \%RULES, \@EQUIV);
print
"Equivalency groups (including unique zones): ",
scalar @EQUIV, "\n";
@EQUIV = sort { my $x = $ZONES{$a->[0]};
my $y = $ZONES{$b->[0]};
TZ::ParseOffset($x->{gmtoff}) <=>
TZ::ParseOffset($y->{gmtoff}) ||
TZ::ZoneCompare($x, $y, \%RULES); } @EQUIV;
foreach my $eg (@EQUIV) {
next unless (@$eg > 1); my @zoneList = sort @$eg;
$eg = \@zoneList;
}
my $i = 0;
foreach my $z (sort keys %ZONES) {
$ZONE_ID_TO_INDEX{$z} = $i++;
}
my $NONE = 'A';
foreach (sort keys %ZONES) {
my $country = $ZONES{$_}->{country};
$country = $NONE unless ($country);
push @{$COUNTRIES{$country}->{zones}}, $_;
}
foreach my $country (keys %COUNTRIES) {
my $intcode = 0;
if ($country ne $NONE) {
if ($country =~ /^([A-Z])([A-Z])$/) {
$intcode = ((ord($1) - ord('A')) << 5) |
(ord($2) - ord('A'));
} else {
die "Can't parse country code $country";
}
}
$COUNTRIES{$country}->{intcode} = $intcode;
}
if ($TXT_FILE) {
emitText($TXT_FILE, \%ZONES, \%RULES, \@EQUIV, $offsetIndex, $aliases,
\%COUNTRIES);
print "$TXT_FILE written.\n";
}
if ($JAVA_FILE) {
emitJava($JAVA_FILE, \%ZONES, \%RULES, \@EQUIV, $offsetIndex, $aliases,
\%COUNTRIES);
print "$JAVA_FILE written.\n";
}
if ($HTML_FILE) {
emitHTML($HTML_FILE, \%ZONES, \%RULES, \@EQUIV, $offsetIndex, $aliases,
\%COUNTRIES);
print "$HTML_FILE written.\n";
}
if (0) {
my %RULEVALS;
foreach my $ruleName (keys %RULES) {
for (my $i=0; $i<2; ++$i) {
foreach my $key (qw(in on at save type letter)) {
if (@{$RULES{$ruleName}} < 2) {
print $ruleName, ":";
::dumpValue($RULES{$ruleName});
}
my $x = $RULES{$ruleName}->[$i]->{$key};
$RULEVALS{$key}->{$x} = 1;
}
}
}
foreach my $key (sort keys %RULEVALS) {
print "$key: ", join(", ", sort keys %{$RULEVALS{$key}}), "\n";
}
}
}
sub createOffsetIndex {
my $zones = shift;
my $defaultFile = shift;
my %offsetMap;
foreach (sort keys %{$zones}) {
my $offset = TZ::ParseOffset($zones->{$_}->{gmtoff});
push @{$offsetMap{$offset}}, $_;
}
my %defaults; my $ok = 1;
open(IN, $defaultFile) or die "Can't open $defaultFile: $!";
while (<IN>) {
my $raw = $_;
s/\ next unless (/\S/); if (/^\s*(\S+)\s*$/) {
my $z = $1;
if (! exists $zones->{$z}) {
print "Error: Nonexistent zone $z listed in $defaultFile line: $raw";
$ok = 0;
next;
}
my $offset = TZ::ParseOffset($zones->{$z}->{gmtoff});
if (exists $defaults{$offset}) {
print
"Error: Offset ", formatOffset($offset), " has both ",
$defaults{$offset}, " and ", $z,
" specified as defaults\n";
$ok = 0;
next;
}
$defaults{$offset} = $z;
} else {
print "Error: Can't parse line in $defaultFile: $raw";
$ok = 0;
}
}
close(IN);
die "Error: Aborting due to errors in $defaultFile\n" unless ($ok);
print "Incorporated ", scalar keys %defaults, " defaults from $defaultFile\n";
my $missing;
foreach my $gmtoff (keys %offsetMap) {
my $aref = $offsetMap{$gmtoff};
my $def;
if (exists $defaults{$gmtoff}) {
$def = $defaults{$gmtoff};
} else {
my $ambiguous;
foreach (sort @{$aref}) {
next if (m|^Etc/|i);
if (!$def) {
$def = $_;
} else {
$ambiguous = 1;
}
}
$def = $aref->[0] unless ($def);
if ($ambiguous) {
$missing = 1;
print
"Warning: No default for GMT", formatOffset($gmtoff),
", using ", $def, "\n";
}
}
unshift @{$aref}, $def;
}
print "Defaults may be specified in $TZ_DEFAULT\n" if ($missing);
return \%offsetMap;
}
sub isDefault {
my $name = shift;
my $offset = shift;
my $offsetIndex = shift;
my $aref = $offsetIndex->{TZ::ParseOffset($offset)};
return ($aref->[0] eq $name);
}
sub emitText {
my $file = shift;
my $zones = shift;
my $rules = shift;
my $equiv = shift;
my $offsetIndex = shift;
my $aliases = shift;
my $countries = shift;
my %perOffset; foreach my $z (keys %$zones) {
++$perOffset{TZ::ParseOffset($zones->{$z}->{gmtoff})};
}
my $maxPerOffset = 0;
foreach (values %perOffset) {
$maxPerOffset = $_ if ($_ > $maxPerOffset);
}
my $maxPerEquiv = 0;
foreach my $eg (@$equiv) {
$maxPerEquiv = @$eg if (@$eg > $maxPerEquiv);
}
my $name_size = 0;
foreach my $z (keys %$zones) {
$name_size += 1 + length($z);
}
local(*OUT);
open(OUT,">$file") or die "Can't open $file for writing: $!";
print OUT "#####################################################################\n";
print OUT "# Copyright (C) 2000-$YEAR, International Business Machines Corporation and\n";
print OUT "# others. All Rights Reserved.\n";
print OUT "#####################################################################\n";
print OUT "#--- Header --- Generated by tz.pl\n";
print OUT $TZ_TXT_VERSION, " # format version number of this file\n";
print OUT $VERSION_YEAR, " # ($RAW_VERSION) version of Olson zone\n";
print OUT $VERSION_SUFFIX, " # data from $URL\n";
print OUT scalar keys %$zones, " # total zone count\n";
print OUT scalar @$equiv, " # equivalency groups count\n";
print OUT $maxPerOffset, " # max zones with same gmtOffset\n";
print OUT $maxPerEquiv, " # max zones in an equivalency group\n";
print OUT $name_size, " # length of name table in bytes\n";
print OUT $END_MARKER, "\n\n";
print OUT "#--- Zone table ---\n";
print OUT "#| equiv_index,name\n";
print OUT scalar keys %$zones, " # count of zones to follow\n";
foreach my $z (sort keys %$zones) {
assertInvariantChars($z);
print OUT equivIndexOf($z, $equiv), ',', $z, "\n";
}
print OUT $END_MARKER, "\n\n";
print OUT "#--- Equivalency table ---\n";
print OUT "#| ('s'|'d'),zone_spec,id_count,id_list\n";
print OUT scalar @$equiv, " # count of equivalency groups to follow\n";
my $i = 0;
foreach my $aref (@$equiv) {
my $z = $aref->[0];
my $isStd = ($zones->{$z}->{rule} eq $TZ::STANDARD);
if (!$isStd) {
my $rule = $rules->{$zones->{$z}->{rule}};
if (!(@{$rule} >= 4 && ($rule->[3] & 1) && ($rule->[3] & 2))) {
$isStd = 1;
}
}
print OUT $isStd ? 's,' : 'd,';
my ($spec, $notes) = formatZone($z, $zones->{$z}, $rules);
push @$spec, scalar @$aref;
push @$notes, "[";
my $min = -1;
foreach $z (@$aref) {
my $index = $ZONE_ID_TO_INDEX{$z};
die("Unsorted equiv table indices") if ($index <= $min);
$min = $index;
push @$spec, $index;
push @$notes, $z;
}
push @$notes, "]";
unshift @$notes, $i++; print OUT join($SEP, @$spec) . " # " . join(' ', @$notes), "\n";
}
print OUT $END_MARKER, "\n\n";
my %zoneNumber;
my @zoneName;
$i = 0;
foreach (sort keys %$zones) {
$zoneName[$i] = $_;
$zoneNumber{$_} = $i++;
}
print OUT "#--- Offset INDEX ---\n";
print OUT "#| gmt_offset,default_id,id_count,id_list\n";
print OUT scalar keys %{$offsetIndex}, " # index by offset entries to follow\n";
foreach (sort {$a <=> $b} keys %{$offsetIndex}) {
my $aref = $offsetIndex->{$_};
my $def = $aref->[0];
my @b = @{$aref}[1..$ print OUT
$_, ",", $zoneNumber{$def}, ",",
scalar @b, ",",
join(",", map($zoneNumber{$_}, @b)),
" # ", formatOffset($_), " d=", $def, " ",
join(" ", @b), "\n";
}
print OUT $END_MARKER, "\n\n";
print OUT "#--- Country INDEX ---\n";
print OUT "#| country_int_code,id_count,id_list\n";
print OUT scalar keys %$countries, " # index by country entries to follow\n";
foreach my $country (sort keys %$countries) {
my $intcode = $countries->{$country}->{intcode};
my $aref = $countries->{$country}->{zones};
print OUT
$intcode, ",", scalar @$aref, ",",
join(",", map($zoneNumber{$_}, @$aref)), " # ",
($intcode ? ($country . " (" . $COUNTRY_CODES{$country} . ") ") : "(None) "),
join(" ", @$aref), "\n";
}
print OUT $END_MARKER, "\n";
close(OUT);
}
sub emitJava {
my $file = shift;
my $zones = shift;
my $rules = shift;
my $equiv = shift;
my $offsetIndex = shift;
my $aliases = shift;
my $countries = shift;
my $_indent = " ";
my $_IDS;
foreach my $z (sort keys %$zones) {
$_IDS .= "$_indent\"$z\",\n";
}
my $_DATA;
my %equiv_id_to_index;
my $i = 0;
my $index = 0;
foreach my $aref (@$equiv) {
$equiv_id_to_index{$i} = $index;
my $z = $aref->[0];
$_DATA .= $_indent;
my $isStd = ($zones->{$z}->{rule} eq $TZ::STANDARD);
if (!$isStd) {
my $rule = $rules->{$zones->{$z}->{rule}};
if (!(@{$rule} >= 4 && ($rule->[3] & 1) && ($rule->[3] & 2))) {
$isStd = 1;
}
}
$_DATA .= $isStd ? '0/*s*/,' : '1/*d*/,';
my ($spec, $notes) = formatZone($z, $zones->{$z}, $rules);
push @$spec, scalar @$aref;
push @$notes, "[";
my $min = -1;
foreach $z (@$aref) {
my $index = $ZONE_ID_TO_INDEX{$z};
die("Unsorted equiv table indices") if ($index <= $min);
$min = $index;
push @$spec, $index;
push @$notes, $z;
}
push @$notes, "]";
unshift @$notes, $i++;
foreach (@$spec) {
if (/^w$/) {
$_ = "0/*w*/";
} elsif (/^s$/) {
$_ = "1/*s*/";
} elsif (/^u$/) {
$_ = "2/*u*/";
}
}
$_DATA .= join($SEP, @$spec) . ", // " . join(' ', @$notes) . "\n";
$index += (scalar @$spec) + 1; }
my $_INDEX_BY_NAME;
foreach my $z (sort keys %$zones) {
$_INDEX_BY_NAME .=
$_indent .
$equiv_id_to_index{equivIndexOf($z, $equiv)} .
", // $z\n";
}
my $_INDEX_BY_OFFSET;
my %zoneNumber;
my @zoneName;
$i = 0;
foreach (sort keys %$zones) {
$zoneName[$i] = $_;
$zoneNumber{$_} = $i++;
}
foreach (sort {$a <=> $b} keys %{$offsetIndex}) {
my $aref = $offsetIndex->{$_};
my $def = $aref->[0];
my @b = @{$aref}[1..$ $_INDEX_BY_OFFSET .=
$_indent . $_ . "," . $zoneNumber{$def} . "," .
scalar @b . "," .
join(",", map($zoneNumber{$_}, @b)) .
", // " . formatOffset($_) . " d=" . $def . " " .
join(" ", @b) . "\n";
}
my $_INDEX_BY_COUNTRY;
foreach my $country (sort keys %$countries) {
my $intcode = $countries->{$country}->{intcode};
my $aref = $countries->{$country}->{zones};
$_INDEX_BY_COUNTRY .=
$_indent . $intcode . ", " .
scalar(@$aref) . ", " .
join(", ", map($zoneNumber{$_}, @$aref)) . ", // " .
($intcode ? ($country . " (" . $COUNTRY_CODES{$country} . ")") : "(None)") . ": " .
join(" ", @$aref) .
"\n";
}
my $java = <<"END";
// Instructions: Build against icu4j. Run and save output.
// Paste output into icu4j/src/com/ibm/util/TimeZoneData.java
import com.ibm.icu.impl.Utility;
import java.util.Date;
public class tz {
public static void main(String[] args) {
System.out.println(" // BEGIN GENERATED SOURCE CODE");
System.out.println(" // Date: " + new Date());
System.out.println(" // Version: $RAW_VERSION from $URL");
System.out.println(" // Tool: icu/source/tools/gentz");
System.out.println(" // See: icu/source/tools/gentz/readme.txt");
System.out.println(" // DO NOT EDIT THIS SECTION");
System.out.println();
System.out.println(" /**");
System.out.println(" * Array of IDs in lexicographic order. The INDEX_BY_OFFSET and DATA");
System.out.println(" * arrays refer to zones using indices into this array. To map from ID");
System.out.println(" * to equivalency group, use the INDEX_BY_NAME Hashtable.");
System.out.println(" * >> GENERATED DATA: DO NOT EDIT <<");
System.out.println(" */");
System.out.println(" static final String[] IDS = {");
for (int i=0;i<IDS.length;++i) {
System.out.println(" \\\"" + IDS[i] + "\\\",");
}
System.out.println(" };\\n");
System.out.println(" /**");
System.out.println(" * RLE encoded form of DATA.");
System.out.println(" * \@see com.ibm.util.Utility.RLEStringToIntArray");
System.out.println(" * >> GENERATED DATA: DO NOT EDIT <<");
System.out.println(" */");
System.out.println(" static final String DATA_RLE =");
System.out.println(Utility.formatForSource(Utility.arrayToRLEString(DATA)));
System.out.println(" ;\\n");
System.out.println(" /**");
System.out.println(" * RLE encoded form of INDEX_BY_NAME_ARRAY.");
System.out.println(" * \@see com.ibm.util.Utility.RLEStringToIntArray");
System.out.println(" * >> GENERATED DATA: DO NOT EDIT <<");
System.out.println(" */");
System.out.println(" static final String INDEX_BY_NAME_ARRAY_RLE =");
System.out.println(Utility.formatForSource(Utility.arrayToRLEString(INDEX_BY_NAME_ARRAY)));
System.out.println(" ;\\n");
System.out.println(" /**");
System.out.println(" * RLE encoded form of INDEX_BY_OFFSET.");
System.out.println(" * \@see com.ibm.util.Utility.RLEStringToIntArray");
System.out.println(" * >> GENERATED DATA: DO NOT EDIT <<");
System.out.println(" */");
System.out.println(" static final String INDEX_BY_OFFSET_RLE =");
System.out.println(Utility.formatForSource(Utility.arrayToRLEString(INDEX_BY_OFFSET)));
System.out.println(" ;\\n");
System.out.println(" /**");
System.out.println(" * RLE encoded form of INDEX_BY_COUNTRY.");
System.out.println(" * \@see com.ibm.util.Utility.RLEStringToIntArray");
System.out.println(" * >> GENERATED DATA: DO NOT EDIT <<");
System.out.println(" */");
System.out.println(" static final String INDEX_BY_COUNTRY_RLE =");
System.out.println(Utility.formatForSource(Utility.arrayToRLEString(INDEX_BY_COUNTRY)));
System.out.println(" ;\\n");
System.out.println(" // END GENERATED SOURCE CODE");
}
static final String[] IDS = {
$_IDS
};
static final int[] DATA = {
$_DATA
};
static final int[] INDEX_BY_NAME_ARRAY = {
$_INDEX_BY_NAME
};
static final int[] INDEX_BY_OFFSET = {
// gmt_offset,default_id,id_count,id_list
$_INDEX_BY_OFFSET
};
static final int[] INDEX_BY_COUNTRY = {
$_INDEX_BY_COUNTRY
};
}
END
open(OUT, ">$file") or die "Can't open $file for writing: $!";
print OUT $java;
close(OUT);
}
sub emitHTML {
my $file = shift;
my $zones = shift;
my $rules = shift;
my $equiv = shift;
my $offsetIndex = shift;
my $aliases = shift;
my $countries = shift;
my $_count = scalar keys %{$zones};
my $_equiv = scalar @$equiv;
my $_offsetTable = "<p><table>\n";
foreach (sort {$a <=> $b} keys %{$offsetIndex}) {
my $aref = $offsetIndex->{$_};
my $def = $aref->[0];
my @b = @{$aref}[1..$ my $gmtoff = "GMT" . formatOffset($_);
$_offsetTable .=
"<tr valign=top>" .
"<td><a name=\"" . bookmark($gmtoff) . "\">$gmtoff</a></td>" .
"<td>" .
join(", ", map($_ eq $def ?
"<a href=\"#" . bookmark($_) . "\"><b>$_</b></a>" :
"<a href=\"#" . bookmark($_) . "\">$_</a>", @b)) .
"</td>" .
"</tr>\n";
}
$_offsetTable .= "</table>\n";
my $_nameTable = "<p><table>\n";
$_nameTable .= "<tr><td>ID</td>";
$_nameTable .= "<td>Offset</td><td>DST Begins</td><td>DST Ends</td>";
$_nameTable .= "<td>Savings</td><td></td></tr>\n";
$_nameTable .= "<tr><td><hr></td>";
$_nameTable .= "<td><hr></td><td><hr></td>";
$_nameTable .= "<td><hr></td><td><hr></td><td></td></tr>\n";
my %revaliases = reverse(%$aliases);
foreach my $z (sort keys %$zones) {
$_nameTable .= emitHTMLZone($z, $zones->{$z}, $rules, $offsetIndex,
$aliases, \%revaliases);
}
$_nameTable .= "</table>\n";
my $_equivTable = "<p><table>\n";
$_equivTable .= "<tr><td>Offset</td><td>DST Begins</td><td>DST Ends</td>";
$_equivTable .= "<td>Savings</td><td>Zones</td></tr>\n";
$_equivTable .= "<tr><td><hr></td>";
$_equivTable .= "<td><hr></td><td><hr></td>";
$_equivTable .= "<td><hr></td><td><hr></td><td><hr></td></tr>\n";
foreach my $eg (@$equiv) {
$_equivTable .= emitHTMLEquiv($eg, $zones, $rules);
}
$_equivTable .= "</table>\n";
my $_countryTable;
$_countryTable .= "<p><table>\n";
$_countryTable .= "<tr><td>Country</td><td>Zones</td></tr>\n";
$_countryTable .= "<tr><td><hr></td><td><hr></td></tr>\n";
foreach my $country (sort keys %$countries) {
$_countryTable .=
"<tr valign=top><td nowrap>" .
(($country ne 'A') ? ($country . " (" . $COUNTRY_CODES{$country} . ")") : "(None)") .
"</td>" . "<td>" .
join(", ", map("<a href=\"#" . bookmark($_) . "\">$_</a>", @{$countries->{$country}->{zones}})) .
"</td></tr>\n";
}
$_countryTable .= "</table>\n";
my $_timeStamp = localtime;
my $html = <<"END";
<html>
<head>
<title>ICU System Time Zones</title>
</head>
<body>
<h1>ICU System Time Zones</h1>
<table border="0">
<tr>
<td>Version</td>
<td><strong>$RAW_VERSION</strong> ($VERSION_YEAR.$VERSION_SUFFIX)</td>
</tr>
<tr>
<td>Total zone count</td>
<td><strong>$_count</strong> in <strong>$_equiv</strong> equivalency groups</td>
</tr>
<tr>
<td>Original source</td>
<td><strong><a href="$URL">$URL</a></strong></td>
</tr>
<tr>
<td>Author</td>
<td><strong>Alan Liu <a href="mailto:liuas\@us.ibm.com"><liuas\@us.ibm.com></a></strong></td>
</tr>
<tr>
<td>This document generated</td>
<td><strong>$_timeStamp</strong></td>
</tr>
</table>
<h3>Background</h3>
<p>A time zone represents an offset applied to Greenwich Mean Time
(GMT) to obtain local time. The offset may vary throughout the year,
if daylight savings time (DST) is used, or may be the same all year
long. Typically, regions closer to the equator do not use DST. If DST
is in use, then specific rules define the point at which the offset
changes, and the amount by which it changes. Thus, a time zone is
described by the following information:
<ul>
<li><a name="cols">An</a> identifying string, or ID. This consists only of invariant characters (see the file <code>utypes.h</code>).
It typically has the format <em>continent</em> / <em>city</em>. The city chosen is
not the only city in which the zone applies, but rather a representative city for the
region. Some IDs consist of three or four uppercase letters; these are legacy zone
names that are aliases to standard zone names.</li>
<li>An offset from GMT, either positive or negative. Offsets range from approximately minus
half a day to plus half a day.</li>
</ul>
<p>If DST is observed, then three additional pieces of information are needed:
<ul>
<li>The precise date and time during the year when DST begins. This is in the first
half of the year in the northern hemisphere, and in the second half of the year in the
southern hemisphere.</li>
<li>The precise date and time during the year when DST ends. This is in the first half
of the year in the southern hemisphere, and in the second half of the year in the northern
hemisphere.</li>
<li>The amount by which the GMT offset changes when DST is in effect. This is almost
always one hour.</li>
</ul>
<h3>System and User Time Zones</h3>
<p>ICU supports local time zones through the classes
<code>TimeZone</code> and <code>SimpleTimeZone</code> in the C++
API. In the C API, time zones are designated by their ID strings.</p>
<p>Users may construct their own time zone objects by specifying the
above information to the C++ API. However, it is more typical for
users to use a pre-existing system time zone, since these represent
all current international time zones in use. This document lists the
system time zones, both in order of GMT offset, and in alphabetical
order of ID.</p>
<p>Since this list changes one or more times a year, <em>this document
only represents a snapshot</em>. For the current list of ICU system
zones, use the method <code>TimeZone::getAvailableIDs()</code>.</p>
<h3>Notes</h3>
<p><a name="order">The</a> zones are listed in binary sort order. That is, 'A' through
'Z' come before 'a' through 'z'. This is the same order in which the
zones are stored internally, and the same order in which they are
returned by <code>TimeZone::getAvailableIDs()</code>. The reason for
this is that ICU locates zones using a binary search, and the binary
search relies on this sort order.</p>
<p>You may notice that zones such as <a href="#EtcGMTp1">Etc/GMT+1</a>
appear to have the wrong sign for their GMT offset. In fact, their
sign is inverted because the the Etc zones follow the POSIX sign
conventions. This is the way the original Olson data is set up, and
ICU reproduces the Olson data faithfully, including this confusing
aspect. See the Olson files for more details.
<h3>References</h3>
<p>The ICU system time zones are derived from the Olson data at <a
href="$URL">$URL</a>. This is the data used by UNIX systems and is
updated one or more times each year. Unlike the Olson zone data, ICU
only contains data for current zone usage. There is no support for
historical zone data in ICU at this time.</p>
<hr>
<h2>Time Zones in order of GMT offset</h2>
<p>Zone listed in <strong>bold</strong> are the default zone for a
given GMT offset. This default is used by ICU if it cannot identify
the host OS time zone by name. In that case, it uses the default zone
for the host zone offset.</p>
$_offsetTable
<hr>
<h2>Time Zones in order of ID</h2>
<p>Zone listed in <strong>bold</strong> are the default zone for their
GMT offset. This default is used by ICU if it cannot identify the host
OS time zone by name. In that case, it uses the default zone for the
host zone offset. See above for a description of <a
href="#cols">columns</a>. See note above for an explanation of the
sort <a href="#order">order</a>.</p>
<p>Times suffixed with 's' are in standard time. Times suffixed with 'u' are in UTC time.
Times without suffixes are in wall time (that is, either standard time or daylight savings
time, depending on which is in effect).</p>
$_nameTable
<hr>
<h2>Time Zone Equivalency Groups</h2>
<p>ICU groups zones into <em>equivalency groups</em>. These are
groups of zones that are identical in GMT offset and in rules, but
that have different IDs. Knowledge of equivalency groups allows ICU
to reduce the amount of data stored. More importantly, it allows ICU
to apply data for one zone to other equivalent zones when appropriate
(e.g., in formatting). Equivalency groups are formed at build time,
not at runtime, so the runtime cost to lookup the equivalency group of
a given zone is negligible.</p>
$_equivTable
<hr>
<h2>Time Zones by Country</h2>
<p>ICU captures and exports the country data from the Olson database.
The country code is the ISO 3166 two-letter code. Some zones have no
associated country; these are listed under the entry "(None)".
$_countryTable
</body>
</html>
END
open(HTML, ">$file") or die "Can't open $file for writing: $!";
print HTML $html;
close(HTML);
}
sub bookmark {
local $_ = shift;
s/-/m/g;
s/\+/p/g;
s/\W//g;
$_;
}
sub emitHTMLEquiv {
my $eg = shift;
my $zone = shift;
my $rule = shift;
local $_ = "<tr valign=top>";
$_ .= _emitHTMLZone($zone->{$eg->[0]}, $rule);
$_ .= "<td>" . join(" ", @$eg) . "</td>";
$_ .= "</tr>\n";
$_;
}
sub _emitHTMLZone {
my ($zone, $rules) = @_;
my $gmtoff = "GMT" . formatOffset(TZ::ParseOffset($zone->{gmtoff}));
local $_ = "<td><a href=\"#" . bookmark($gmtoff) . "\">$gmtoff</a></td>";
if ($zone->{rule} ne $TZ::STANDARD) {
my $rule = $rules->{$zone->{rule}};
$_ .= "<td nowrap>" . emitHTMLRule($rule->[0]) . "</td>";
$_ .= "<td nowrap>" . emitHTMLRule($rule->[1]) . "</td>";
$_ .= "<td>" . $rule->[0]->{save} . "</td>";
} else {
$_ .= "<td colspan=3></td>";
}
$_;
}
sub emitHTMLZone {
my ($name, $zone, $rules, $offsetIndex, $aliases, $revaliases) = @_;
my $isDefault = isDefault($name, $zone->{gmtoff}, $offsetIndex);
my $alias = exists $aliases->{$name} ? $aliases->{$name} : '';
my $revalias = exists $revaliases->{$name} ? $revaliases->{$name} : '';
local $_ = "<tr><td>" . ($isDefault?"<b>":"") .
"<a name=\"" . bookmark($name) . "\">$name</a>" . ($isDefault?"</b>":"") . "</td>";
$_ .= _emitHTMLZone($zone, $rules);
if ($alias) {
$_ .= "<td><em>alias for</em> <a href=\"#" .
bookmark($alias) . "\">$alias</a></td>";
} elsif ($revalias) {
$_ .= "<td><em>alias </em> <a href=\"#" .
bookmark($revalias) . "\">$revalias</a></td>";
} else {
$_ .= "<td></td>";
}
$_ .= "</tr>\n";
$_;
}
sub emitHTMLRule {
my $rule = shift;
$rule->{in} ." ". $rule->{on} ." ". $rule->{at};
}
sub incorporateAliases {
my $aliasFile = shift;
my $zones = shift;
my $links = shift;
my $n = 0;
my %hash;
local *IN;
open(IN,$aliasFile) or die "Can't open $aliasFile: $!";
while (<IN>) {
s/\ next unless (/\S/); if (/^\s*(\S+)\s+(\S+)\s*$/) {
my ($alias, $original) = ($1, $2);
if (exists $zones->{$alias}) {
die "Bad alias in $aliasFile: $alias is a standard UNIX zone. " .
"Please remove $alias from the alias table.\n";
}
if (!exists $zones->{$original}) {
die "Bad alias in $aliasFile: $alias maps to the nonexistent " .
"zone $original. Please fix this entry in the alias table.\n";
}
if (exists $links->{$alias} &&
$links->{$alias} ne $original) {
print STDERR "Warning: Alias $alias for $original exists as link for ",
$links->{$alias}, "\n";
}
$zones->{$alias} = $zones->{$original};
$hash{$alias} = $original;
$n++;
} else {
die "Bad line in alias table $aliasFile: $_\n";
}
}
print "Incorporated $n aliases from $aliasFile\n";
close(IN);
\%hash;
}
sub formatZone { my $name = shift;
my $zone = shift;
my $rules = shift;
my @spec;
my @notes;
push @notes, ($zone->{gmtoff}=~/^-/?"GMT":"GMT+") . $zone->{gmtoff};
push @spec, TZ::ParseOffset($zone->{gmtoff});
my $rule = $zone->{rule};
if ($rule ne $TZ::STANDARD) {
$rule = $rules->{$rule};
if (@{$rule} >= 4 && ($rule->[3] & 1) && ($rule->[3] & 2)) {
formatRule($rule->[0], \@spec, \@notes); formatRule($rule->[1], \@spec, \@notes);
my @a = parseTime($rule->[0]->{save});
if ($a[1] ne 'w') {
die "Strange DST savings value: \"$rule->[0]->{save}\"";
}
push @notes, $rule->[0]->{save};
push @spec, $a[0];
}
}
(\@spec, \@notes);
}
sub formatRule {
my $rule = shift;
my $spec = shift;
my $notes = shift;
push @$notes, $rule->{in}, $rule->{on}, $rule->{at};
push @$spec, parseMonth($rule->{in}); push @$spec, parseDaySpecifier($rule->{on}); push @$spec, parseTime($rule->{at}); }
sub formatOffset {
local $_ = shift;
my $result = $_<0 ? "-":"+";
$_ = -$_ if ($_ < 0);
my $sec = $_ % 60; $_ = ($_ - $sec) / 60;
my $min = $_ % 60; $_ = ($_ - $min) / 60;
$min = "0$min" if ($min < 10);
$sec = $sec ? ($sec < 10 ? ":0$sec" : ":$sec") : "";
$result . $_ . ":" . $min . $sec;
}
sub parseTime {
local $_ = shift;
if (/^(\d{1,2}):(\d\d)([su])?$/) {
my $a = ($1*60) + $2;
my $s = defined $3?$3:'w';
return ( $a, $s );
} else {
die "Cannot parse time \"$_\"";
}
}
sub parseMonth {
local $_ = shift;
for (my $i=0; $i<12; $i++) {
return $i if (/$MONTH[$i]/i);
}
die "Can't parse month \"$_\"";
}
sub parseDaySpecifier {
local $_ = shift;
my $dowim;
my $dow = 0;
if (/^\d+$/) {
$dowim = $_;
$dow = 0;
return ( $dowim, $dow );
}
my @DOW = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
for (my $i=0; $i<@DOW; $i++) {
if (s/$DOW[$i]//) {
$dow = $i + 1;
last;
}
}
if ($dow == 0) {
die "Cannot parse day specifier \"$_\"";
}
if (/^last$/) {
$dowim = -1;
} elsif (/^first$/) {
$dowim = 1;
} elsif (/^>=(\d+)$/) {
$dowim = $1;
$dow = -$dow;
} elsif (/^<=(\d+)$/) {
$dowim = -$1;
$dow = -$dow;
} else {
die "Cannot parse day specifier \"$_\"";
}
( $dowim, $dow );
}
sub assertInvariantChars {
local $_ = shift;
if (/[^A-Za-z0-9 \"%&\'()*+,-.\/:;<=>?_]/) {
die "Error: Zone ID \"$_\" contains non-invariant characters\n";
}
}
sub equivIndexOf {
my $id = shift;
my $a = shift;
for (my $i=0; $i < scalar @{$a}; ++$i) {
my $aa = $a->[$i];
foreach (@$aa) {
return $i if ($_ eq $id);
}
}
return -1;
}
__END__