package Date::Manip::TZdata;
# Copyright (c) 2008-2017 Sullivan Beck. All rights reserved.
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.
###############################################################################
require 5.010000;
use IO::File;
use Date::Manip::Base;
use strict;
use integer;
use warnings;
our $VERSION;
$VERSION='6.60';
END { undef $VERSION; }
###############################################################################
# GLOBAL VARIABLES
###############################################################################
our ($Verbose,@StdFiles,$dmb);
END {
undef $Verbose;
undef @StdFiles;
undef $dmb;
}
$dmb = new Date::Manip::Base;
# Whether to print some debugging stuff.
$Verbose = 0;
# Standard tzdata files that need to be parsed.
@StdFiles = qw(africa
antarctica
asia
australasia
europe
northamerica
pacificnew
southamerica
etcetera
backward
);
our ($TZ_DOM,$TZ_LAST,$TZ_GE,$TZ_LE);
END {
undef $TZ_DOM;
undef $TZ_LAST;
undef $TZ_GE;
undef $TZ_LE;
}
$TZ_DOM = 1;
$TZ_LAST = 2;
$TZ_GE = 3;
$TZ_LE = 4;
our ($TZ_STANDARD,$TZ_RULE,$TZ_OFFSET);
END {
undef $TZ_STANDARD;
undef $TZ_RULE;
undef $TZ_OFFSET;
}
$TZ_STANDARD = 1;
$TZ_RULE = 2;
$TZ_OFFSET = 3;
###############################################################################
# BASE METHODS
###############################################################################
#
# The Date::Manip::TZdata object is a hash of the form:
#
# { dir => DIR where to find the tzdata directory
# zone => { ZONE => [ ZONEDESC ] }
# ruleinfo => { INFO => [ VAL ... ] }
# zoneinfo => { INFO => [ VAL ... ] }
# zonelines => { ZONE => [ VAL ... ] }
# }
sub new {
my($class,$dir) = @_;
$dir = '.' if (! $dir);
if (! -d "$dir/tzdata") {
die "ERROR: no tzdata directory found\n";
}
my $self = {
'dir' => $dir,
'zone' => {},
'ruleinfo' => {},
'zoneinfo' => {},
'zonelines' => {},
};
bless $self, $class;
$self->_tzd_ParseFiles();
return $self;
}
###############################################################################
# RULEINFO
###############################################################################
my($Error);
# @info = $tzd->ruleinfo($rule,@args);
#
# This takes the name of a set of rules (e.g. NYC or US as defined in
# the zoneinfo database) and returns information based on the arguments
# given.
#
# @args
# ------------
#
# rules YEAR : Return a list of all rules used during that year
# stdlett YEAR : The letter(s) used during standard time that year
# savlett YEAR : The letter(s) used during saving time that year
# lastoff YEAR : Returns the last DST offset of the year
# rdates YEAR : Returns a list of critical dates for the given
# rule during a year. It returns:
# (date dst_offset timetype lett ...)
# where dst_offset is the daylight saving time offset
# that starts at that date and timetype is 'u', 'w', or
# 's', and lett is the letter to use in the abbrev.
#
sub _ruleInfo {
my($self,$rule,$info,@args) = @_;
my $year = shift(@args);
if (exists $$self{'ruleinfo'}{$info} &&
exists $$self{'ruleinfo'}{$info}{$rule} &&
exists $$self{'ruleinfo'}{$info}{$rule}{$year}) {
if (ref $$self{'ruleinfo'}{$info}{$rule}{$year}) {
return @{ $$self{'ruleinfo'}{$info}{$rule}{$year} };
} else {
return $$self{'ruleinfo'}{$info}{$rule}{$year};
}
}
if ($info eq 'rules') {
my @ret;
foreach my $r ($self->_tzd_Rule($rule)) {
my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
$lett) = @$r;
next if ($y0>$year || $y1<$year);
push(@ret,$r) if ($ytype eq "-" ||
$year == 9999 ||
($ytype eq 'even' && $year =~ /[02468]$/) ||
($ytype eq 'odd' && $year =~ /[13579]$/));
}
# We'll sort them... if there are ever two time changes in a
# single month, this will cause problems... hopefully there
# never will be.
@ret = sort { $$a[3] <=> $$b[3] } @ret;
$$self{'ruleinfo'}{$info}{$rule}{$year} = [ @ret ];
return @ret;
} elsif ($info eq 'stdlett' ||
$info eq 'savlett') {
my @rules = $self->_ruleInfo($rule,'rules',$year);
my %lett = ();
foreach my $r (@rules) {
my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
$lett) = @$r;
$lett{$lett} = 1
if ( ($info eq 'stdlett' && $offset eq '00:00:00') ||
($info eq 'savlett' && $offset ne '00:00:00') );
}
my $ret;
if (! %lett) {
$ret = '';
} else {
$ret = join(",",sort keys %lett);
}
$$self{'ruleinfo'}{$info}{$rule}{$year} = $ret;
return $ret;
} elsif ($info eq 'lastoff') {
my $ret;
my @rules = $self->_ruleInfo($rule,'rules',$year);
return '00:00:00' if (! @rules);
my $r = pop(@rules);
my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
$lett) = @$r;
$$self{'ruleinfo'}{$info}{$rule}{$year} = $offset;
return $offset;
} elsif ($info eq 'rdates') {
my @ret;
my @rules = $self->_ruleInfo($rule,'rules',$year);
foreach my $r (@rules) {
my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
$lett) = @$r;
my($date) = $self->_tzd_ParseRuleDate($year,$mon,$dow,$num,$flag,$time);
push(@ret,$date,$offset,$timetype,$lett);
}
$$self{'ruleinfo'}{$info}{$rule}{$year} = [ @ret ];
return @ret;
}
}
###############################################################################
# ZONEINFO
###############################################################################
# zonelines is:
# ( ZONE => numlines => N,
# I => { start => DATE,
# end => DATE,
# stdoff => OFFSET,
# dstbeg => OFFSET,
# dstend => OFFSET,
# letbeg => LETTER,
# letend => LETTER,
# abbrev => ABBREV,
# rule => RULE
# }
# )
# where I = 1..N
# start, end the wallclock start/end time of this period
# stdoff the standard GMT offset during this period
# dstbeg the DST offset at the start of this period
# dstend the DST offset at the end of this period
# letbeg the letter (if any) used at the start of this period
# letend the letter (if any) used at the end of this period
# abbrev the zone abbreviation during this period
# rule the rule that applies (if any) during this period
# @info = $tzd->zoneinfo($zone,@args);
#
# Obtain information from a zone
#
# @args
# ------------
#
# zonelines Y : Return the full zone line(s) which apply for
# a given year.
# rules YEAR : Returns a list of rule names and types which
# apply for the given year.
#
sub _zoneInfo {
my($self,$zone,$info,@args) = @_;
if (! exists $$self{'zonelines'}{$zone}) {
$self->_tzd_ZoneLines($zone);
}
my @z = $self->_tzd_Zone($zone);
shift(@z); # Get rid of timezone name
my $ret;
# if ($info eq 'numzonelines') {
# return $$self{'zonelines'}{$zone}{'numlines'};
# } elsif ($info eq 'zoneline') {
# my ($i) = @args;
# my @ret = map { $$self{'zonelines'}{$zone}{$i}{$_} }
# qw(start end stdoff dstbeg dstend letbeg letend abbrev rule);
# return @ret;
# }
my $y = shift(@args);
if (exists $$self{'zoneinfo'}{$info} &&
exists $$self{'zoneinfo'}{$info}{$zone} &&
exists $$self{'zoneinfo'}{$info}{$zone}{$y}) {
if (ref($$self{'zoneinfo'}{$info}{$zone}{$y})) {
return @{ $$self{'zoneinfo'}{$info}{$zone}{$y} };
} else {
return $$self{'zoneinfo'}{$info}{$zone}{$y};
}
}
if ($info eq 'zonelines') {
my (@ret);
while (@z) {
# y = 1920
# until = 1919 NO
# until = 1920 NO
# until = 1920 Feb... YES
# until = 1921... YES, last
my $z = shift(@z);
my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
$timetype,$start,$end) = @$z;
next if ($yr < $y);
next if ($yr == $y && $flag == $TZ_DOM &&
$mon == 1 && $num == 1 && $time eq '00:00:00');
push(@ret,$z);
last if ($yr > $y);
}
$$self{'zoneinfo'}{$info}{$zone}{$y} = [ @ret ];
return @ret;
} elsif ($info eq 'rules') {
my (@ret);
@z = $self->_zoneInfo($zone,'zonelines',$y);
foreach my $z (@z) {
my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
$timetype,$start,$end) = @$z;
push(@ret,$rule,$ruletype);
}
$$self{'zoneinfo'}{$info}{$zone}{$y} = [ @ret ];
return @ret;
}
}
########################################################################
# PARSING TZDATA FILES
########################################################################
# These routine parses the raw tzdata file. Files contain three types
# of lines:
#
# Link CANONICAL ALIAS
# Rule NAME FROM TO TYPE IN ON AT SAVE LETTERS
# Zone NAME GMTOFF RULE FORMAT UNTIL
# GMTOFF RULE FORMAT UNTIL
# ...
# GMTOFF RULE FORMAT
# Parse all files
sub _tzd_ParseFiles {
my($self) = @_;
print "PARSING FILES...\n" if ($Verbose);
foreach my $file (@StdFiles) {
$self->_tzd_ParseFile($file);
}
$self->_tzd_CheckData();
}
# Parse a file
sub _tzd_ParseFile {
my($self,$file) = @_;
my $in = new IO::File;
my $dir = $$self{'dir'};
print "... $file\n" if ($Verbose);
if (! $in->open("$dir/tzdata/$file")) {
warn "WARNING: [parse_file] unable to open file: $file: $!\n";
return;
}
my @in = <$in>;
$in->close;
chomp(@in);
# strip out comments
foreach my $line (@in) {
$line =~ s/^\s+//;
$line =~ s/#.*$//;
$line =~ s/\s+$//;
}
# parse all lines
my $n = 0; # line number
my $zone = ''; # current zone (if in a multi-line zone section)
while (@in) {
if (! $in[0]) {
$n++;
shift(@in);
} elsif ($in[0] =~ /^Zone/) {
$self->_tzd_ParseZone($file,\$n,\@in);
} elsif ($in[0] =~ /^Link/) {
$self->_tzd_ParseLink($file,\$n,\@in);
} elsif ($in[0] =~ /^Rule/) {
$self->_tzd_ParseRule($file,\$n,\@in);
} else {
$n++;
my $line = shift(@in);
warn "WARNING: [parse_file] unknown line: $n\n" .
" $line\n";
}
}
}
sub _tzd_ParseLink {
my($self,$file,$n,$lines) = @_;
$$n++;
my $line = shift(@$lines);
my(@tmp) = split(/\s+/,$line);
if ($#tmp != 2 || lc($tmp[0]) ne 'link') {
warn "ERROR: [parse_file] invalid Link line: $file: $$n\n" .
" $line\n";
return;
}
my($tmp,$zone,$alias) = @tmp;
if ($self->_tzd_Alias($alias)) {
warn "WARNING: [parse_file] alias redefined: $file: $$n: $alias\n";
}
$self->_tzd_Alias($alias,$zone);
}
sub _tzd_ParseRule {
my($self,$file,$n,$lines) = @_;
$$n++;
my $line = shift(@$lines);
my(@tmp) = split(/\s+/,$line);
if ($#tmp != 9 || lc($tmp[0]) ne 'rule') {
warn "ERROR: [parse_file] invalid Rule line: $file: $$n:\n" .
" $line\n";
return;
}
my($tmp,$name,$from,$to,$type,$in,$on,$at,$save,$letters) = @tmp;
$self->_tzd_Rule($name,[ $from,$to,$type,$in,$on,$at,$save,$letters ]);
}
sub _tzd_ParseZone {
my($self,$file,$n,$lines) = @_;
# Remove "Zone America/New_York" from the first line
$$n++;
my $line = shift(@$lines);
my @tmp = split(/\s+/,$line);
if ($#tmp < 4 || lc($tmp[0]) ne 'zone') {
warn "ERROR: [parse_file] invalid Zone line: $file :$$n\n" .
" $line\n";
return;
}
shift(@tmp);
my $zone = shift(@tmp);
$line = join(' ',@tmp);
unshift(@$lines,$line);
# Store the zone name information
if ($self->_tzd_Zone($zone)) {
warn "ERROR: [parse_file] zone redefined: $file: $$n: $zone\n";
$self->_tzd_DeleteZone($zone);
}
$self->_tzd_Alias($zone,$zone);
# Parse all zone lines
while (1) {
last if (! @$lines);
$line = $$lines[0];
return if ($line =~ /^(zone|link|rule)/i);
$$n++;
shift(@$lines);
next if (! $line);
@tmp = split(/\s+/,$line);
if ($#tmp < 2) {
warn "ERROR: [parse_file] invalid Zone line: $file: $$n\n" .
" $line\n";
return;
}
my($gmt,$rule,$format,@until) = @tmp;
$self->_tzd_Zone($zone,[ $gmt,$rule,$format,@until ]);
}
}
sub _tzd_CheckData {
my($self) = @_;
print "CHECKING DATA...\n" if ($Verbose);
$self->_tzd_CheckRules();
$self->_tzd_CheckZones();
$self->_tzd_CheckAliases();
}
########################################################################
# LINKS (ALIASES)
########################################################################
sub _tzd_Alias {
my($self,$alias,$zone) = @_;
if (defined $zone) {
$$self{'alias'}{$alias} = $zone;
return;
} elsif (exists $$self{'alias'}{$alias}) {
return $$self{'alias'}{$alias};
} else {
return '';
}
}
sub _tzd_DeleteAlias {
my($self,$alias) = @_;
delete $$self{'alias'}{$alias};
}
sub _tzd_AliasKeys {
my($self) = @_;
return keys %{ $$self{'alias'} };
}
# TZdata file:
#
# Link America/Denver America/Shiprock
#
# Stored locally as:
#
# (
# "us/eastern" => "America/New_York"
# "america/new_york" => "America/New_York"
# )
sub _tzd_CheckAliases {
my($self) = @_;
# Replace
# ALIAS1 -> ALIAS2 -> ... -> ZONE
# with
# ALIAS1 -> ZONE
print "... aliases\n" if ($Verbose);
ALIAS:
foreach my $alias ($self->_tzd_AliasKeys()) {
my $zone = $self->_tzd_Alias($alias);
my %tmp;
$tmp{$alias} = 1;
while (1) {
if ($self->_tzd_Zone($zone)) {
$self->_tzd_Alias($alias,$zone);
next ALIAS;
} elsif (exists $tmp{$zone}) {
warn "ERROR: [check_aliases] circular alias definition: $alias\n";
$self->_tzd_DeleteAlias($alias);
next ALIAS;
} elsif ($self->_tzd_Alias($zone)) {
$tmp{$zone} = 1;
$zone = $self->_tzd_Alias($zone);
next;
}
warn "ERROR: [check_aliases] unresolved alias definition: $alias\n";
$self->_tzd_DeleteAlias($alias);
next ALIAS;
}
}
}
########################################################################
# PARSING RULES
########################################################################
sub _tzd_Rule {
my($self,$rule,$listref) = @_;
if (defined $listref) {
if (! exists $$self{'rule'}{$rule}) {
$$self{'rule'}{$rule} = [];
}
push @{ $$self{'rule'}{$rule} }, [ @$listref ];
} elsif (exists $$self{'rule'}{$rule}) {
return @{ $$self{'rule'}{$rule} };
} else {
return ();
}
}
sub _tzd_DeleteRule {
my($self,$rule) = @_;
delete $$self{'rule'}{$rule};
}
sub _tzd_RuleNames {
my($self) = @_;
return keys %{ $$self{'rule'} };
}
sub _tzd_CheckRules {
my($self) = @_;
print "... rules\n" if ($Verbose);
foreach my $rule ($self->_tzd_RuleNames()) {
$Error = 0;
my @rule = $self->_tzd_Rule($rule);
$self->_tzd_DeleteRule($rule);
while (@rule) {
my($from,$to,$type,$in,$on,$at,$save,$letters) =
@{ shift(@rule) };
my($dow,$num,$attype);
$from = $self->_rule_From ($rule,$from);
$to = $self->_rule_To ($rule,$to,$from);
$type = $self->_rule_Type ($rule,$type);
$in = $self->_rule_In ($rule,$in);
($on,$dow,$num) = $self->_rule_On ($rule,$on);
($attype,$at) = $self->_rule_At ($rule,$at);
$save = $self->_rule_Save ($rule,$save);
$letters = $self->_rule_Letters($rule,$letters);
if (! $Error) {
$self->_tzd_Rule($rule,[ $from,$to,$type,$in,$on,$dow,$num,$attype,
$at,$save,$letters ]);
}
}
$self->_tzd_DeleteRule($rule) if ($Error);
}
}
# TZdata file:
#
# #Rule NAME FROM TO TYPE IN ON AT SAVE LETTER
# Rule NYC 1920 only - Mar lastSun 2:00 1:00 D
# Rule NYC 1920 only - Oct lastSun 2:00 0 S
# Rule NYC 1921 1966 - Apr lastSun 2:00 1:00 D
# Rule NYC 1921 1954 - Sep lastSun 2:00 0 S
# Rule NYC 1955 1966 - Oct lastSun 2:00 0 S
#
# Stored locally as:
#
# %Rule = (
# 'NYC' =>
# [
# [ 1920 1920 - 3 2 7 0 w 02:00:00 01:00:00 D ],
# [ 1920 1920 - 10 2 7 0 w 02:00:00 00:00:00 S ],
# [ 1921 1966 - 4 2 7 0 w 02:00:00 01:00:00 D ],
# [ 1921 1954 - 9 2 7 0 w 02:00:00 00:00:00 S ],
# [ 1955 1966 - 10 2 7 0 w 02:00:00 00:00:00 S ],
# ],
# 'US' =>
# [
# [ 1918 1919 - 3 2 7 0 w 02:00:00 01:00:00 W ],
# [ 1918 1919 - 10 2 7 0 w 02:00:00 00:00:00 S ],
# [ 1942 1942 - 2 1 0 9 w 02:00:00 01:00:00 W ],
# [ 1945 1945 - 9 1 0 30 w 02:00:00 00:00:00 S ],
# [ 1967 9999 - 10 2 7 0 u 02:00:00 00:00:00 S ],
# [ 1967 1973 - 4 2 7 0 w 02:00:00 01:00:00 D ],
# [ 1974 1974 - 1 1 0 6 w 02:00:00 01:00:00 D ],
# [ 1975 1975 - 2 1 0 23 w 02:00:00 01:00:00 D ],
# [ 1976 1986 - 4 2 7 0 w 02:00:00 01:00:00 D ],
# [ 1987 9999 - 4 3 7 1 u 02:00:00 01:00:00 D ],
# ]
# )
#
# Each %Rule list consists of:
# Y0 Y1 YTYPE MON FLAG DOW NUM TIMETYPE TIME OFFSET LETTER
# where
# Y0, Y1 : the year range for which this rule line might apply
# YTYPE : type of year where the rule does apply
# even : only applies to even numbered years
# odd : only applies to odd numbered years
# - : applies to all years in the range
# MON : the month where a change occurs
# FLAG/DOW/NUM : specifies the day a time change occurs (interpreted
# the same way the as in the zone description below)
# TIMETYPE : the type of time that TIME is
# w : wallclock time
# u : univeral time
# s : standard time
# TIME : HH:MM:SS where the time change occurs
# OFFSET : the offset (which is added to standard time offset)
# starting at that time
# LETTER : letters that are substituted for %s in abbreviations
# Parses a day-of-month which can be given as a # (1-31), lastSun, or
# Sun>=13 or Sun<=24 format.
sub _rule_DOM {
my($self,$dom) = @_;
my %days = qw(mon 1 tue 2 wed 3 thu 4 fri 5 sat 6 sun 7);
my($dow,$num,$flag,$err) = (0,0,0,0);
my($i);
if ($dom =~ /^(\d\d?)$/) {
($dow,$num,$flag)=(0,$1,$TZ_DOM);
} elsif ($dom =~ /^last(.+)$/) {
($dow,$num,$flag)=($1,0,$TZ_LAST);
} elsif ($dom =~ /^(.+)>=(\d\d?)$/) {
($dow,$num,$flag)=($1,$2,$TZ_GE);
} elsif ($dom =~ /^(.+)<=(\d\d?)$/) {
($dow,$num,$flag)=($1,$2,$TZ_LE);
} else {
$err = 1;
}
if ($dow) {
if (exists $days{ lc($dow) }) {
$dow = $days{ lc($dow) };
} else {
$err = 1;
}
}
$err = 1 if ($num>31);
return ($dow,$num,$flag,$err);
}
# Parses a month from a string
sub _rule_Month {
my($self,$mmm) = @_;
my %months = qw(jan 1 feb 2 mar 3 apr 4 may 5 jun 6
jul 7 aug 8 sep 9 oct 10 nov 11 dec 12);
if (exists $months{ lc($mmm) }) {
return $months{ lc($mmm) };
} else {
return 0;
}
}
# Returns a time. The time (HH:MM:SS) which may optionally be signed (if $sign
# is 1), and may optionally (if $type is 1) be followed by a type
# ('w', 'u', or 's').
sub _rule_Time {
my($self,$time,$sign,$type) = @_;
my($s,$t);
if ($type) {
$t = 'w';
if ($type && $time =~ s/(w|u|s)$//i) {
$t = lc($1);
}
}
if ($sign) {
if ($time =~ s/^-//) {
$s = "-";
} else {
$s = '';
$time =~ s/^\+//;
}
} else {
$s = '';
}
return '' if ($time !~ /^(\d\d?)(?::(\d\d))?(?::(\d\d))?$/);
my($hr,$mn,$se)=($1,$2,$3);
$hr = '00' if (! $hr);
$mn = '00' if (! $mn);
$se = '00' if (! $se);
$hr = "0$hr" if (length($hr)<2);
$mn = "0$mn" if (length($mn)<2);
$se = "0$se" if (length($se)<2);
$time = "$s$hr:$mn:$se";
if ($type) {
return ($time,$t);
} else {
return $time;
}
}
# a year or 'minimum'
sub _rule_From {
my($self,$rule,$from) = @_;
$from = lc($from);
if ($from =~ /^\d\d\d\d$/) {
return $from;
} elsif ($from eq 'minimum' || $from eq 'min') {
return '0001';
}
warn "ERROR: [rule_from] invalid: $rule: $from\n";
$Error = 1;
return '';
}
# a year, 'maximum', or 'only'
sub _rule_To {
my($self,$rule,$to,$from) = @_;
$to = lc($to);
if ($to =~ /^\d\d\d\d$/) {
return $to;
} elsif ($to eq 'maximum' || $to eq 'max') {
return '9999';
} elsif (lc($to) eq 'only') {
return $from;
}
warn "ERROR: [rule_to] invalid: $rule: $to\n";
$Error = 1;
return '';
}
# "-", 'even', or 'odd'
sub _rule_Type {
my($self,$rule,$type) = @_;
return lc($type) if (lc($type) eq "-" ||
lc($type) eq 'even' ||
lc($type) eq 'odd');
warn "ERROR: [rule_type] invalid: $rule: $type\n";
$Error = 1;
return '';
}
# a month
sub _rule_In {
my($self,$rule,$in) = @_;
my($i) = $self->_rule_Month($in);
if (! $i) {
warn "ERROR: [rule_in] invalid: $rule: $in\n";
$Error = 1;
}
return $i;
}
# DoM (1-31), lastDow (lastSun), DoW<=number (Mon<=12),
# DoW>=number (Sat>=14)
#
# Returns: (flag,dow,num)
sub _rule_On {
my($self,$rule,$on) = @_;
my($dow,$num,$flag,$err) = $self->_rule_DOM($on);
if ($err) {
warn "ERROR: [rule_on] invalid: $rule: $on\n";
$Error = 1;
}
return ($flag,$dow,$num);
}
# a time followed by 'w' (default), 'u', or 's';
sub _rule_At {
my($self,$rule,$at) = @_;
my($ret,$attype) = $self->_rule_Time($at,0,1);
if (! $ret) {
warn "ERROR: [rule_at] invalid: $rule: $at\n";
$Error = 1;
}
return($attype,$ret);
}
# a signed time (or "-" which is equivalent to 0)
sub _rule_Save {
my($self,$rule,$save) = @_;
$save = '00:00:00' if ($save eq "-");
my($ret) = $self->_rule_Time($save,1);
if (! $ret) {
warn "ERROR: [rule_save] invalid: $rule: $save\n";
$Error=1;
}
return $ret;
}
# letters (or "-")
sub _rule_Letters {
my($self,$rule,$letters)=@_;
return '' if ($letters eq "-");
return $letters;
}
########################################################################
# PARSING ZONES
########################################################################
my($TZ_START) = $dmb->join('date',['0001',1,2,0,0,0]);
my($TZ_END) = $dmb->join('date',['9999',12,30,23,59,59]);
sub _tzd_Zone {
my($self,$zone,$listref) = @_;
if (defined $listref) {
if (! exists $$self{'zone'}{$zone}) {
$$self{'zone'}{$zone} = [$zone];
}
push @{ $$self{'zone'}{$zone} }, [ @$listref ];
} elsif (exists $$self{'zone'}{$zone}) {
return @{ $$self{'zone'}{$zone} };
} else {
return ();
}
}
sub _tzd_DeleteZone {
my($self,$zone) = @_;
delete $$self{'zone'}{$zone};
return;
}
sub _tzd_ZoneKeys {
my($self) = @_;
return keys %{ $$self{'zone'} };
}
sub _tzd_CheckZones {
my($self) = @_;
print "... zones\n" if ($Verbose);
foreach my $zone ($self->_tzd_ZoneKeys()) {
my($start) = $TZ_START;
$Error = 0;
my ($name,@zone) = $self->_tzd_Zone($zone);
$self->_tzd_DeleteZone($zone);
while (@zone) {
my($gmt,$rule,$format,@until) = @{ shift(@zone) };
my($ruletype);
$gmt = $self->_zone_GMTOff($zone,$gmt);
($ruletype,$rule) = $self->_zone_Rule ($zone,$rule);
$format = $self->_zone_Format($zone,$format);
my($y,$m,$dow,$num,$flag,$t,$type,$end,$nextstart)
= $self->_zone_Until ($zone,@until);
if (! $Error) {
$self->_tzd_Zone($zone,[ $gmt,$ruletype,$rule,$format,$y,$m,$dow,
$num,$flag,$t,$type,$start,$end ]);
$start = $nextstart;
}
}
$self->_tzd_DeleteZone($zone) if ($Error);
}
return;
}
# TZdata file:
#
# #Zone NAME GMTOFF RULES FORMAT [UNTIL]
# Zone America/New_York -4:56:02 - LMT 1883 Nov 18 12:03:58
# -5:00 US E%sT 1920
# -5:00 NYC E%sT 1942
# -5:00 US E%sT 1946
# -5:00 NYC E%sT 1967
# -5:00 US E%sT
#
# Stored locally as:
#
# %Zone = (
# "America/New_York" =>
# [
# "America/New_York",
# [ -04:56:02 1 - LMT 1883 11 0 18 1 12:03:58 w START END ]
# ,[ -05:00:00 2 US E%sT 1920 01 0 01 1 00:00:00 w START END ]
# ,[ -05:00:00 2 NYC E%sT 1942 01 0 01 1 00:00:00 w START END ]
# ,[ -05:00:00 2 US E%sT 1946 01 0 01 1 00:00:00 w START END ]
# ,[ -05:00:00 2 NYC E%sT 1967 01 0 01 1 00:00:00 w START END ]
# ,[ -05:00:00 2 US E%sT 9999 12 0 31 1 00:00:00 u START END ]
# ]
# )
#
# Each %Zone list consists of:
# GMTOFF RULETYPE RULE ABBREV YEAR MON DOW NUM FLAG TIME TIMETYPE START
# where
# GMTOFF : the standard time offset for the time period starting
# at the end of the previous peried, and ending at the
# time specified by TIME/TIMETYPE
# RULETYPE : what type of value RULE can have
# $TZ_STANDARD the entire period is standard time
# $TZ_RULE the name of a rule to use for this period
# $TZ_OFFSET an additional offset to apply for the
# entire period (which is in saving time)
# RULE : a dash (-), the name of the rule, or an offset
# ABBREV : an abbreviation for the timezone (which may include a %s
# where letters from a rule are substituted)
# YEAR/MON : the year and month where the time period ends
# DOW/NUM/FLAG : the day of the month where the time period ends (see
# note below)
# TIME : HH:MM:SS where the time period ends
# TIMETYPE : how the time is to be interpreted
# u in UTC
# w wallclock time
# s in standard time
# START : This is the wallclock time when this zoneline starts. If the
# wallclock time cannot be decided yet, it is left blank. In
# the case of a non-wallclock time, the change should NOT
# occur on Dec 31 or Jan 1.
# END : The wallclock date/time when the zoneline ends. Blank if
# it cannot be decided.
#
# The time stored in the until fields (which is turned into the
# YEAR/MON/DOW/NUM/FLAG fields) actually refers to the exact second when
# the following zone line takes affect. When a rule specifies a time
# change exactly at that time (unfortunately, this situation DOES occur),
# the change will only apply to the next zone line.
#
# In interpreting DOW, NUM, FLAG, the value of FLAG determines how it is
# done. Values are:
# $TZ_DOM NUM is the day of month (1-31), DOW is ignored
# $TZ_LAST NUM is ignored, DOW is the day of week (1-7); the day
# of month is the last DOW in the month
# $TZ_GE NUM is a cutoff date (1-31), DOW is the day of week; the
# day of month is the first DOW in the month on or after
# the cutoff date
# $TZ_LE Similar to $TZ_GE but the day of month is the last DOW in
# the month on or before the cutoff date
#
# In a time period which uses a named rule, if the named rule doesn't
# cover a year, just use the standard time for that year.
# The total period covered by zones is from Jan 2, 0001 (00:00:00) to
# Dec 30, 9999 (23:59:59). The first and last days are ignored so that
# they can safely be expressed as wallclock time.
# a signed time
sub _zone_GMTOff {
my($self,$zone,$gmt) = @_;
my($ret) = $self->_rule_Time($gmt,1);
if (! $ret) {
warn "ERROR: [zone_gmtoff] invalid: $zone: $gmt\n";
$Error = 1;
}
return $ret;
}
# rule, a signed time, or "-"
sub _zone_Rule {
my($self,$zone,$rule) = @_;
return ($TZ_STANDARD,$rule) if ($rule eq "-");
my($ret) = $self->_rule_Time($rule,1);
return ($TZ_OFFSET,$ret) if ($ret);
if (! $self->_tzd_Rule($rule)) {
warn "ERROR: [zone_rule] rule undefined: $zone: $rule\n";
$Error = 1;
}
return ($TZ_RULE,$rule);
}
# a format
sub _zone_Format {
my($self,$zone,$format)=@_;
return $format;
}
# a date (YYYY MMM DD TIME)
sub _zone_Until {
my($self,$zone,$y,$m,$d,$t) = @_;
my($tmp,$type,$dow,$num,$flag,$err);
if (! $y) {
# Until '' == Until '9999 Dec 31 00:00:00'
$y = 9999;
$m = 12;
$d = 31;
$t = '00:00:00';
} else {
# Until '1975 ...'
if ($y !~ /^\d\d\d\d$/) {
warn "ERROR: [zone_until] invalid year: $zone: $y\n";
$Error = 1;
return ();
}
if (! $m) {
# Until '1920' == Until '1920 Jan 1 00:00:00'
$m = 1;
$d = 1;
$t = '00:00:00';
} else {
# Until '1920 Mar ...'
$tmp = $self->_rule_Month($m);
if (! $tmp) {
warn "ERROR: [zone_until] invalid month: $zone: $m\n";
$Error = 1;
return ();
}
$m = $tmp;
if (! $d) {
# Until '1920 Feb' == Until '1920 Feb 1 00:00:00'
$d = 1;
$t = '00:00:00';
} elsif ($d =~ /^last(.*)/) {
# Until '1920 Feb lastSun ...'
my(@tmp) = $self->_rule_DOM($d);
my($dow) = $tmp[0];
my $ymd = $dmb->nth_day_of_week($y,-1,$dow,$m);
$d = $$ymd[2];
} elsif ($d =~ />=/) {
my(@tmp) = $self->_rule_DOM($d);
my $dow = $tmp[0];
$d = $tmp[1];
my $ddow = $dmb->day_of_week([$y,$m,$d]);
if ($dow > $ddow) {
my $ymd = $dmb->calc_date_days([$y,$m,$d],$dow-$ddow);
$d = $$ymd[2];
} elsif ($dow < $ddow) {
my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($ddow-$dow));
$d = $$ymd[2];
}
} elsif ($d =~ /<=/) {
my(@tmp) = $self->_rule_DOM($d);
my $dow = $tmp[0];
$d = $tmp[1];
my $ddow = $dmb->day_of_week([$y,$m,$d]);
if ($dow < $ddow) {
my $ymd = $dmb->calc_date_days([$y,$m,$d],$ddow-$dow,1);
$d = $$ymd[2];
} elsif ($dow > $ddow) {
my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($dow-$ddow),1);
$d = $$ymd[2];
}
} else {
# Until '1920 Feb 20 ...'
}
if (! $t) {
# Until '1920 Feb 20' == Until '1920 Feb 20 00:00:00'
$t = '00:00:00';
}
}
}
# Make sure that day and month are valid and formatted correctly
($dow,$num,$flag,$err) = $self->_rule_DOM($d);
if ($err) {
warn "ERROR: [zone_until] invalid day: $zone: $d\n";
$Error = 1;
return ();
}
$m = "0$m" if (length($m)<2);
# Get the time type
if ($y == 9999) {
$type = 'w';
} else {
($tmp,$type) = $self->_rule_Time($t,0,1);
if (! $tmp) {
warn "ERROR: [zone_until] invalid time: $zone: $t\n";
$Error = 1;
return ();
}
$t = $tmp;
}
# Get the wallclock end of this zone line (and the start of the
# next one 1 second later) if possible. Since we cannot assume that
# the rules are present yet, we can only do this for wallclock time
# types. 'u' and 's' time types will be done later.
my ($start,$end) = ('','');
if ($type eq 'w') {
# Start of next time is Y-M-D-TIME
$start = $dmb->join('date',[$y,$m,$d,@{ $dmb->split('hms',$t) }]);
# End of this time is Y-M-D-TIME minus 1 second
$end = $dmb->_calc_date_time_strings($start,'0:0:1',1);
}
return ($y,$m,$dow,$num,$flag,$t,$type,$end,$start);
}
###############################################################################
# ROUTINES FOR GETTING INFORMATION OUT OF RULES/ZONES
###############################################################################
sub _tzd_ZoneLines {
my($self,$zone) = @_;
my @z = $self->_tzd_Zone($zone);
shift(@z);
# This will fill in any missing start/end values using the rules
# (which are now all present).
my $i = 0;
my($lastend,$lastdstend) = ('','00:00:00');
foreach my $z (@z) {
my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
$timetype,$start,$end) = @$z;
# Make sure that we have a start wallclock time. We ALWAYS have the
# start time of the first zone line, and we will always have the
# end time of the zoneline before (if this is not the first) since
# we will determine it below.
if (! $start) {
$start = $dmb->_calc_date_time_strings($lastend,'0:0:1',0);
}
# If we haven't got a wallclock end, we can't get it yet... but
# we can get an unadjusted end which we'll use for determining
# what offsets apply from the rules.
my $fixend = 0;
if (! $end) {
$end = $self->_tzd_ParseRuleDate($yr,$mon,$dow,$num,$flag,$time);
$fixend = 1;
}
# Now we need to get the DST offset at the start and end of
# the period.
my($dstbeg,$dstend,$letbeg,$letend);
if ($ruletype == $TZ_RULE) {
$dstbeg = $lastdstend;
# Get the year from the end time for the zone line
# Get the dates for this rule.
# Find the latest one which is less than the end date.
# That's the end DST offset.
my %lett = ();
my $tmp = $dmb->split('date',$end);
my $y = $$tmp[0];
my(@rdate) = $self->_ruleInfo($rule,'rdates',$y);
my $d = $start;
while (@rdate) {
my($date,$off,$type,$lett,@tmp) = @rdate;
$lett{$off} = $lett;
@rdate = @tmp;
next if ($date lt $d || $date gt $end);
$d = $date;
$dstend = $off;
}
# If we didn't find $dstend, it's because the zone line
# ends before any rules can go into affect. If that is
# the case, we'll do one of two things:
#
# If the zone line starts this year, no time changes
# occured, so we set $dstend to the same as $dstbeg.
#
# Otherwise, set it to the last DST offset of the year
# before.
if (! $dstend) {
my($yrbeg) = $dmb->join('date',[$y,1,1,0,0,0]);
if ($start ge $yrbeg) {
$dstend = $dstbeg;
} else {
$dstend = $self->_ruleInfo($rule,'lastoff',$y);
}
}
$letbeg = $lett{$dstbeg};
$letend = $lett{$dstend};
} elsif ($ruletype == $TZ_STANDARD) {
$dstbeg = '00:00:00';
$dstend = $dstbeg;
$letbeg = '';
$letend = '';
} else {
$dstbeg = $rule;
$dstend = $dstbeg;
$letbeg = '';
$letend = '';
}
# Now we calculate the wallclock end time (if we don't already
# have it).
if ($fixend) {
if ($timetype eq 'u') {
# UT time -> STD time
$end = $dmb->_calc_date_time_strings($end,$offset,0);
}
# STD time -> wallclock time
$end = $dmb->_calc_date_time_strings($end,$dstend,1);
}
# Store the information
$i++;
$$self{'zonelines'}{$zone}{$i}{'start'} = $start;
$$self{'zonelines'}{$zone}{$i}{'end'} = $end;
$$self{'zonelines'}{$zone}{$i}{'stdoff'} = $offset;
$$self{'zonelines'}{$zone}{$i}{'dstbeg'} = $dstbeg;
$$self{'zonelines'}{$zone}{$i}{'dstend'} = $dstend;
$$self{'zonelines'}{$zone}{$i}{'letbeg'} = $letbeg;
$$self{'zonelines'}{$zone}{$i}{'letend'} = $letend;
$$self{'zonelines'}{$zone}{$i}{'abbrev'} = $abbrev;
$$self{'zonelines'}{$zone}{$i}{'rule'} = ($ruletype == $TZ_RULE ?
$rule : '');
$lastend = $end;
$lastdstend = $dstend;
}
$$self{'zonelines'}{$zone}{'numlines'} = $i;
return;
}
# Parses date information from a single rule and returns a date.
# The date is not adjusted for standard time or daylight saving time
# offsets.
sub _tzd_ParseRuleDate {
my($self,$year,$mon,$dow,$num,$flag,$time) = @_;
# Calculate the day-of-month
my($dom);
if ($flag==$TZ_DOM) {
$dom = $num;
} elsif ($flag==$TZ_LAST) {
($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
} elsif ($flag==$TZ_GE) {
($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,1,$dow,$mon) };
while ($dom<$num) {
$dom += 7;
}
} elsif ($flag==$TZ_LE) {
($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
while ($dom>$num) {
$dom -= 7;
}
}
# Split the time and then form the date
my($h,$mn,$s) = split(/:/,$time);
return $dmb->join('date',[$year,$mon,$dom,$h,$mn,$s]);
}
1;
# Local Variables:
# mode: cperl
# indent-tabs-mode: nil
# cperl-indent-level: 3
# cperl-continued-statement-offset: 2
# cperl-continued-brace-offset: 0
# cperl-brace-offset: 0
# cperl-brace-imaginary-offset: 0
# cperl-label-offset: 0
# End: