#!/usr/bin/perl -w # Create the timezone tables for java/util/TimeZone from the # standard timezone sources by Arthur David Olson (as used by glibc) # # This needs the files from the package tzdata2000h which may be found # at ftp://ftp.cs.mu.oz.au/pub/. $TIMEZONEDIR = "tzdata"; @TIMEZONEFILES = ("africa", "antarctica", "asia", "australasia", "europe", "northamerica", "pacificnew", "southamerica", "../tzabbrevs"); # rules hash table: # key is a rule name # value is either "-" (no daylight savings) or a list of three elements: # $value[0] = end savings rule (list containing MONTH, DAY and TIME) # $value[1] = start savings rule (ditto) # $value[2] = daylight offset in milliseconds my %rules = ("-" => "-"); # timezones list, list of pairs: # $timezones[$i][0] is a timezone name # $timezones[$i][1] = raw offset in milliseconds # $timezones[$i][2] = rule in the same format as the value of # the rules table, but TIME in milliseconds # $timezones[$i][3] = list of timezone names with this rule (aliases) my @timezones = ( [ "GMT", 0, "-", [ "GMT", "UTC" ] ]); # parse the offset of form +/-hh:mm:ss (:ss is optional) and return it # in milliseconds against UTC sub parseOffset($) { my $offset = $_[0]; $offset =~ /^([+-]?)(\d+)(:(\d+)(:(\d+))?)?$/ or die "Can't parse offset $offset"; my $seconds = $2 * 3600; $seconds += $4 * 60 if ($3); $seconds += $6 if ($3 && $5); if ($1 eq "-") { $seconds = - $seconds; } return $seconds * 1000; } # parse the time of form +/-hh:mm:ss[swguz] (:ss is optional) and return it # in milliseconds since midnight in local wall time my $timezonename; sub parseTime($$$) { my ($rawoffset, $stdoffset, $time) = @_; $time =~ /^([+-]?)(\d+):(\d+)(:(\d+))?([swguz]?)$/ or die "Can't parse time $time"; my ($hour, $min) = ($2, $3); my $sec = ($4) ? $5 : 0; my $millis = ((($hour * 60) + $min) * 60 + $sec) * 1000; if ($1 eq "-") { $millis = -$millis; } # Normally millis is in wall time, adjust for utc and standard time. if ($6 =~ /[guz]/) { $millis += $rawoffset + $stdoffset; } elsif ($6 =~ /s/) { $millis += $stdoffset; } return $millis; } my %monthnames = ( "Jan" => "1", "Feb" => "2", "Mar" => "3", "Apr" => "4", "May" => "5", "Jun" => "6", "Jul" => "7", "Aug" => "8", "Sep" => "9", "Oct" => "10", "Nov" => "11", "Dec" => "12" ); sub parseMonth($) { my $month = $monthnames{"$_[0]"} or die "Unknown month $_[0]"; return $month; } my %weekdaynames = ( "Sun" => "7", "Mon" => "1", "Tue" => "2", "Wed" => "3", "Thu" => "4", "Fri" => "5", "Sat" => "6" ); sub parseWeekday($) { my $weekday = $weekdaynames{"$_[0]"} or die "Unknown weekday $_[0]"; return $weekday; } my @weekdayjavanames = ( "Calendar.SUNDAY", "Calendar.MONDAY", "Calendar.TUESDAY", "Calendar.WEDNESDAY", "Calendar.THURSDAY", "Calendar.FRIDAY", "Calendar.SATURDAY" ); my @daysInMonths = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ); sub parseDay($$$) { my ($dayoffset, $month, $day) = @_; if ($day =~ /^\d+$/) { return "$day, 0"; } elsif ($day =~ /^last([A-Z][a-z][a-z])$/) { my $weekday = ( parseWeekday($1) + $dayoffset + 7 ) % 7; if ($dayoffset) { my $day = $daysInMonths[$month - 1] + $dayoffset; warn "Can only approximate $day with dayoffset in $file" if ($month == 2); return "$day, -$weekdayjavanames[$weekday]"; } else { return "-1, $weekdayjavanames[$weekday]"; } } elsif ($day =~ /^([A-Z][a-z][a-z])>=(\d+)$/) { my $start = $2 + $dayoffset; my $weekday = ( parseWeekday($1) + $dayoffset + 7 ) % 7; if (($start % 7) == 1) { $start = ($start + 6) / 7; return "$start, $weekdayjavanames[$weekday]"; } else { return "$start, -$weekdayjavanames[$weekday]"; } } else { die "Unknown day $day"; } } my @monthjavanames = ( "Calendar.JANUARY", "Calendar.FEBRUARY", "Calendar.MARCH", "Calendar.APRIL", "Calendar.MAY", "Calendar.JUNE", "Calendar.JULY", "Calendar.AUGUST", "Calendar.SEPTEMBER", "Calendar.OCTOBER", "Calendar.NOVEMBER", "Calendar.DECEMBER" ); sub parseRule($$$) { my ($rawoffset, $stdoffset, $rule) = @_; my $monthnr = parseMonth($rule->[0]); my $time = parseTime($rawoffset, $stdoffset, $rule->[2]); my $dayoffset = 0; while ($time < 0) { $time += 24*3600*1000; $dayoffset--; } while ($time > 24*3600*1000) { $time -= 24*3600*1000; $dayoffset++; } $day = parseDay($dayoffset, $monthnr, $rule->[1]); return [ $monthjavanames[$monthnr-1], $day, $time ]; } sub ruleEquals($$) { my ($rule1, $rule2) = @_; # check month names return (($rule1->[0] eq $rule2->[0]) && ($rule1->[1] eq $rule2->[1]) && ($rule1->[2] == $rule2->[2])); } sub findAlias($$) { my ($rawoffset, $rule) = @_; foreach $tz (@timezones) { my ($key, $tzoffset, $tzrule, $aliaslist) = @{$tz}; next if ($tzoffset != $rawoffset); if ($rule eq "-") { return $tz if ($tzrule eq "-"); } elsif ($tzrule ne "-") { next if $rule->[2] != $tzrule->[2]; if (ruleEquals($rule->[0], $tzrule->[0]) && ruleEquals($rule->[1], $tzrule->[1])) { return $tz; } } } return ""; } sub makePretty($) { my ($offset) = @_; if (($offset % 3600) == 0) { $offset /= 3600; return "$offset * 3600"; } else { return "$offset"; } } sub tzcompare($$) { my ($a, $b) = @_; if (($a =~ /\//) != ($b =~ /\//)) { return ($a =~ /\//) ? 1 : -1; } else { return $a cmp $b; } } foreach $file (@TIMEZONEFILES) { # print STDERR "$file\n"; open INPUT, "$TIMEZONEDIR/$file" or die "Can't open $TIMEZONEDIR/$file"; my $in_time_zone = 0; # As Zone can reference even Rule which is defined after the # current line, parse the file in 2 passes. # First pass, parse just Rule lines: while () { $_ = $1 if /^([^\#]*)\#/; next if /^\s*$/; my @entries = split; # $, = ","; print "'$_' -> [",@entries,"]\n"; if (!$in_time_zone) { if ($entries[0] eq "Rule") { # check if rule still applies # column 3 is TO entry. if ($entries[3] eq "max") { my $rulename = $entries[1]; my $month = $entries[5]; my $day = $entries[6]; my $time = $entries[7]; if ($entries[8] eq "0") { # This is the end time rule $rules{"$rulename"}[0] = [ $month, $day, $time ]; } else { # This is the start time rule $rules{"$rulename"}[1] = [ $month, $day, $time ]; $rules{"$rulename"}[2] = parseOffset($entries[8]); } } } elsif ($entries[0] eq "Zone") { $in_time_zone = 1; shift @entries; shift @entries; } elsif (!(($entries[0] eq "Remove") || ($entries[0] eq "Link"))) { die "Unknown command: $_"; } } if ($in_time_zone) { die "early end of Zone: $_" if ($entries[0] =~ /^[A-Za-z]+/); if (@entries <= 3) { $in_time_zone = 0; } } } close INPUT; open INPUT, "$TIMEZONEDIR/$file" or die "Can't open $TIMEZONEDIR/$file"; $in_time_zone = 0; # Second pass, parse all but Rule lines. while () { $_ = $1 if /^([^\#]*)\#/; next if /^\s*$/; my @entries = split; # $, = ","; print "'$_' -> [",@entries,"]\n"; if (!$in_time_zone) { if ($entries[0] eq "Rule") { # Do nothing, this was already handled in the # first pass. } elsif ($entries[0] eq "Zone") { $in_time_zone = 1; shift @entries; $timezonename = shift @entries; } elsif ($entries[0] eq "Remove") { my $found = 0; foreach $tz (@timezones) { my @newaliases; foreach $tzname (@{$tz->[3]}) { if ($tzname eq $entries[1]) { $found = 1; } else { push @newaliases, $tzname; } } if ($found) { if ($tz->[0] eq $entries[1]) { $tz->[0] = $newaliases[0]; } $tz->[3] = \@newaliases; last; } } die "Unknown link $_" if ! $found; } elsif ($entries[0] eq "Link") { my $alias = 0; foreach $tz (@timezones) { foreach $tzname (@{$tz->[3]}) { if ($tzname eq $entries[1]) { $alias = $tz; last; } } } die "Unknown link $_" if ! $alias; die "@entries" if $entries[1] =~ /^\d+$/; push @{$alias->[3]}, $entries[2]; } else { die "Unknown command: $_"; } } if ($in_time_zone) { die "early end of Zone: $_" if ($entries[0] =~ /^[A-Za-z]+/); if (@entries <= 3) { # print "found ZONE $timezonename $entries[0] $entries[1] $entries[2]\n"; # This is the last line and the only we look at. # other lines are for historic time zones. my $rawoffset = parseOffset($entries[0]); my $rule = $rules{"$entries[1]"} || "-"; if ($rule ne "-") { if (!defined($rule->[2])) { $rule = "-"; } else { # now we can parse the time since we know raw offset. my $savings = $rule->[2]; my $endrule = parseRule($rawoffset, $savings, $rule->[0]); my $startrule = parseRule($rawoffset, 0, $rule->[1]); $rule = [ $endrule, $startrule, $savings ]; # print "start",@{$rule->[1]}, "end", @{$rule->[0]}, # "offset", $rule->[2],"\n"; } } my $alias = findAlias($rawoffset, $rule); if ($alias) { if (($alias->[0] =~ /\//) && ($timezonename !~ /\//)) { # alias is of Country/City form, timezonename not # make timezonename the real zone name $alias->[0] = $timezonename; } push @{$alias->[3]}, $timezonename; } else { push @timezones, [ $timezonename, $rawoffset, $rule, [ $timezonename ] ]; } $in_time_zone = 0; } } } close INPUT; } @timezones = sort { if ($a->[1] != $b->[1]) { $a->[1] <=> $b->[1] } else { $a->[0] cmp $b->[0] } } @timezones; for (@timezones) { my ($name, $rawoffset, $rule, $aliaslist) = @{$_}; my @aliases = sort { tzcompare($a, $b); } @{$aliaslist}; $name = $aliases[0]; $rawoffset = makePretty($rawoffset); if ($rule eq "-") { print <[0]}; my ($startmonth, $startday, $starttime) = @{$rule->[1]}; $endtime = makePretty($endtime); $starttime = makePretty($starttime); my $savings = $rule->[2]; if ($savings == 3600 * 1000) { print <