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-2023 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   1300 use IO::File;
  2         4  
  2         345  
9 2     2   17 use Date::Manip::Base;
  2         5  
  2         62  
10 2     2   12 use Carp;
  2         11  
  2         145  
11              
12 2     2   15 use strict;
  2         5  
  2         43  
13 2     2   9 use integer;
  2         4  
  2         10  
14 2     2   61 use warnings;
  2         5  
  2         14453  
15              
16             our $VERSION;
17             $VERSION='6.91';
18 2     2   13 END { undef $VERSION; }
19              
20             ###############################################################################
21             # GLOBAL VARIABLES
22             ###############################################################################
23              
24             our ($Verbose,@StdFiles,$dmb);
25             END {
26 2     2   4 undef $Verbose;
27 2         6 undef @StdFiles;
28 2         226 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         4 undef $TZ_LAST;
53 2         4 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   27738 undef $TZ_STANDARD;
65 2         5 undef $TZ_RULE;
66 2         8 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              
740 0 0         if (exists $months{ lc($mmm) }) {
741 0           return $months{ lc($mmm) };
742             } else {
743 0           return 0;
744             }
745             }
746              
747             # Returns a time. The time (HH:MM:SS) which may optionally be signed (if $sign
748             # is 1), and may optionally (if $type is 1) be followed by a type
749             # ('w', 'u', or 's').
750             sub _rule_Time {
751 0     0     my($self,$time,$sign,$type) = @_;
752 0           my($s,$t);
753              
754 0 0         if ($type) {
755 0           $t = 'w';
756 0 0 0       if ($type && $time =~ s/(w|u|s)$//i) {
757 0           $t = lc($1);
758             }
759             }
760              
761 0 0         if ($sign) {
762 0 0         if ($time =~ s/^-//) {
763 0           $s = "-";
764             } else {
765 0           $s = '';
766 0           $time =~ s/^\+//;
767             }
768             } else {
769 0           $s = '';
770             }
771              
772 0 0         return '' if ($time !~ /^(\d\d?)(?::(\d\d))?(?::(\d\d))?$/);
773 0           my($hr,$mn,$se)=($1,$2,$3);
774 0 0         $hr = '00' if (! $hr);
775 0 0         $mn = '00' if (! $mn);
776 0 0         $se = '00' if (! $se);
777 0 0         $hr = "0$hr" if (length($hr)<2);
778 0 0         $mn = "0$mn" if (length($mn)<2);
779 0 0         $se = "0$se" if (length($se)<2);
780 0           $time = "$s$hr:$mn:$se";
781 0 0         if ($type) {
782 0           return ($time,$t);
783             } else {
784 0           return $time;
785             }
786             }
787              
788             # a year or 'minimum'
789             sub _rule_From {
790 0     0     my($self,$rule,$from) = @_;
791 0           $from = lc($from);
792 0 0 0       if ($from =~ /^\d\d\d\d$/) {
    0          
793 0           return $from;
794             } elsif ($from eq 'minimum' || $from eq 'min') {
795 0           return '0001';
796             }
797 0           carp "ERROR: [rule_from] invalid: $rule: $from";
798 0           $Error = 1;
799 0           return '';
800             }
801              
802             # a year, 'maximum', or 'only'
803             sub _rule_To {
804 0     0     my($self,$rule,$to,$from) = @_;
805 0           $to = lc($to);
806 0 0 0       if ($to =~ /^\d\d\d\d$/) {
    0          
    0          
807 0           return $to;
808             } elsif ($to eq 'maximum' || $to eq 'max') {
809 0           return '9999';
810             } elsif (lc($to) eq 'only') {
811 0           return $from;
812             }
813 0           carp "ERROR: [rule_to] invalid: $rule: $to";
814 0           $Error = 1;
815 0           return '';
816             }
817              
818             # "-", 'even', or 'odd'
819             sub _rule_Type {
820 0     0     my($self,$rule,$type) = @_;
821 0 0 0       return lc($type) if (lc($type) eq "-" ||
      0        
822             lc($type) eq 'even' ||
823             lc($type) eq 'odd');
824 0           carp "ERROR: [rule_type] invalid: $rule: $type";
825 0           $Error = 1;
826 0           return '';
827             }
828              
829             # a month
830             sub _rule_In {
831 0     0     my($self,$rule,$in) = @_;
832 0           my($i) = $self->_rule_Month($in);
833 0 0         if (! $i) {
834 0           carp "ERROR: [rule_in] invalid: $rule: $in";
835 0           $Error = 1;
836             }
837 0           return $i;
838             }
839              
840             # DoM (1-31), lastDow (lastSun), DoW<=number (Mon<=12),
841             # DoW>=number (Sat>=14)
842             #
843             # Returns: (flag,dow,num)
844             sub _rule_On {
845 0     0     my($self,$rule,$on) = @_;
846 0           my($dow,$num,$flag,$err) = $self->_rule_DOM($on);
847              
848 0 0         if ($err) {
849 0           carp "ERROR: [rule_on] invalid: $rule: $on";
850 0           $Error = 1;
851             }
852              
853 0           return ($flag,$dow,$num);
854             }
855              
856             # a time followed by 'w' (default), 'u', or 's';
857             sub _rule_At {
858 0     0     my($self,$rule,$at) = @_;
859 0           my($ret,$attype) = $self->_rule_Time($at,0,1);
860 0 0         if (! $ret) {
861 0           carp "ERROR: [rule_at] invalid: $rule: $at";
862 0           $Error = 1;
863             }
864 0           return($attype,$ret);
865             }
866              
867             # a signed time (or "-" which is equivalent to 0)
868             sub _rule_Save {
869 0     0     my($self,$rule,$save) = @_;
870 0 0         $save = '00:00:00' if ($save eq "-");
871 0           my($ret) = $self->_rule_Time($save,1);
872 0 0         if (! $ret) {
873 0           carp "ERROR: [rule_save] invalid: $rule: $save";
874 0           $Error=1;
875             }
876 0           return $ret;
877             }
878              
879             # letters (or "-")
880             sub _rule_Letters {
881 0     0     my($self,$rule,$letters)=@_;
882 0 0         return '' if ($letters eq "-");
883 0           return $letters;
884             }
885              
886             ########################################################################
887             # PARSING ZONES
888             ########################################################################
889              
890             my($TZ_START) = $dmb->join('date',['0001',1,2,0,0,0]);
891             my($TZ_END) = $dmb->join('date',['9999',12,30,23,59,59]);
892              
893             sub _tzd_Zone {
894 0     0     my($self,$zone,$listref) = @_;
895              
896 0 0         if (defined $listref) {
    0          
897 0 0         if (! exists $$self{'zone'}{$zone}) {
898 0           $$self{'zone'}{$zone} = [$zone];
899             }
900 0           push @{ $$self{'zone'}{$zone} }, [ @$listref ];
  0            
901              
902             } elsif (exists $$self{'zone'}{$zone}) {
903 0           return @{ $$self{'zone'}{$zone} };
  0            
904              
905             } else {
906 0           return ();
907             }
908             }
909              
910             sub _tzd_DeleteZone {
911 0     0     my($self,$zone) = @_;
912 0           delete $$self{'zone'}{$zone};
913              
914 0           return;
915             }
916              
917             sub _tzd_ZoneKeys {
918 0     0     my($self) = @_;
919 0           return keys %{ $$self{'zone'} };
  0            
920             }
921              
922             sub _tzd_CheckZones {
923 0     0     my($self) = @_;
924 0 0         print "... zones\n" if ($Verbose);
925 0           foreach my $zone ($self->_tzd_ZoneKeys()) {
926 0           my($start) = $TZ_START;
927 0           $Error = 0;
928 0           my ($name,@zone) = $self->_tzd_Zone($zone);
929 0           $self->_tzd_DeleteZone($zone);
930 0           while (@zone) {
931 0           my($gmt,$rule,$format,@until) = @{ shift(@zone) };
  0            
932 0           my($ruletype);
933 0           $gmt = $self->_zone_GMTOff($zone,$gmt);
934 0           ($ruletype,$rule) = $self->_zone_Rule ($zone,$rule);
935 0           $format = $self->_zone_Format($zone,$format);
936 0           my($y,$m,$dow,$num,$flag,$t,$type,$end,$nextstart)
937             = $self->_zone_Until ($zone,@until);
938              
939 0 0         if (! $Error) {
940 0           $self->_tzd_Zone($zone,[ $gmt,$ruletype,$rule,$format,$y,$m,$dow,
941             $num,$flag,$t,$type,$start,$end ]);
942 0           $start = $nextstart;
943             }
944             }
945 0 0         $self->_tzd_DeleteZone($zone) if ($Error);
946             }
947              
948 0           return;
949             }
950              
951             # TZdata file:
952             #
953             # #Zone NAME GMTOFF RULES FORMAT [UNTIL]
954             # Zone America/New_York -4:56:02 - LMT 1883 Nov 18 12:03:58
955             # -5:00 US E%sT 1920
956             # -5:00 NYC E%sT 1942
957             # -5:00 US E%sT 1946
958             # -5:00 NYC E%sT 1967
959             # -5:00 US E%sT
960             #
961             # Stored locally as:
962             #
963             # %Zone = (
964             # "America/New_York" =>
965             # [
966             # "America/New_York",
967             # [ -04:56:02 1 - LMT 1883 11 0 18 1 12:03:58 w START END ]
968             # ,[ -05:00:00 2 US E%sT 1920 01 0 01 1 00:00:00 w START END ]
969             # ,[ -05:00:00 2 NYC E%sT 1942 01 0 01 1 00:00:00 w START END ]
970             # ,[ -05:00:00 2 US E%sT 1946 01 0 01 1 00:00:00 w START END ]
971             # ,[ -05:00:00 2 NYC E%sT 1967 01 0 01 1 00:00:00 w START END ]
972             # ,[ -05:00:00 2 US E%sT 9999 12 0 31 1 00:00:00 u START END ]
973             # ]
974             # )
975             #
976             # Each %Zone list consists of:
977             # GMTOFF RULETYPE RULE ABBREV YEAR MON DOW NUM FLAG TIME TIMETYPE START
978             # where
979             # GMTOFF : the standard time offset for the time period starting
980             # at the end of the previous peried, and ending at the
981             # time specified by TIME/TIMETYPE
982             # RULETYPE : what type of value RULE can have
983             # $TZ_STANDARD the entire period is standard time
984             # $TZ_RULE the name of a rule to use for this period
985             # $TZ_OFFSET an additional offset to apply for the
986             # entire period (which is in saving time)
987             # RULE : a dash (-), the name of the rule, or an offset
988             # ABBREV : an abbreviation for the timezone (which may include a %s
989             # where letters from a rule are substituted)
990             # YEAR/MON : the year and month where the time period ends
991             # DOW/NUM/FLAG : the day of the month where the time period ends (see
992             # note below)
993             # TIME : HH:MM:SS where the time period ends
994             # TIMETYPE : how the time is to be interpreted
995             # u in UTC
996             # w wallclock time
997             # s in standard time
998             # START : This is the wallclock time when this zoneline starts. If the
999             # wallclock time cannot be decided yet, it is left blank. In
1000             # the case of a non-wallclock time, the change should NOT
1001             # occur on Dec 31 or Jan 1.
1002             # END : The wallclock date/time when the zoneline ends. Blank if
1003             # it cannot be decided.
1004             #
1005             # The time stored in the until fields (which is turned into the
1006             # YEAR/MON/DOW/NUM/FLAG fields) actually refers to the exact second when
1007             # the following zone line takes affect. When a rule specifies a time
1008             # change exactly at that time (unfortunately, this situation DOES occur),
1009             # the change will only apply to the next zone line.
1010             #
1011             # In interpreting DOW, NUM, FLAG, the value of FLAG determines how it is
1012             # done. Values are:
1013             # $TZ_DOM NUM is the day of month (1-31), DOW is ignored
1014             # $TZ_LAST NUM is ignored, DOW is the day of week (1-7); the day
1015             # of month is the last DOW in the month
1016             # $TZ_GE NUM is a cutoff date (1-31), DOW is the day of week; the
1017             # day of month is the first DOW in the month on or after
1018             # the cutoff date
1019             # $TZ_LE Similar to $TZ_GE but the day of month is the last DOW in
1020             # the month on or before the cutoff date
1021             #
1022             # In a time period which uses a named rule, if the named rule doesn't
1023             # cover a year, just use the standard time for that year.
1024              
1025             # The total period covered by zones is from Jan 2, 0001 (00:00:00) to
1026             # Dec 30, 9999 (23:59:59). The first and last days are ignored so that
1027             # they can safely be expressed as wallclock time.
1028              
1029             # a signed time
1030             sub _zone_GMTOff {
1031 0     0     my($self,$zone,$gmt) = @_;
1032 0           my($ret) = $self->_rule_Time($gmt,1);
1033 0 0         if (! $ret) {
1034 0           carp "ERROR: [zone_gmtoff] invalid: $zone: $gmt";
1035 0           $Error = 1;
1036             }
1037 0           return $ret;
1038             }
1039              
1040             # rule, a signed time, or "-"
1041             sub _zone_Rule {
1042 0     0     my($self,$zone,$rule) = @_;
1043 0 0         return ($TZ_STANDARD,$rule) if ($rule eq "-");
1044 0           my($ret) = $self->_rule_Time($rule,1);
1045 0 0         return ($TZ_OFFSET,$ret) if ($ret);
1046 0 0         if (! $self->_tzd_Rule($rule)) {
1047 0           carp "ERROR: [zone_rule] rule undefined: $zone: $rule";
1048 0           $Error = 1;
1049             }
1050 0           return ($TZ_RULE,$rule);
1051             }
1052              
1053             # a format
1054             sub _zone_Format {
1055 0     0     my($self,$zone,$format)=@_;
1056 0           return $format;
1057             }
1058              
1059             # a date (YYYY MMM DD TIME)
1060             sub _zone_Until {
1061 0     0     my($self,$zone,$y,$m,$d,$t) = @_;
1062 0           my($tmp,$type,$dow,$num,$flag,$err);
1063              
1064 0 0         if (! $y) {
1065             # Until '' == Until '9999 Dec 31 00:00:00'
1066 0           $y = 9999;
1067 0           $m = 12;
1068 0           $d = 31;
1069 0           $t = '00:00:00';
1070              
1071             } else {
1072             # Until '1975 ...'
1073 0 0         if ($y !~ /^\d\d\d\d$/) {
1074 0           carp "ERROR: [zone_until] invalid year: $zone: $y";
1075 0           $Error = 1;
1076 0           return ();
1077             }
1078              
1079 0 0         if (! $m) {
1080             # Until '1920' == Until '1920 Jan 1 00:00:00'
1081 0           $m = 1;
1082 0           $d = 1;
1083 0           $t = '00:00:00';
1084              
1085             } else {
1086              
1087             # Until '1920 Mar ...'
1088 0           $tmp = $self->_rule_Month($m);
1089 0 0         if (! $tmp) {
1090 0           carp "ERROR: [zone_until] invalid month: $zone: $m";
1091 0           $Error = 1;
1092 0           return ();
1093             }
1094 0           $m = $tmp;
1095              
1096 0 0         if (! $d) {
    0          
    0          
    0          
1097             # Until '1920 Feb' == Until '1920 Feb 1 00:00:00'
1098 0           $d = 1;
1099 0           $t = '00:00:00';
1100              
1101             } elsif ($d =~ /^last(.*)/) {
1102             # Until '1920 Feb lastSun ...'
1103 0           my(@tmp) = $self->_rule_DOM($d);
1104 0           my($dow) = $tmp[0];
1105 0           my $ymd = $dmb->nth_day_of_week($y,-1,$dow,$m);
1106 0           $d = $$ymd[2];
1107              
1108             } elsif ($d =~ />=/) {
1109 0           my(@tmp) = $self->_rule_DOM($d);
1110 0           my $dow = $tmp[0];
1111 0           $d = $tmp[1];
1112 0           my $ddow = $dmb->day_of_week([$y,$m,$d]);
1113 0 0         if ($dow > $ddow) {
    0          
1114 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],$dow-$ddow);
1115 0           $d = $$ymd[2];
1116             } elsif ($dow < $ddow) {
1117 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($ddow-$dow));
1118 0           $d = $$ymd[2];
1119             }
1120              
1121             } elsif ($d =~ /<=/) {
1122 0           my(@tmp) = $self->_rule_DOM($d);
1123 0           my $dow = $tmp[0];
1124 0           $d = $tmp[1];
1125 0           my $ddow = $dmb->day_of_week([$y,$m,$d]);
1126 0 0         if ($dow < $ddow) {
    0          
1127 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],$ddow-$dow,1);
1128 0           $d = $$ymd[2];
1129             } elsif ($dow > $ddow) {
1130 0           my $ymd = $dmb->calc_date_days([$y,$m,$d],7-($dow-$ddow),1);
1131 0           $d = $$ymd[2];
1132             }
1133              
1134             } else {
1135             # Until '1920 Feb 20 ...'
1136             }
1137              
1138 0 0         if (! $t) {
1139             # Until '1920 Feb 20' == Until '1920 Feb 20 00:00:00'
1140 0           $t = '00:00:00';
1141             }
1142             }
1143             }
1144              
1145             # Make sure that day and month are valid and formatted correctly
1146 0           ($dow,$num,$flag,$err) = $self->_rule_DOM($d);
1147 0 0         if ($err) {
1148 0           carp "ERROR: [zone_until] invalid day: $zone: $d";
1149 0           $Error = 1;
1150 0           return ();
1151             }
1152              
1153 0 0         $m = "0$m" if (length($m)<2);
1154              
1155             # Get the time type
1156 0 0         if ($y == 9999) {
1157 0           $type = 'w';
1158             } else {
1159 0           ($tmp,$type) = $self->_rule_Time($t,0,1);
1160 0 0         if (! $tmp) {
1161 0           carp "ERROR: [zone_until] invalid time: $zone: $t";
1162 0           $Error = 1;
1163 0           return ();
1164             }
1165 0           $t = $tmp;
1166             }
1167              
1168             # Get the wallclock end of this zone line (and the start of the
1169             # next one 1 second later) if possible. Since we cannot assume that
1170             # the rules are present yet, we can only do this for wallclock time
1171             # types. 'u' and 's' time types will be done later.
1172 0           my ($start,$end) = ('','');
1173 0 0         if ($type eq 'w') {
1174             # Start of next time is Y-M-D-TIME
1175 0           $start = $dmb->join('date',[$y,$m,$d,@{ $dmb->split('hms',$t) }]);
  0            
1176             # End of this time is Y-M-D-TIME minus 1 second
1177 0           $end = $dmb->_calc_date_time_strings($start,'0:0:1',1);
1178             }
1179 0           return ($y,$m,$dow,$num,$flag,$t,$type,$end,$start);
1180             }
1181              
1182             ###############################################################################
1183             # ROUTINES FOR GETTING INFORMATION OUT OF RULES/ZONES
1184             ###############################################################################
1185              
1186             sub _tzd_ZoneLines {
1187 0     0     my($self,$zone) = @_;
1188 0           my @z = $self->_tzd_Zone($zone);
1189 0           shift(@z);
1190              
1191             # This will fill in any missing start/end values using the rules
1192             # (which are now all present).
1193              
1194 0           my $i = 0;
1195 0           my($lastend,$lastdstend) = ('','00:00:00');
1196 0           foreach my $z (@z) {
1197 0           my($offset,$ruletype,$rule,$abbrev,$yr,$mon,$dow,$num,$flag,$time,
1198             $timetype,$start,$end) = @$z;
1199              
1200             # Make sure that we have a start wallclock time. We ALWAYS have the
1201             # start time of the first zone line, and we will always have the
1202             # end time of the zoneline before (if this is not the first) since
1203             # we will determine it below.
1204              
1205 0 0         if (! $start) {
1206 0           $start = $dmb->_calc_date_time_strings($lastend,'0:0:1',0);
1207             }
1208              
1209             # If we haven't got a wallclock end, we can't get it yet... but
1210             # we can get an unadjusted end which we'll use for determining
1211             # what offsets apply from the rules.
1212              
1213 0           my $fixend = 0;
1214 0 0         if (! $end) {
1215 0           $end = $self->_tzd_ParseRuleDate($yr,$mon,$dow,$num,$flag,$time);
1216 0           $fixend = 1;
1217             }
1218              
1219             # Now we need to get the DST offset at the start and end of
1220             # the period.
1221              
1222 0           my($dstbeg,$dstend,$letbeg,$letend);
1223 0 0         if ($ruletype == $TZ_RULE) {
    0          
1224 0           $dstbeg = $lastdstend;
1225              
1226             # Get the year from the end time for the zone line
1227             # Get the dates for this rule.
1228             # Find the latest one which is less than the end date.
1229             # That's the end DST offset.
1230              
1231 0           my %lett = ();
1232 0           my $tmp = $dmb->split('date',$end);
1233 0           my $y = $$tmp[0];
1234 0           my(@rdate) = $self->_ruleInfo($rule,'rdates',$y);
1235 0           my $d = $start;
1236 0           while (@rdate) {
1237 0           my($date,$off,$type,$lett,@tmp) = @rdate;
1238 0           $lett{$off} = $lett;
1239 0           @rdate = @tmp;
1240 0 0 0       next if ($date lt $d || $date gt $end);
1241 0           $d = $date;
1242 0           $dstend = $off;
1243             }
1244              
1245             # If we didn't find $dstend, it's because the zone line
1246             # ends before any rules can go into affect. If that is
1247             # the case, we'll do one of two things:
1248             #
1249             # If the zone line starts this year, no time changes
1250             # occured, so we set $dstend to the same as $dstbeg.
1251             #
1252             # Otherwise, set it to the last DST offset of the year
1253             # before.
1254              
1255 0 0         if (! $dstend) {
1256 0           my($yrbeg) = $dmb->join('date',[$y,1,1,0,0,0]);
1257 0 0         if ($start ge $yrbeg) {
1258 0           $dstend = $dstbeg;
1259             } else {
1260 0           $dstend = $self->_ruleInfo($rule,'lastoff',$y);
1261             }
1262             }
1263              
1264 0           $letbeg = $lett{$dstbeg};
1265 0           $letend = $lett{$dstend};
1266              
1267             } elsif ($ruletype == $TZ_STANDARD) {
1268 0           $dstbeg = '00:00:00';
1269 0           $dstend = $dstbeg;
1270 0           $letbeg = '';
1271 0           $letend = '';
1272             } else {
1273 0           $dstbeg = $rule;
1274 0           $dstend = $dstbeg;
1275 0           $letbeg = '';
1276 0           $letend = '';
1277             }
1278              
1279             # Now we calculate the wallclock end time (if we don't already
1280             # have it).
1281              
1282 0 0         if ($fixend) {
1283 0 0         if ($timetype eq 'u') {
1284             # UT time -> STD time
1285 0           $end = $dmb->_calc_date_time_strings($end,$offset,0);
1286             }
1287             # STD time -> wallclock time
1288 0           $end = $dmb->_calc_date_time_strings($end,$dstend,1);
1289             }
1290              
1291             # Store the information
1292              
1293 0           $i++;
1294 0           $$self{'zonelines'}{$zone}{$i}{'start'} = $start;
1295 0           $$self{'zonelines'}{$zone}{$i}{'end'} = $end;
1296 0           $$self{'zonelines'}{$zone}{$i}{'stdoff'} = $offset;
1297 0           $$self{'zonelines'}{$zone}{$i}{'dstbeg'} = $dstbeg;
1298 0           $$self{'zonelines'}{$zone}{$i}{'dstend'} = $dstend;
1299 0           $$self{'zonelines'}{$zone}{$i}{'letbeg'} = $letbeg;
1300 0           $$self{'zonelines'}{$zone}{$i}{'letend'} = $letend;
1301 0           $$self{'zonelines'}{$zone}{$i}{'abbrev'} = $abbrev;
1302 0 0         $$self{'zonelines'}{$zone}{$i}{'rule'} = ($ruletype == $TZ_RULE ?
1303             $rule : '');
1304 0           $lastend = $end;
1305 0           $lastdstend = $dstend;
1306             }
1307 0           $$self{'zonelines'}{$zone}{'numlines'} = $i;
1308              
1309 0           return;
1310             }
1311              
1312             # Parses date information from a single rule and returns a date.
1313             # The date is not adjusted for standard time or daylight saving time
1314             # offsets.
1315             sub _tzd_ParseRuleDate {
1316 0     0     my($self,$year,$mon,$dow,$num,$flag,$time) = @_;
1317              
1318             # Calculate the day-of-month
1319 0           my($dom);
1320 0 0         if ($flag==$TZ_DOM) {
    0          
    0          
    0          
1321 0           $dom = $num;
1322             } elsif ($flag==$TZ_LAST) {
1323 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
  0            
1324             } elsif ($flag==$TZ_GE) {
1325 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,1,$dow,$mon) };
  0            
1326 0           while ($dom<$num) {
1327 0           $dom += 7;
1328             }
1329             } elsif ($flag==$TZ_LE) {
1330 0           ($year,$mon,$dom) = @{ $dmb->nth_day_of_week($year,-1,$dow,$mon) };
  0            
1331 0           while ($dom>$num) {
1332 0           $dom -= 7;
1333             }
1334             }
1335              
1336             # Split the time and then form the date
1337 0           my($h,$mn,$s) = split(/:/,$time);
1338              
1339 0           return $dmb->join('date',[$year,$mon,$dom,$h,$mn,$s]);
1340             }
1341              
1342             1;
1343             # Local Variables:
1344             # mode: cperl
1345             # indent-tabs-mode: nil
1346             # cperl-indent-level: 3
1347             # cperl-continued-statement-offset: 2
1348             # cperl-continued-brace-offset: 0
1349             # cperl-brace-offset: 0
1350             # cperl-brace-imaginary-offset: 0
1351             # cperl-label-offset: 0
1352             # End: