File Coverage

lib/Date/Manip/TZdata.pm
Criterion Covered Total %
statement 29 550 5.2
branch 0 248 0.0
condition 0 81 0.0
subroutine 10 48 20.8
pod 1 1 100.0
total 40 928 4.3


line stmt bran cond sub pod time code
1             package Date::Manip::TZdata;
2             # Copyright (c) 2008-2026 Sullivan Beck. All rights reserved.
3             # This program is free software; you can redistribute it and/or modify it
4             # under the same terms as Perl itself.
5              
6             ###############################################################################
7             require 5.010000;
8 2     2   1277 use IO::File;
  2         3  
  2         304  
9 2     2   30 use Date::Manip::Base;
  2         3  
  2         43  
10 2     2   6 use Carp;
  2         6  
  2         118  
11              
12 2     2   9 use strict;
  2         2  
  2         33  
13 2     2   7 use integer;
  2         2  
  2         12  
14 2     2   35 use warnings;
  2         2  
  2         11840  
15              
16             our $VERSION;
17             $VERSION='6.99';
18 2     2   10 END { undef $VERSION; }
19              
20             ###############################################################################
21             # GLOBAL VARIABLES
22             ###############################################################################
23              
24             our ($Verbose,@StdFiles,$dmb);
25             END {
26 2     2   5 undef $Verbose;
27 2         9 undef @StdFiles;
28 2         244 undef $dmb;
29             }
30             $dmb = new Date::Manip::Base;
31              
32             # Whether to print some debugging stuff.
33              
34             $Verbose = 0;
35              
36             # Standard tzdata files that need to be parsed.
37              
38             @StdFiles = qw(africa
39             antarctica
40             asia
41             australasia
42             europe
43             northamerica
44             southamerica
45             etcetera
46             backward
47             );
48              
49             our ($TZ_DOM,$TZ_LAST,$TZ_GE,$TZ_LE);
50             END {
51 2     2   3 undef $TZ_DOM;
52 2         3 undef $TZ_LAST;
53 2         3 undef $TZ_GE;
54 2         5 undef $TZ_LE;
55             }
56              
57             $TZ_DOM = 1;
58             $TZ_LAST = 2;
59             $TZ_GE = 3;
60             $TZ_LE = 4;
61              
62             our ($TZ_STANDARD,$TZ_RULE,$TZ_OFFSET);
63             END {
64 2     2   33051 undef $TZ_STANDARD;
65 2         3 undef $TZ_RULE;
66 2         6 undef $TZ_OFFSET;
67             }
68             $TZ_STANDARD = 1;
69             $TZ_RULE = 2;
70             $TZ_OFFSET = 3;
71              
72             ###############################################################################
73             # BASE METHODS
74             ###############################################################################
75             #
76             # The Date::Manip::TZdata object is a hash of the form:
77             #
78             # { dir => DIR where to find the tzdata directory
79             # zone => { ZONE => [ ZONEDESC ] }
80             # ruleinfo => { INFO => [ VAL ... ] }
81             # zoneinfo => { INFO => [ VAL ... ] }
82             # zonelines => { ZONE => [ VAL ... ] }
83             # }
84              
85             sub new {
86 0     0 1   my($class,$dir) = @_;
87              
88 0 0         $dir = '.' if (! $dir);
89              
90 0 0         if (! -d "$dir/tzdata") {
91 0           croak "ERROR: no tzdata directory found\n";
92             }
93              
94 0           my $self = {
95             'dir' => $dir,
96             'zone' => {},
97             'ruleinfo' => {},
98             'zoneinfo' => {},
99             'zonelines' => {},
100             };
101 0           bless $self, $class;
102              
103 0           $self->_tzd_ParseFiles();
104              
105 0           return $self;
106             }
107              
108             ###############################################################################
109             # RULEINFO
110             ###############################################################################
111              
112             my($Error);
113              
114             # @info = $tzd->ruleinfo($rule,@args);
115             #
116             # This takes the name of a set of rules (e.g. NYC or US as defined in
117             # the zoneinfo database) and returns information based on the arguments
118             # given.
119             #
120             # @args
121             # ------------
122             #
123             # rules YEAR : Return a list of all rules used during that year
124             # stdlett YEAR : The letter(s) used during standard time that year
125             # savlett YEAR : The letter(s) used during saving time that year
126             # lastoff YEAR : Returns the last DST offset of the year
127             # rdates YEAR : Returns a list of critical dates for the given
128             # rule during a year. It returns:
129             # (date dst_offset timetype lett ...)
130             # where dst_offset is the daylight saving time offset
131             # that starts at that date and timetype is 'u', 'w', or
132             # 's', and lett is the letter to use in the abbrev.
133             #
134             sub _ruleInfo {
135 0     0     my($self,$rule,$info,@args) = @_;
136 0           my $year = shift(@args);
137              
138 0 0 0       if (exists $$self{'ruleinfo'}{$info} &&
      0        
139             exists $$self{'ruleinfo'}{$info}{$rule} &&
140             exists $$self{'ruleinfo'}{$info}{$rule}{$year}) {
141 0 0         if (ref $$self{'ruleinfo'}{$info}{$rule}{$year}) {
142 0           return @{ $$self{'ruleinfo'}{$info}{$rule}{$year} };
  0            
143             } else {
144 0           return $$self{'ruleinfo'}{$info}{$rule}{$year};
145             }
146             }
147              
148 0 0 0       if ($info eq 'rules') {
    0          
    0          
    0          
149 0           my @ret;
150 0           foreach my $r ($self->_tzd_Rule($rule)) {
151 0           my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
152             $lett) = @$r;
153 0 0 0       next if ($y0>$year || $y1<$year);
154 0 0 0       push(@ret,$r) if ($ytype eq "-" ||
      0        
      0        
      0        
      0        
155             $year == 9999 ||
156             ($ytype eq 'even' && $year =~ /[02468]$/) ||
157             ($ytype eq 'odd' && $year =~ /[13579]$/));
158             }
159              
160             # We'll sort them... if there are ever two time changes in a
161             # single month, this will cause problems... hopefully there
162             # never will be.
163              
164 0           @ret = sort { $$a[3] <=> $$b[3] } @ret;
  0            
165 0           $$self{'ruleinfo'}{$info}{$rule}{$year} = [ @ret ];
166 0           return @ret;
167              
168             } elsif ($info eq 'stdlett' ||
169             $info eq 'savlett') {
170 0           my @rules = $self->_ruleInfo($rule,'rules',$year);
171 0           my %lett = ();
172 0           foreach my $r (@rules) {
173 0           my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
174             $lett) = @$r;
175 0 0 0       $lett{$lett} = 1
      0        
      0        
176             if ( ($info eq 'stdlett' && $offset eq '00:00:00') ||
177             ($info eq 'savlett' && $offset ne '00:00:00') );
178             }
179              
180 0           my $ret;
181 0 0         if (! %lett) {
182 0           $ret = '';
183             } else {
184 0           $ret = join(",",sort keys %lett);
185             }
186 0           $$self{'ruleinfo'}{$info}{$rule}{$year} = $ret;
187 0           return $ret;
188              
189             } elsif ($info eq 'lastoff') {
190 0           my $ret;
191 0           my @rules = $self->_ruleInfo($rule,'rules',$year);
192 0 0         return '00:00:00' if (! @rules);
193 0           my $r = pop(@rules);
194 0           my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
195             $lett) = @$r;
196              
197 0           $$self{'ruleinfo'}{$info}{$rule}{$year} = $offset;
198 0           return $offset;
199              
200             } elsif ($info eq 'rdates') {
201 0           my @ret;
202 0           my @rules = $self->_ruleInfo($rule,'rules',$year);
203 0           foreach my $r (@rules) {
204 0           my($y0,$y1,$ytype,$mon,$flag,$dow,$num,$timetype,$time,$offset,
205             $lett) = @$r;
206 0           my($date) = $self->_tzd_ParseRuleDate($year,$mon,$dow,$num,$flag,$time);
207 0           push(@ret,$date,$offset,$timetype,$lett);
208             }
209              
210 0           $$self{'ruleinfo'}{$info}{$rule}{$year} = [ @ret ];
211 0           return @ret;
212             }
213             }
214              
215             ###############################################################################
216             # ZONEINFO
217             ###############################################################################
218              
219             # zonelines is:
220             # ( ZONE => numlines => N,
221             # I => { start => DATE,
222             # end => DATE,
223             # stdoff => OFFSET,
224             # dstbeg => OFFSET,
225             # dstend => OFFSET,
226             # letbeg => LETTER,
227             # letend => LETTER,
228             # abbrev => ABBREV,
229             # rule => RULE
230             # }
231             # )
232             # where I = 1..N
233             # start, end the wallclock start/end time of this period
234             # stdoff the standard GMT offset during this period
235             # dstbeg the DST offset at the start of this period
236             # dstend the DST offset at the end of this period
237             # letbeg the letter (if any) used at the start of this period
238             # letend the letter (if any) used at the end of this period
239             # abbrev the zone abbreviation during this period
240             # rule the rule that applies (if any) during this period
241              
242             # @info = $tzd->zoneinfo($zone,@args);
243             #
244             # Obtain information from a zone
245             #
246             # @args
247             # ------------
248             #
249             # zonelines Y : Return the full zone line(s) which apply for
250             # a given year.
251             # rules YEAR : Returns a list of rule names and types which
252             # apply for the given year.
253             #
254             sub _zoneInfo {
255 0     0     my($self,$zone,$info,@args) = @_;
256              
257 0 0         if (! exists $$self{'zonelines'}{$zone}) {
258 0           $self->_tzd_ZoneLines($zone);
259             }
260              
261 0           my @z = $self->_tzd_Zone($zone);
262 0           shift(@z); # Get rid of timezone name
263              
264 0           my $ret;
265              
266             # if ($info eq 'numzonelines') {
267             # return $$self{'zonelines'}{$zone}{'numlines'};
268              
269             # } elsif ($info eq 'zoneline') {
270             # my ($i) = @args;
271             # my @ret = map { $$self{'zonelines'}{$zone}{$i}{$_} }
272             # qw(start end stdoff dstbeg dstend letbeg letend abbrev rule);
273              
274             # return @ret;
275             # }
276              
277 0           my $y = shift(@args);
278 0 0 0       if (exists $$self{'zoneinfo'}{$info} &&
      0        
279             exists $$self{'zoneinfo'}{$info}{$zone} &&
280             exists $$self{'zoneinfo'}{$info}{$zone}{$y}) {
281 0 0         if (ref($$self{'zoneinfo'}{$info}{$zone}{$y})) {
282 0           return @{ $$self{'zoneinfo'}{$info}{$zone}{$y} };
  0            
283             } else {
284 0           return $$self{'zoneinfo'}{$info}{$zone}{$y};
285             }
286             }
287              
288 0 0         if ($info eq 'zonelines') {
    0          
289 0           my (@ret);
290 0           while (@z) {
291             # y = 1920
292             # until = 1919 NO
293             # until = 1920 NO
294             # until = 1920 Feb... YES
295             # until = 1921... YES, last
296 0           my $z = shift(@z);
297 0           my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
298             $timetype,$start,$end) = @$z;
299 0 0         next if ($yr < $y);
300 0 0 0       next if ($yr == $y && $flag == $TZ_DOM &&
      0        
      0        
      0        
301             $mon == 1 && $num == 1 && $time eq '00:00:00');
302 0           push(@ret,$z);
303 0 0         last if ($yr > $y);
304             }
305              
306 0           $$self{'zoneinfo'}{$info}{$zone}{$y} = [ @ret ];
307 0           return @ret;
308              
309             } elsif ($info eq 'rules') {
310 0           my (@ret);
311 0           @z = $self->_zoneInfo($zone,'zonelines',$y);
312 0           foreach my $z (@z) {
313 0           my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
314             $timetype,$start,$end) = @$z;
315 0           push(@ret,$rule,$ruletype);
316             }
317              
318 0           $$self{'zoneinfo'}{$info}{$zone}{$y} = [ @ret ];
319 0           return @ret;
320             }
321             }
322              
323             ########################################################################
324             # PARSING TZDATA FILES
325             ########################################################################
326              
327             # These routine parses the raw tzdata file. Files contain three types
328             # of lines:
329             #
330             # Link CANONICAL ALIAS
331             # Rule NAME FROM TO TYPE IN ON AT SAVE LETTERS
332             # Zone NAME GMTOFF RULE FORMAT UNTIL
333             # GMTOFF RULE FORMAT UNTIL
334             # ...
335             # GMTOFF RULE FORMAT
336              
337             # Parse all files
338             sub _tzd_ParseFiles {
339 0     0     my($self) = @_;
340              
341 0 0         print "PARSING FILES...\n" if ($Verbose);
342              
343 0           foreach my $file (@StdFiles) {
344 0           $self->_tzd_ParseFile($file);
345             }
346              
347 0           $self->_tzd_CheckData();
348             }
349              
350             # Parse a file
351             sub _tzd_ParseFile {
352 0     0     my($self,$file) = @_;
353 0           my $in = new IO::File;
354 0           my $dir = $$self{'dir'};
355 0 0         print "... $file\n" if ($Verbose);
356 0 0         if (! $in->open("$dir/tzdata/$file")) {
357 0           carp "WARNING: [parse_file] unable to open file: $file: $!";
358 0           return;
359             }
360 0           my @in = <$in>;
361 0           $in->close;
362 0           chomp(@in);
363              
364             # strip out comments
365 0           foreach my $line (@in) {
366 0           $line =~ s/^\s+//;
367 0           $line =~ s/#.*$//;
368 0           $line =~ s/\s+$//;
369             }
370              
371             # parse all lines
372 0           my $n = 0; # line number
373 0           my $zone = ''; # current zone (if in a multi-line zone section)
374              
375 0           while (@in) {
376 0 0         if (! $in[0]) {
    0          
    0          
    0          
377 0           $n++;
378 0           shift(@in);
379              
380             } elsif ($in[0] =~ /^Zone/) {
381 0           $self->_tzd_ParseZone($file,\$n,\@in);
382              
383             } elsif ($in[0] =~ /^Link/) {
384 0           $self->_tzd_ParseLink($file,\$n,\@in);
385              
386             } elsif ($in[0] =~ /^Rule/) {
387 0           $self->_tzd_ParseRule($file,\$n,\@in);
388              
389             } else {
390 0           $n++;
391 0           my $line = shift(@in);
392 0           carp "WARNING: [parse_file] unknown line: $n\n" .
393             " $line\n";
394             }
395             }
396             }
397              
398             sub _tzd_ParseLink {
399 0     0     my($self,$file,$n,$lines) = @_;
400              
401 0           $$n++;
402 0           my $line = shift(@$lines);
403              
404 0           my(@tmp) = split(/\s+/,$line);
405 0 0 0       if ($#tmp != 2 || lc($tmp[0]) ne 'link') {
406 0           carp "ERROR: [parse_file] invalid Link line: $file: $$n\n" .
407             " $line\n";
408 0           return;
409             }
410              
411 0           my($tmp,$zone,$alias) = @tmp;
412              
413 0 0         if ($self->_tzd_Alias($alias)) {
414 0           carp "WARNING: [parse_file] alias redefined: $file: $$n: $alias";
415             }
416              
417 0           $self->_tzd_Alias($alias,$zone);
418             }
419              
420             sub _tzd_ParseRule {
421 0     0     my($self,$file,$n,$lines) = @_;
422              
423 0           $$n++;
424 0           my $line = shift(@$lines);
425              
426 0           my(@tmp) = split(/\s+/,$line);
427 0 0 0       if ($#tmp != 9 || lc($tmp[0]) ne 'rule') {
428 0           carp "ERROR: [parse_file] invalid Rule line: $file: $$n:\n" .
429             " $line\n";
430 0           return;
431             }
432              
433 0           my($tmp,$name,$from,$to,$type,$in,$on,$at,$save,$letters) = @tmp;
434              
435 0           $self->_tzd_Rule($name,[ $from,$to,$type,$in,$on,$at,$save,$letters ]);
436             }
437              
438             sub _tzd_ParseZone {
439 0     0     my($self,$file,$n,$lines) = @_;
440              
441             # Remove "Zone America/New_York" from the first line
442              
443 0           $$n++;
444 0           my $line = shift(@$lines);
445 0           my @tmp = split(/\s+/,$line);
446              
447 0 0 0       if ($#tmp < 4 || lc($tmp[0]) ne 'zone') {
448 0           carp "ERROR: [parse_file] invalid Zone line: $file :$$n\n" .
449             " $line\n";
450 0           return;
451             }
452              
453 0           shift(@tmp);
454 0           my $zone = shift(@tmp);
455              
456 0           $line = join(' ',@tmp);
457 0           unshift(@$lines,$line);
458              
459             # Store the zone name information
460              
461 0 0         if ($self->_tzd_Zone($zone)) {
462 0           carp "ERROR: [parse_file] zone redefined: $file: $$n: $zone";
463 0           $self->_tzd_DeleteZone($zone);
464             }
465 0           $self->_tzd_Alias($zone,$zone);
466              
467             # Parse all zone lines
468              
469 0           while (1) {
470 0 0         last if (! @$lines);
471              
472 0           $line = $$lines[0];
473 0 0         return if ($line =~ /^(zone|link|rule)/i);
474              
475 0           $$n++;
476 0           shift(@$lines);
477 0 0         next if (! $line);
478              
479 0           @tmp = split(/\s+/,$line);
480              
481 0 0         if ($#tmp < 2) {
482 0           carp "ERROR: [parse_file] invalid Zone line: $file: $$n\n" .
483             " $line\n";
484 0           return;
485             }
486              
487 0           my($gmt,$rule,$format,@until) = @tmp;
488 0           $self->_tzd_Zone($zone,[ $gmt,$rule,$format,@until ]);
489             }
490             }
491              
492             sub _tzd_CheckData {
493 0     0     my($self) = @_;
494 0 0         print "CHECKING DATA...\n" if ($Verbose);
495 0           $self->_tzd_CheckRules();
496 0           $self->_tzd_CheckZones();
497 0           $self->_tzd_CheckAliases();
498             }
499              
500             ########################################################################
501             # LINKS (ALIASES)
502             ########################################################################
503              
504             sub _tzd_Alias {
505 0     0     my($self,$alias,$zone) = @_;
506              
507 0 0         if (defined $zone) {
    0          
508 0           $$self{'alias'}{$alias} = $zone;
509 0           return;
510              
511             } elsif (exists $$self{'alias'}{$alias}) {
512 0           return $$self{'alias'}{$alias};
513              
514             } else {
515 0           return '';
516             }
517             }
518              
519             sub _tzd_DeleteAlias {
520 0     0     my($self,$alias) = @_;
521 0           delete $$self{'alias'}{$alias};
522             }
523              
524             sub _tzd_AliasKeys {
525 0     0     my($self) = @_;
526 0           return keys %{ $$self{'alias'} };
  0            
527             }
528              
529             # TZdata file:
530             #
531             # Link America/Denver America/Shiprock
532             #
533             # Stored locally as:
534             #
535             # (
536             # "us/eastern" => "America/New_York"
537             # "america/new_york" => "America/New_York"
538             # )
539              
540             sub _tzd_CheckAliases {
541 0     0     my($self) = @_;
542              
543             # Replace
544             # ALIAS1 -> ALIAS2 -> ... -> ZONE
545             # with
546             # ALIAS1 -> ZONE
547              
548 0 0         print "... aliases\n" if ($Verbose);
549              
550             ALIAS:
551 0           foreach my $alias ($self->_tzd_AliasKeys()) {
552 0           my $zone = $self->_tzd_Alias($alias);
553              
554 0           my %tmp;
555 0           $tmp{$alias} = 1;
556 0           while (1) {
557              
558 0 0         if ($self->_tzd_Zone($zone)) {
    0          
    0          
559 0           $self->_tzd_Alias($alias,$zone);
560 0           next ALIAS;
561              
562             } elsif (exists $tmp{$zone}) {
563 0           carp "ERROR: [check_aliases] circular alias definition: $alias";
564 0           $self->_tzd_DeleteAlias($alias);
565 0           next ALIAS;
566              
567             } elsif ($self->_tzd_Alias($zone)) {
568 0           $tmp{$zone} = 1;
569 0           $zone = $self->_tzd_Alias($zone);
570 0           next;
571             }
572              
573 0           carp "ERROR: [check_aliases] unresolved alias definition: $alias";
574 0           $self->_tzd_DeleteAlias($alias);
575 0           next ALIAS;
576             }
577             }
578             }
579              
580             ########################################################################
581             # PARSING RULES
582             ########################################################################
583              
584             sub _tzd_Rule {
585 0     0     my($self,$rule,$listref) = @_;
586              
587 0 0         if (defined $listref) {
    0          
588 0 0         if (! exists $$self{'rule'}{$rule}) {
589 0           $$self{'rule'}{$rule} = [];
590             }
591 0           push @{ $$self{'rule'}{$rule} }, [ @$listref ];
  0            
592              
593             } elsif (exists $$self{'rule'}{$rule}) {
594 0           return @{ $$self{'rule'}{$rule} };
  0            
595              
596             } else {
597 0           return ();
598             }
599             }
600              
601             sub _tzd_DeleteRule {
602 0     0     my($self,$rule) = @_;
603 0           delete $$self{'rule'}{$rule};
604             }
605              
606             sub _tzd_RuleNames {
607 0     0     my($self) = @_;
608 0           return keys %{ $$self{'rule'} };
  0            
609             }
610              
611             sub _tzd_CheckRules {
612 0     0     my($self) = @_;
613 0 0         print "... rules\n" if ($Verbose);
614 0           foreach my $rule ($self->_tzd_RuleNames()) {
615 0           $Error = 0;
616 0           my @rule = $self->_tzd_Rule($rule);
617 0           $self->_tzd_DeleteRule($rule);
618 0           while (@rule) {
619             my($from,$to,$type,$in,$on,$at,$save,$letters) =
620 0           @{ shift(@rule) };
  0            
621 0           my($dow,$num,$attype);
622 0           $from = $self->_rule_From ($rule,$from);
623 0           $to = $self->_rule_To ($rule,$to,$from);
624 0           $type = $self->_rule_Type ($rule,$type);
625 0           $in = $self->_rule_In ($rule,$in);
626 0           ($on,$dow,$num) = $self->_rule_On ($rule,$on);
627 0           ($attype,$at) = $self->_rule_At ($rule,$at);
628 0           $save = $self->_rule_Save ($rule,$save);
629 0           $letters = $self->_rule_Letters($rule,$letters);
630              
631 0 0         if (! $Error) {
632 0           $self->_tzd_Rule($rule,[ $from,$to,$type,$in,$on,$dow,$num,$attype,
633             $at,$save,$letters ]);
634             }
635             }
636 0 0         $self->_tzd_DeleteRule($rule) if ($Error);
637             }
638             }
639              
640             # TZdata file:
641             #
642             # #Rule NAME FROM TO TYPE IN ON AT SAVE LETTER
643             # Rule NYC 1920 only - Mar lastSun 2:00 1:00 D
644             # Rule NYC 1920 only - Oct lastSun 2:00 0 S
645             # Rule NYC 1921 1966 - Apr lastSun 2:00 1:00 D
646             # Rule NYC 1921 1954 - Sep lastSun 2:00 0 S
647             # Rule NYC 1955 1966 - Oct lastSun 2:00 0 S
648             #
649             # Stored locally as:
650             #
651             # %Rule = (
652             # 'NYC' =>
653             # [
654             # [ 1920 1920 - 3 2 7 0 w 02:00:00 01:00:00 D ],
655             # [ 1920 1920 - 10 2 7 0 w 02:00:00 00:00:00 S ],
656             # [ 1921 1966 - 4 2 7 0 w 02:00:00 01:00:00 D ],
657             # [ 1921 1954 - 9 2 7 0 w 02:00:00 00:00:00 S ],
658             # [ 1955 1966 - 10 2 7 0 w 02:00:00 00:00:00 S ],
659             # ],
660             # 'US' =>
661             # [
662             # [ 1918 1919 - 3 2 7 0 w 02:00:00 01:00:00 W ],
663             # [ 1918 1919 - 10 2 7 0 w 02:00:00 00:00:00 S ],
664             # [ 1942 1942 - 2 1 0 9 w 02:00:00 01:00:00 W ],
665             # [ 1945 1945 - 9 1 0 30 w 02:00:00 00:00:00 S ],
666             # [ 1967 9999 - 10 2 7 0 u 02:00:00 00:00:00 S ],
667             # [ 1967 1973 - 4 2 7 0 w 02:00:00 01:00:00 D ],
668             # [ 1974 1974 - 1 1 0 6 w 02:00:00 01:00:00 D ],
669             # [ 1975 1975 - 2 1 0 23 w 02:00:00 01:00:00 D ],
670             # [ 1976 1986 - 4 2 7 0 w 02:00:00 01:00:00 D ],
671             # [ 1987 9999 - 4 3 7 1 u 02:00:00 01:00:00 D ],
672             # ]
673             # )
674             #
675             # Each %Rule list consists of:
676             # Y0 Y1 YTYPE MON FLAG DOW NUM TIMETYPE TIME OFFSET LETTER
677             # where
678             # Y0, Y1 : the year range for which this rule line might apply
679             # YTYPE : type of year where the rule does apply
680             # even : only applies to even numbered years
681             # odd : only applies to odd numbered years
682             # - : applies to all years in the range
683             # MON : the month where a change occurs
684             # FLAG/DOW/NUM : specifies the day a time change occurs (interpreted
685             # the same way the as in the zone description below)
686             # TIMETYPE : the type of time that TIME is
687             # w : wallclock time
688             # u : univeral time
689             # s : standard time
690             # TIME : HH:MM:SS where the time change occurs
691             # OFFSET : the offset (which is added to standard time offset)
692             # starting at that time
693             # LETTER : letters that are substituted for %s in abbreviations
694              
695             # Parses a day-of-month which can be given as a # (1-31), lastSun, or
696             # Sun>=13 or Sun<=24 format.
697             sub _rule_DOM {
698 0     0     my($self,$dom) = @_;
699              
700 0           my %days = qw(mon 1 tue 2 wed 3 thu 4 fri 5 sat 6 sun 7);
701              
702 0           my($dow,$num,$flag,$err) = (0,0,0,0);
703 0           my($i);
704              
705 0 0         if ($dom =~ /^(\d\d?)$/) {
    0          
    0          
    0          
706 0           ($dow,$num,$flag)=(0,$1,$TZ_DOM);
707              
708             } elsif ($dom =~ /^last(.+)$/) {
709 0           ($dow,$num,$flag)=($1,0,$TZ_LAST);
710              
711             } elsif ($dom =~ /^(.+)>=(\d\d?)$/) {
712 0           ($dow,$num,$flag)=($1,$2,$TZ_GE);
713              
714             } elsif ($dom =~ /^(.+)<=(\d\d?)$/) {
715 0           ($dow,$num,$flag)=($1,$2,$TZ_LE);
716              
717             } else {
718 0           $err = 1;
719             }
720              
721 0 0         if ($dow) {
722 0 0         if (exists $days{ lc($dow) }) {
723 0           $dow = $days{ lc($dow) };
724             } else {
725 0           $err = 1;
726             }
727             }
728              
729 0 0         $err = 1 if ($num>31);
730 0           return ($dow,$num,$flag,$err);
731             }
732              
733             # Parses a month from a string
734             sub _rule_Month {
735 0     0     my($self,$mmm) = @_;
736              
737 0           my %months = qw(jan 1 feb 2 mar 3 apr 4 may 5 jun 6
738             jul 7 aug 8 sep 9 oct 10 nov 11 dec 12
739             january 1 february 2 march 3 april 4 june 6
740             july 7 august 8 september 9 october 10 november 11 december 12
741             );
742              
743 0 0         if (exists $months{ lc($mmm) }) {
744 0           return $months{ lc($mmm) };
745             } else {
746 0           return 0;
747             }
748             }
749              
750             # Returns a time. The time (HH:MM:SS) which may optionally be signed (if $sign
751             # is 1), and may optionally (if $type is 1) be followed by a type
752             # ('w', 'u', or 's').
753             sub _rule_Time {
754 0     0     my($self,$time,$sign,$type) = @_;
755 0           my($s,$t);
756              
757 0 0         if ($type) {
758 0           $t = 'w';
759 0 0 0       if ($type && $time =~ s/(w|u|s)$//i) {
760 0           $t = lc($1);
761             }
762             }
763              
764 0 0         if ($sign) {
765 0 0         if ($time =~ s/^-//) {
766 0           $s = "-";
767             } else {
768 0           $s = '';
769 0           $time =~ s/^\+//;
770             }
771             } else {
772 0           $s = '';
773             }
774              
775 0 0         return '' if ($time !~ /^(\d\d?)(?::(\d\d))?(?::(\d\d))?$/);
776 0           my($hr,$mn,$se)=($1,$2,$3);
777 0 0         $hr = '00' if (! $hr);
778 0 0         $mn = '00' if (! $mn);
779 0 0         $se = '00' if (! $se);
780 0 0         $hr = "0$hr" if (length($hr)<2);
781 0 0         $mn = "0$mn" if (length($mn)<2);
782 0 0         $se = "0$se" if (length($se)<2);
783 0           $time = "$s$hr:$mn:$se";
784 0 0         if ($type) {
785 0           return ($time,$t);
786             } else {
787 0           return $time;
788             }
789             }
790              
791             # a year or 'minimum'
792             sub _rule_From {
793 0     0     my($self,$rule,$from) = @_;
794 0           $from = lc($from);
795 0 0 0       if ($from =~ /^\d\d\d\d$/) {
    0          
796 0           return $from;
797             } elsif ($from eq 'minimum' || $from eq 'min') {
798 0           return '0001';
799             }
800 0           carp "ERROR: [rule_from] invalid: $rule: $from";
801 0           $Error = 1;
802 0           return '';
803             }
804              
805             # a year, 'maximum', or 'only'
806             sub _rule_To {
807 0     0     my($self,$rule,$to,$from) = @_;
808 0           $to = lc($to);
809 0 0 0       if ($to =~ /^\d\d\d\d$/) {
    0          
    0          
810 0           return $to;
811             } elsif ($to eq 'maximum' || $to eq 'max') {
812 0           return '9999';
813             } elsif (lc($to) eq 'only') {
814 0           return $from;
815             }
816 0           carp "ERROR: [rule_to] invalid: $rule: $to";
817 0           $Error = 1;
818 0           return '';
819             }
820              
821             # "-", 'even', or 'odd'
822             sub _rule_Type {
823 0     0     my($self,$rule,$type) = @_;
824 0 0 0       return lc($type) if (lc($type) eq "-" ||
      0        
825             lc($type) eq 'even' ||
826             lc($type) eq 'odd');
827 0           carp "ERROR: [rule_type] invalid: $rule: $type";
828 0           $Error = 1;
829 0           return '';
830             }
831              
832             # a month
833             sub _rule_In {
834 0     0     my($self,$rule,$in) = @_;
835 0           my($i) = $self->_rule_Month($in);
836 0 0         if (! $i) {
837 0           carp "ERROR: [rule_in] invalid: $rule: $in";
838 0           $Error = 1;
839             }
840 0           return $i;
841             }
842              
843             # DoM (1-31), lastDow (lastSun), DoW<=number (Mon<=12),
844             # DoW>=number (Sat>=14)
845             #
846             # Returns: (flag,dow,num)
847             sub _rule_On {
848 0     0     my($self,$rule,$on) = @_;
849 0           my($dow,$num,$flag,$err) = $self->_rule_DOM($on);
850              
851 0 0         if ($err) {
852 0           carp "ERROR: [rule_on] invalid: $rule: $on";
853 0           $Error = 1;
854             }
855              
856 0           return ($flag,$dow,$num);
857             }
858              
859             # a time followed by 'w' (default), 'u', or 's';
860             sub _rule_At {
861 0     0     my($self,$rule,$at) = @_;
862 0           my($ret,$attype) = $self->_rule_Time($at,0,1);
863 0 0         if (! $ret) {
864 0           carp "ERROR: [rule_at] invalid: $rule: $at";
865 0           $Error = 1;
866             }
867 0           return($attype,$ret);
868             }
869              
870             # a signed time (or "-" which is equivalent to 0)
871             sub _rule_Save {
872 0     0     my($self,$rule,$save) = @_;
873 0 0         $save = '00:00:00' if ($save eq "-");
874 0           my($ret) = $self->_rule_Time($save,1);
875 0 0         if (! $ret) {
876 0           carp "ERROR: [rule_save] invalid: $rule: $save";
877 0           $Error=1;
878             }
879 0           return $ret;
880             }
881              
882             # letters (or "-")
883             sub _rule_Letters {
884 0     0     my($self,$rule,$letters)=@_;
885 0 0         return '' if ($letters eq "-");
886 0           return $letters;
887             }
888              
889             ########################################################################
890             # PARSING ZONES
891             ########################################################################
892              
893             my($TZ_START) = $dmb->join('date',['0001',1,2,0,0,0]);
894             my($TZ_END) = $dmb->join('date',['9999',12,30,23,59,59]);
895              
896             sub _tzd_Zone {
897 0     0     my($self,$zone,$listref) = @_;
898              
899 0 0         if (defined $listref) {
    0          
900 0 0         if (! exists $$self{'zone'}{$zone}) {
901 0           $$self{'zone'}{$zone} = [$zone];
902             }
903 0           push @{ $$self{'zone'}{$zone} }, [ @$listref ];
  0            
904              
905             } elsif (exists $$self{'zone'}{$zone}) {
906 0           return @{ $$self{'zone'}{$zone} };
  0            
907              
908             } else {
909 0           return ();
910             }
911             }
912              
913             sub _tzd_DeleteZone {
914 0     0     my($self,$zone) = @_;
915 0           delete $$self{'zone'}{$zone};
916              
917 0           return;
918             }
919              
920             sub _tzd_ZoneKeys {
921 0     0     my($self) = @_;
922 0           return keys %{ $$self{'zone'} };
  0            
923             }
924              
925             sub _tzd_CheckZones {
926 0     0     my($self) = @_;
927 0 0         print "... zones\n" if ($Verbose);
928 0           foreach my $zone ($self->_tzd_ZoneKeys()) {
929 0           my($start) = $TZ_START;
930 0           $Error = 0;
931 0           my ($name,@zone) = $self->_tzd_Zone($zone);
932 0           $self->_tzd_DeleteZone($zone);
933 0           while (@zone) {
934 0           my($gmt,$rule,$format,@until) = @{ shift(@zone) };
  0            
935 0           my($ruletype);
936 0           $gmt = $self->_zone_GMTOff($zone,$gmt);
937 0           ($ruletype,$rule) = $self->_zone_Rule ($zone,$rule);
938 0           $format = $self->_zone_Format($zone,$format);
939 0           my($y,$m,$dow,$num,$flag,$t,$type,$end,$nextstart)
940             = $self->_zone_Until ($zone,@until);
941              
942 0 0         if (! $Error) {
943 0           $self->_tzd_Zone($zone,[ $gmt,$ruletype,$rule,$format,$y,$m,$dow,
944             $num,$flag,$t,$type,$start,$end ]);
945 0           $start = $nextstart;
946             }
947             }
948 0 0         $self->_tzd_DeleteZone($zone) if ($Error);
949             }
950              
951 0           return;
952             }
953              
954             # TZdata file:
955             #
956             # #Zone NAME GMTOFF RULES FORMAT [UNTIL]
957             # Zone America/New_York -4:56:02 - LMT 1883 Nov 18 12:03:58
958             # -5:00 US E%sT 1920
959             # -5:00 NYC E%sT 1942
960             # -5:00 US E%sT 1946
961             # -5:00 NYC E%sT 1967
962             # -5:00 US E%sT
963             #
964             # Stored locally as:
965             #
966             # %Zone = (
967             # "America/New_York" =>
968             # [
969             # "America/New_York",
970             # [ -04:56:02 1 - LMT 1883 11 0 18 1 12:03:58 w START END ]
971             # ,[ -05:00:00 2 US E%sT 1920 01 0 01 1 00:00:00 w START END ]
972             # ,[ -05:00:00 2 NYC E%sT 1942 01 0 01 1 00:00:00 w START END ]
973             # ,[ -05:00:00 2 US E%sT 1946 01 0 01 1 00:00:00 w START END ]
974             # ,[ -05:00:00 2 NYC E%sT 1967 01 0 01 1 00:00:00 w START END ]
975             # ,[ -05:00:00 2 US E%sT 9999 12 0 31 1 00:00:00 u START END ]
976             # ]
977             # )
978             #
979             # Each %Zone list consists of:
980             # GMTOFF RULETYPE RULE ABBREV YEAR MON DOW NUM FLAG TIME TIMETYPE START
981             # where
982             # GMTOFF : the standard time offset for the time period starting
983             # at the end of the previous peried, and ending at the
984             # time specified by TIME/TIMETYPE
985             # RULETYPE : what type of value RULE can have
986             # $TZ_STANDARD the entire period is standard time
987             # $TZ_RULE the name of a rule to use for this period
988             # $TZ_OFFSET an additional offset to apply for the
989             # entire period (which is in saving time)
990             # RULE : a dash (-), the name of the rule, or an offset
991             # ABBREV : an abbreviation for the timezone (which may include a %s
992             # where letters from a rule are substituted)
993             # YEAR/MON : the year and month where the time period ends
994             # DOW/NUM/FLAG : the day of the month where the time period ends (see
995             # note below)
996             # TIME : HH:MM:SS where the time period ends
997             # TIMETYPE : how the time is to be interpreted
998             # u in UTC
999             # w wallclock time
1000             # s in standard time
1001             # START : This is the wallclock time when this zoneline starts. If the
1002             # wallclock time cannot be decided yet, it is left blank. In
1003             # the case of a non-wallclock time, the change should NOT
1004             # occur on Dec 31 or Jan 1.
1005             # END : The wallclock date/time when the zoneline ends. Blank if
1006             # it cannot be decided.
1007             #
1008             # The time stored in the until fields (which is turned into the
1009             # YEAR/MON/DOW/NUM/FLAG fields) actually refers to the exact second when
1010             # the following zone line takes affect. When a rule specifies a time
1011             # change exactly at that time (unfortunately, this situation DOES occur),
1012             # the change will only apply to the next zone line.
1013             #
1014             # In interpreting DOW, NUM, FLAG, the value of FLAG determines how it is
1015             # done. Values are:
1016             # $TZ_DOM NUM is the day of month (1-31), DOW is ignored
1017             # $TZ_LAST NUM is ignored, DOW is the day of week (1-7); the day
1018             # of month is the last DOW in the month
1019             # $TZ_GE NUM is a cutoff date (1-31), DOW is the day of week; the
1020             # day of month is the first DOW in the month on or after
1021             # the cutoff date
1022             # $TZ_LE Similar to $TZ_GE but the day of month is the last DOW in
1023             # the month on or before the cutoff date
1024             #
1025             # In a time period which uses a named rule, if the named rule doesn't
1026             # cover a year, just use the standard time for that year.
1027              
1028             # The total period covered by zones is from Jan 2, 0001 (00:00:00) to
1029             # Dec 30, 9999 (23:59:59). The first and last days are ignored so that
1030             # they can safely be expressed as wallclock time.
1031              
1032             # a signed time
1033             sub _zone_GMTOff {
1034 0     0     my($self,$zone,$gmt) = @_;
1035 0           my($ret) = $self->_rule_Time($gmt,1);
1036 0 0         if (! $ret) {
1037 0           carp "ERROR: [zone_gmtoff] invalid: $zone: $gmt";
1038 0           $Error = 1;
1039             }
1040 0           return $ret;
1041             }
1042              
1043             # rule, a signed time, or "-"
1044             sub _zone_Rule {
1045 0     0     my($self,$zone,$rule) = @_;
1046 0 0         return ($TZ_STANDARD,$rule) if ($rule eq "-");
1047 0           my($ret) = $self->_rule_Time($rule,1);
1048 0 0         return ($TZ_OFFSET,$ret) if ($ret);
1049 0 0         if (! $self->_tzd_Rule($rule)) {
1050 0           carp "ERROR: [zone_rule] rule undefined: $zone: $rule";
1051 0           $Error = 1;
1052             }
1053 0           return ($TZ_RULE,$rule);
1054             }
1055              
1056             # a format
1057             sub _zone_Format {
1058 0     0     my($self,$zone,$format)=@_;
1059 0           return $format;
1060             }
1061              
1062             # a date (YYYY MMM DD TIME)
1063             sub _zone_Until {
1064 0     0     my($self,$zone,$y,$m,$d,$t) = @_;
1065 0           my($tmp,$type,$dow,$num,$flag,$err);
1066              
1067 0 0         if (! $y) {
1068             # Until '' == Until '9999 Dec 31 00:00:00'
1069 0           $y = 9999;
1070 0           $m = 12;
1071 0           $d = 31;
1072 0           $t = '00:00:00';
1073              
1074             } else {
1075             # Until '1975 ...'
1076 0 0         if ($y !~ /^\d\d\d\d$/) {
1077 0           carp "ERROR: [zone_until] invalid year: $zone: $y";
1078 0           $Error = 1;
1079 0           return ();
1080             }
1081              
1082 0 0         if (! $m) {
1083             # Until '1920' == Until '1920 Jan 1 00:00:00'
1084 0           $m = 1;
1085 0           $d = 1;
1086 0           $t = '00:00:00';
1087              
1088             } else {
1089              
1090             # Until '1920 Mar ...'
1091 0           $tmp = $self->_rule_Month($m);
1092 0 0         if (! $tmp) {
1093 0           carp "ERROR: [zone_until] invalid month: $zone: $m";
1094 0           $Error = 1;
1095 0           return ();
1096             }
1097 0           $m = $tmp;
1098              
1099 0 0         if (! $d) {
    0          
    0          
    0          
1100             # Until '1920 Feb' == Until '1920 Feb 1 00:00:00'
1101 0           $d = 1;
1102 0           $t = '00:00:00';
1103              
1104             } elsif ($d =~ /^last(.*)/) {
1105             # Until '1920 Feb lastSun ...'
1106 0           my(@tmp) = $self->_rule_DOM($d);
1107 0           my($dow) = $tmp[0];
1108 0           my $ymd = $dmb->nth_day_of_week($y,-1,$dow,$m);
1109 0           $d = $$ymd[2];
1110              
1111             } elsif ($d =~ />=/) {
1112 0           my(@tmp) = $self->_rule_DOM($d);
1113 0           my $dow = $tmp[0];
1114 0           $d = $tmp[1];
1115 0           my $ddow = $dmb->day_of_week([$y,$m,$d]);
1116 0 0         if ($dow > $ddow) {
    0          
1117 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],$dow-$ddow);
1118 0           $d = $$ymd[2];
1119             } elsif ($dow < $ddow) {
1120 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($ddow-$dow));
1121 0           $d = $$ymd[2];
1122             }
1123              
1124             } elsif ($d =~ /<=/) {
1125 0           my(@tmp) = $self->_rule_DOM($d);
1126 0           my $dow = $tmp[0];
1127 0           $d = $tmp[1];
1128 0           my $ddow = $dmb->day_of_week([$y,$m,$d]);
1129 0 0         if ($dow < $ddow) {
    0          
1130 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],$ddow-$dow,1);
1131 0           $d = $$ymd[2];
1132             } elsif ($dow > $ddow) {
1133 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($dow-$ddow),1);
1134 0           $d = $$ymd[2];
1135             }
1136              
1137             } else {
1138             # Until '1920 Feb 20 ...'
1139             }
1140              
1141 0 0         if (! $t) {
1142             # Until '1920 Feb 20' == Until '1920 Feb 20 00:00:00'
1143 0           $t = '00:00:00';
1144             }
1145             }
1146             }
1147              
1148             # Make sure that day and month are valid and formatted correctly
1149 0           ($dow,$num,$flag,$err) = $self->_rule_DOM($d);
1150 0 0         if ($err) {
1151 0           carp "ERROR: [zone_until] invalid day: $zone: $d";
1152 0           $Error = 1;
1153 0           return ();
1154             }
1155              
1156 0 0         $m = "0$m" if (length($m)<2);
1157              
1158             # Get the time type
1159 0 0         if ($y == 9999) {
1160 0           $type = 'w';
1161             } else {
1162 0           ($tmp,$type) = $self->_rule_Time($t,0,1);
1163 0 0         if (! $tmp) {
1164 0           carp "ERROR: [zone_until] invalid time: $zone: $t";
1165 0           $Error = 1;
1166 0           return ();
1167             }
1168 0           $t = $tmp;
1169             }
1170              
1171             # Get the wallclock end of this zone line (and the start of the
1172             # next one 1 second later) if possible. Since we cannot assume that
1173             # the rules are present yet, we can only do this for wallclock time
1174             # types. 'u' and 's' time types will be done later.
1175 0           my ($start,$end) = ('','');
1176 0 0         if ($type eq 'w') {
1177             # Start of next time is Y-M-D-TIME
1178 0           $start = $dmb->join('date',[$y,$m,$d,@{ $dmb->split('hms',$t) }]);
  0            
1179             # End of this time is Y-M-D-TIME minus 1 second
1180 0           $end = $dmb->_calc_date_time_strings($start,'0:0:1',1);
1181             }
1182 0           return ($y,$m,$dow,$num,$flag,$t,$type,$end,$start);
1183             }
1184              
1185             ###############################################################################
1186             # ROUTINES FOR GETTING INFORMATION OUT OF RULES/ZONES
1187             ###############################################################################
1188              
1189             sub _tzd_ZoneLines {
1190 0     0     my($self,$zone) = @_;
1191 0           my @z = $self->_tzd_Zone($zone);
1192 0           shift(@z);
1193              
1194             # This will fill in any missing start/end values using the rules
1195             # (which are now all present).
1196              
1197 0           my $i = 0;
1198 0           my($lastend,$lastdstend) = ('','00:00:00');
1199 0           foreach my $z (@z) {
1200 0           my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
1201             $timetype,$start,$end) = @$z;
1202              
1203             # Make sure that we have a start wallclock time. We ALWAYS have the
1204             # start time of the first zone line, and we will always have the
1205             # end time of the zoneline before (if this is not the first) since
1206             # we will determine it below.
1207              
1208 0 0         if (! $start) {
1209 0           $start = $dmb->_calc_date_time_strings($lastend,'0:0:1',0);
1210             }
1211              
1212             # If we haven't got a wallclock end, we can't get it yet... but
1213             # we can get an unadjusted end which we'll use for determining
1214             # what offsets apply from the rules.
1215              
1216 0           my $fixend = 0;
1217 0 0         if (! $end) {
1218 0           $end = $self->_tzd_ParseRuleDate($yr,$mon,$dow,$num,$flag,$time);
1219 0           $fixend = 1;
1220             }
1221              
1222             # Now we need to get the DST offset at the start and end of
1223             # the period.
1224              
1225 0           my($dstbeg,$dstend,$letbeg,$letend);
1226 0 0         if ($ruletype == $TZ_RULE) {
    0          
1227 0           $dstbeg = $lastdstend;
1228              
1229             # Get the year from the end time for the zone line
1230             # Get the dates for this rule.
1231             # Find the latest one which is less than the end date.
1232             # That's the end DST offset.
1233              
1234 0           my %lett = ();
1235 0           my $tmp = $dmb->split('date',$end);
1236 0           my $y = $$tmp[0];
1237 0           my(@rdate) = $self->_ruleInfo($rule,'rdates',$y);
1238 0           my $d = $start;
1239 0           while (@rdate) {
1240 0           my($date,$off,$type,$lett,@tmp) = @rdate;
1241 0           $lett{$off} = $lett;
1242 0           @rdate = @tmp;
1243 0 0 0       next if ($date lt $d || $date gt $end);
1244 0           $d = $date;
1245 0           $dstend = $off;
1246             }
1247              
1248             # If we didn't find $dstend, it's because the zone line
1249             # ends before any rules can go into affect. If that is
1250             # the case, we'll do one of two things:
1251             #
1252             # If the zone line starts this year, no time changes
1253             # occured, so we set $dstend to the same as $dstbeg.
1254             #
1255             # Otherwise, set it to the last DST offset of the year
1256             # before.
1257              
1258 0 0         if (! $dstend) {
1259 0           my($yrbeg) = $dmb->join('date',[$y,1,1,0,0,0]);
1260 0 0         if ($start ge $yrbeg) {
1261 0           $dstend = $dstbeg;
1262             } else {
1263 0           $dstend = $self->_ruleInfo($rule,'lastoff',$y);
1264             }
1265             }
1266              
1267 0           $letbeg = $lett{$dstbeg};
1268 0           $letend = $lett{$dstend};
1269              
1270             } elsif ($ruletype == $TZ_STANDARD) {
1271 0           $dstbeg = '00:00:00';
1272 0           $dstend = $dstbeg;
1273 0           $letbeg = '';
1274 0           $letend = '';
1275             } else {
1276 0           $dstbeg = $rule;
1277 0           $dstend = $dstbeg;
1278 0           $letbeg = '';
1279 0           $letend = '';
1280             }
1281              
1282             # Now we calculate the wallclock end time (if we don't already
1283             # have it).
1284              
1285 0 0         if ($fixend) {
1286 0 0         if ($timetype eq 'u') {
1287             # UT time -> STD time
1288 0           $end = $dmb->_calc_date_time_strings($end,$offset,0);
1289             }
1290             # STD time -> wallclock time
1291 0           $end = $dmb->_calc_date_time_strings($end,$dstend,1);
1292             }
1293              
1294             # Store the information
1295              
1296 0           $i++;
1297 0           $$self{'zonelines'}{$zone}{$i}{'start'} = $start;
1298 0           $$self{'zonelines'}{$zone}{$i}{'end'} = $end;
1299 0           $$self{'zonelines'}{$zone}{$i}{'stdoff'} = $offset;
1300 0           $$self{'zonelines'}{$zone}{$i}{'dstbeg'} = $dstbeg;
1301 0           $$self{'zonelines'}{$zone}{$i}{'dstend'} = $dstend;
1302 0           $$self{'zonelines'}{$zone}{$i}{'letbeg'} = $letbeg;
1303 0           $$self{'zonelines'}{$zone}{$i}{'letend'} = $letend;
1304 0           $$self{'zonelines'}{$zone}{$i}{'abbrev'} = $abbrev;
1305 0 0         $$self{'zonelines'}{$zone}{$i}{'rule'} = ($ruletype == $TZ_RULE ?
1306             $rule : '');
1307 0           $lastend = $end;
1308 0           $lastdstend = $dstend;
1309             }
1310 0           $$self{'zonelines'}{$zone}{'numlines'} = $i;
1311              
1312 0           return;
1313             }
1314              
1315             # Parses date information from a single rule and returns a date.
1316             # The date is not adjusted for standard time or daylight saving time
1317             # offsets.
1318             sub _tzd_ParseRuleDate {
1319 0     0     my($self,$year,$mon,$dow,$num,$flag,$time) = @_;
1320              
1321             # Calculate the day-of-month
1322 0           my($dom);
1323 0 0         if ($flag==$TZ_DOM) {
    0          
    0          
    0          
1324 0           $dom = $num;
1325             } elsif ($flag==$TZ_LAST) {
1326 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
  0            
1327             } elsif ($flag==$TZ_GE) {
1328 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,1,$dow,$mon) };
  0            
1329 0           while ($dom<$num) {
1330 0           $dom += 7;
1331             }
1332             } elsif ($flag==$TZ_LE) {
1333 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
  0            
1334 0           while ($dom>$num) {
1335 0           $dom -= 7;
1336             }
1337             }
1338              
1339             # Split the time and then form the date
1340 0           my($h,$mn,$s) = split(/:/,$time);
1341              
1342 0           return $dmb->join('date',[$year,$mon,$dom,$h,$mn,$s]);
1343             }
1344              
1345             1;
1346             # Local Variables:
1347             # mode: cperl
1348             # indent-tabs-mode: nil
1349             # cperl-indent-level: 3
1350             # cperl-continued-statement-offset: 2
1351             # cperl-continued-brace-offset: 0
1352             # cperl-brace-offset: 0
1353             # cperl-brace-imaginary-offset: 0
1354             # cperl-label-offset: 0
1355             # End: