File Coverage

blib/lib/DateTimeX/Lite/OlsonDB.pm
Criterion Covered Total %
statement 112 400 28.0
branch 21 182 11.5
condition 3 78 3.8
subroutine 33 73 45.2
pod 0 9 0.0
total 169 742 22.7


line stmt bran cond sub pod time code
1             package DateTimeX::Lite::OlsonDB;
2 56     56   330 use strict;
  56         106  
  56         5739  
3 56     56   579 use warnings;
  56         319  
  56         2347  
4 56     56   9139 use DateTimeX::Lite;
  56         127  
  56         2599  
5              
6 56     56   344 use vars qw( %MONTHS %DAYS $PLUS_ONE_DAY_DUR $MINUS_ONE_DAY_DUR );
  56         266  
  56         202402  
7              
8             sub DEBUG () { 0 }
9              
10             my $x = 1;
11             %MONTHS = map { $_ => $x++ }
12             qw( Jan Feb Mar Apr May Jun
13             Jul Aug Sep Oct Nov Dec);
14              
15             $x = 1;
16             %DAYS = map { $_ => $x++ }
17             qw( Mon Tue Wed Thu Fri Sat Sun );
18              
19             $PLUS_ONE_DAY_DUR = DateTimeX::Lite::Duration->new( days => 1 );
20             $MINUS_ONE_DAY_DUR = DateTimeX::Lite::Duration->new( days => -1 );
21              
22             sub new
23             {
24 0     0 0 0 my $class = shift;
25              
26 0         0 return bless { rules => {},
27             zones => {},
28             links => {},
29             }, $class;
30             }
31              
32             sub parse_file {
33 0     0 0 0 my $self = shift;
34 0         0 my $file = shift;
35              
36 0 0       0 open my $fh, "<$file"
37             or die "Cannot read $file: $!";
38              
39 0         0 while (<$fh>) {
40 0         0 chomp;
41 0         0 $self->_parse_line($_);
42             }
43             }
44              
45             sub _parse_line {
46 0     0   0 my $self = shift;
47 0         0 my $line = shift;
48              
49 0 0       0 return if $line =~ /^\s+$/;
50 0 0       0 return if $line =~ /^#/;
51              
52             # remove any comments at the end of the line
53 0         0 $line =~ s/\s*#.+$//;
54              
55 0 0 0     0 if ( $self->{in_zone} && $line =~ /^\t/ )
56             {
57 0         0 $self->_parse_zone( $line, $self->{in_zone} );
58 0         0 return;
59             }
60              
61 0         0 foreach ( qw( Rule Zone Link ) )
62             {
63 0 0       0 if ( substr( $line, 0, 4 ) eq $_ )
64             {
65 0         0 my $m = '_parse_' . lc $_;
66 0         0 $self->$m($line);
67             }
68             }
69             }
70              
71             sub _parse_rule
72             {
73 0     0   0 my $self = shift;
74 0         0 my $rule = shift;
75              
76 0         0 my @items = split /\s+/, $rule, 10;
77              
78 0         0 shift @items;
79 0         0 my $name = shift @items;
80              
81 0         0 my %rule;
82 0         0 @rule{ qw( from to type in on at save letter ) } = @items;
83 0 0       0 delete $rule{letter} if $rule{letter} eq '-';
84              
85             # As of the 2003a data, there are no rules with a type set
86 0 0       0 delete $rule{type} if $rule{type} eq '-';
87              
88 0         0 push @{ $self->{rules}{$name} },
  0         0  
89             DateTimeX::Lite::OlsonDB::Rule->new( name => $name, %rule );
90              
91 0         0 undef $self->{in_zone};
92             }
93              
94             sub _parse_zone
95             {
96 0     0   0 my $self = shift;
97 0         0 my $zone = shift;
98 0         0 my $name = shift;
99              
100 0 0       0 my $expect = $name ? 5 : 6;
101 0 0       0 my @items = grep { defined && length } split /\s+/, $zone, $expect;
  0         0  
102              
103 0         0 my %obs;
104 0 0       0 unless ($name)
105             {
106 0         0 shift @items; # remove "Zone"
107 0         0 $name = shift @items;
108             }
109              
110 0         0 @obs{ qw( gmtoff rules format until ) } = @items;
111              
112 0 0       0 if ( $obs{rules} =~ /\d\d?:\d\d/ )
113             {
114 0         0 $obs{offset_from_std} = delete $obs{rules};
115             }
116             else
117             {
118 0 0       0 delete $obs{rules} if $obs{rules} eq '-';
119             }
120              
121 0 0       0 delete $obs{until} unless defined $obs{until};
122              
123 0         0 push @{ $self->{zones}{$name} }, \%obs;
  0         0  
124              
125 0         0 $self->{in_zone} = $name;
126             }
127              
128             sub _parse_link
129             {
130 0     0   0 my $self = shift;
131 0         0 my $link = shift;
132              
133 0         0 my @items = split /\s+/, $link, 3;
134              
135 0         0 $self->{links}{ $items[2] } = $items[1];
136              
137 0         0 undef $self->{in_zone};
138             }
139              
140 0     0 0 0 sub links { %{ $_[0]->{links} } }
  0         0  
141              
142 0     0 0 0 sub zone_names { keys %{ $_[0]->{zones} } }
  0         0  
143              
144             sub zone
145             {
146 0     0 0 0 my $self = shift;
147 0         0 my $name = shift;
148              
149 0 0       0 die "Invalid zone name $name"
150             unless exists $self->{zones}{$name};
151              
152             return
153 0         0 DateTimeX::Lite::OlsonDB::Zone->new
154             ( name => $name,
155             observances => $self->{zones}{$name},
156             olson_db => $self,
157             );
158             }
159              
160             sub expanded_zone
161             {
162 0     0 0 0 my ($self, %p) = @_;
163              
164 0   0     0 $p{expand_to_year} ||= (localtime)[5] + 1910;
165              
166 0         0 my $zone = $self->zone( $p{name} );
167              
168 0         0 $zone->expand_observances( $self, $p{expand_to_year} );
169              
170 0         0 return $zone;
171             }
172              
173             sub rules_by_name
174             {
175 0     0 0 0 my $self = shift;
176 0         0 my $name = shift;
177              
178 0 0       0 return unless defined $name;
179              
180 0 0       0 die "Invalid rule name $name"
181             unless exists $self->{rules}{$name};
182              
183 0         0 return @{ $self->{rules}{$name} };
  0         0  
184             }
185              
186             sub parse_day_spec
187             {
188 1118     1118 0 1889 my ( $day, $month, $year ) = @_;
189              
190 1118 50       10569 return $day if $day =~ /^\d+$/;
191              
192 1118 100       7873 if ( $day =~ /^last(\w\w\w)$/ )
    50          
193             {
194 46         215 my $dow = $DateTimeX::Lite::OlsonDB::DAYS{$1};
195              
196 46         249 my $last_day = DateTimeX::Lite->last_day_of_month( year => $year,
197             month => $month,
198             time_zone => 'floating',
199             );
200              
201 46         213 my $dt =
202             DateTimeX::Lite->new( year => $year,
203             month => $month,
204             day => $last_day->day,
205             time_zone => 'floating',
206             );
207              
208 46         210 while ( $dt->day_of_week != $dow )
209             {
210 135         521 $dt -= $PLUS_ONE_DAY_DUR;
211             }
212              
213 46         185 return $dt->day;
214             }
215             elsif ( $day =~ /^(\w\w\w)([><])=(\d\d?)$/ )
216             {
217 1072         4324 my $dow = $DateTimeX::Lite::OlsonDB::DAYS{$1};
218              
219 1072         4651 my $dt = DateTimeX::Lite->new( year => $year,
220             month => $month,
221             day => $3,
222             time_zone => 'floating',
223             );
224              
225 1072 50       4967 my $dur = $2 eq '<' ? $MINUS_ONE_DAY_DUR : $PLUS_ONE_DAY_DUR;
226              
227 1072         3945 while ( $dt->day_of_week != $dow )
228             {
229 3264         13878 $dt += $dur;
230             }
231              
232 1072         4606 return $dt->day;
233             }
234             else
235             {
236 0         0 die "Invalid on spec for rule: $day\n";
237             }
238             }
239              
240             sub utc_datetime_for_time_spec {
241 1118     1118 0 5707 my %p = @_;
242              
243             # 'w'all - ignore it, because that's the default
244 1118         3735 $p{spec} =~ s/w$//;
245              
246             # 'g'reenwich, 'u'tc, or 'z'ulu
247 1118         5010 my $is_utc = $p{spec} =~ s/[guz]$//;
248              
249             # 's'tandard time - ignore DS offset
250 1118         3050 my $is_std = $p{spec} =~ s/s$//;
251              
252 1118         5950 my ($hour, $minute, $second) = split /:/, $p{spec};
253 1118 50       3549 $minute = 0 unless defined $minute;
254 1118 50       3038 $second = 0 unless defined $second;
255              
256 1118         1477 my $add_day = 0;
257 1118 50       3462 if ( $hour == 24 )
258             {
259 0         0 $hour = 0;
260 0         0 $add_day = 1;
261             }
262              
263 1118         6666 my $utc;
264 1118 100       2203 if ($is_utc)
265             {
266 46         263 $utc = DateTimeX::Lite->new( year => $p{year},
267             month => $p{month},
268             day => $p{day},
269             hour => $hour,
270             minute => $minute,
271             second => $second,
272             time_zone => 'floating',
273             );
274             }
275             else
276             {
277 1072         5777 my $local = DateTimeX::Lite->new( year => $p{year},
278             month => $p{month},
279             day => $p{day},
280             hour => $hour,
281             minute => $minute,
282             second => $second,
283             time_zone => 'floating',
284             );
285              
286 1072 100       2927 $p{offset_from_std} = 0 if $is_std;
287              
288 1072         7597 my $dur =
289             DateTimeX::Lite::Duration->new
290             ( seconds => $p{offset_from_utc} + $p{offset_from_std} );
291              
292 1072         4805 $utc = $local - $dur;
293             }
294              
295 1118 50       3642 $utc->add( days => 1 ) if $add_day;
296              
297 1118         4123 return $utc;
298             }
299              
300              
301             package DateTimeX::Lite::OlsonDB::Zone;
302              
303 56     56   2110 use strict;
  56         113  
  56         3544  
304              
305 56     56   35411 use DateTimeX::Lite::Util;
  56         149  
  56         80951  
306              
307             sub new
308             {
309 0     0   0 my ($class, %p) = @_;
310              
311 0         0 my $self = { name => $p{name},
312             observances => $p{observances},
313             changes => [],
314             infinite_rules => {},
315             };
316              
317 0         0 return bless $self, $class;
318             }
319              
320 0     0   0 sub name { $_[0]->{name} }
321              
322             sub expand_observances
323             {
324 0     0   0 my $self = shift;
325 0         0 my $odb = shift;
326 0         0 my $max_year = shift;
327              
328 0         0 my $prev_until;
329 0         0 for ( my $x = 0; $x < @{ $self->{observances} }; $x++ )
  0         0  
330             {
331 0         0 my %p = %{ $self->{observances}[$x] };
  0         0  
332 0         0 my $rules_name = delete $p{rules};
333              
334 0 0       0 my $last_offset_from_std =
335             $self->last_change ? $self->last_change->offset_from_std : 0;
336 0 0       0 my $last_offset_from_utc =
337             $self->last_change ? $self->last_change->offset_from_utc : 0;
338              
339 0         0 my $obs =
340             DateTimeX::Lite::OlsonDB::Observance->new
341             ( %p,
342             utc_start_datetime => $prev_until,
343             rules => [ $odb->rules_by_name($rules_name) ],
344             last_offset_from_utc => $last_offset_from_utc,
345             last_offset_from_std => $last_offset_from_std,
346             );
347              
348 0         0 my $rule = $obs->first_rule;
349 0 0       0 my $letter = $rule ? $rule->letter : '';
350              
351 0 0       0 my $change =
352             DateTimeX::Lite::OlsonDB::Change->new
353             ( type => 'observance',
354             utc_start_datetime => $obs->utc_start_datetime,
355             local_start_datetime => $obs->local_start_datetime,
356             short_name => sprintf( $obs->format, $letter ),
357             observance => $obs,
358             $rule ? ( rule => $rule ) : (),
359             );
360              
361 0         0 if (DateTimeX::Lite::OlsonDB::DEBUG)
362             {
363             warn "Adding observance change ...\n";
364              
365             $change->_debug_output;
366             }
367              
368 0         0 $self->add_change($change);
369              
370 0 0       0 if ( $obs->rules )
371             {
372 0         0 $obs->expand_from_rules( $self, $max_year );
373             }
374              
375             $prev_until =
376 0 0       0 $obs->until( $self->last_change ? $self->last_change->offset_from_std : 0 );
377              
378             # last observance
379 0 0       0 if ( $x == $#{ $self->{observances} } )
  0         0  
380             {
381 0         0 foreach my $rule ( $obs->rules )
382             {
383 0 0       0 if ( $rule->is_infinite )
384             {
385 0         0 $self->add_infinite_rule($rule);
386             }
387             }
388             }
389             }
390             }
391              
392             sub add_change
393             {
394 0     0   0 my $self = shift;
395 0         0 my $change = shift;
396              
397 0 0       0 if ( defined $change->utc_start_datetime )
398             {
399 0 0 0     0 if ( @{ $self->{changes} }
  0   0     0  
400             && $self->{changes}[-1]->utc_start_datetime
401             && $self->{changes}[-1]->utc_start_datetime == $change->utc_start_datetime
402             )
403             {
404 0 0 0     0 if ( $self->{changes}[-1]->rule && $change->observance )
405             {
406 0         0 warn " Ignoring previous rule change, that starts the same time as current observance change\n\n"
407             if DateTimeX::Lite::OlsonDB::DEBUG;
408              
409 0         0 $self->{changes}[-1] = $change;
410              
411 0         0 return;
412             }
413              
414 0         0 die "Cannot add two different changes that have the same UTC start datetime!\n";
415             }
416              
417 0         0 my $last_change = $self->last_change;
418              
419 0 0 0     0 if ( $last_change->short_name eq $change->short_name
      0        
      0        
420             && $last_change->total_offset == $change->total_offset
421             && $last_change->is_dst == $change->is_dst
422             && $last_change->observance eq $change->observance
423             )
424             {
425 0   0     0 my $last_rule = $last_change->rule || '';
426 0   0     0 my $new_rule = $change->rule || '';
427              
428 0 0       0 if ( $last_rule eq $new_rule )
429             {
430 0         0 warn "Skipping identical change\n" if DateTimeX::Lite::OlsonDB::DEBUG;
431              
432 0         0 return;
433             }
434             }
435              
436 0         0 push @{ $self->{changes} }, $change;
  0         0  
437             }
438             else
439             {
440 0 0       0 if ( $self->{earliest} )
441             {
442 0         0 die "There can only be one earliest time zone change!";
443             }
444             else
445             {
446 0         0 $self->{earliest} = $change;
447             }
448             }
449             }
450              
451             sub add_infinite_rule
452             {
453 0     0   0 $_[0]->{infinite_rules}{ $_[1] } = $_[1];
454             }
455              
456 0 0 0 0   0 sub last_change { return unless @{ $_[0]->{changes} } || $_[0]->{earliest};
  0         0  
457 0 0       0 return ( @{ $_[0]->{changes} } ?
  0         0  
458             $_[0]->{changes}[-1] :
459             $_[0]->{earliest} ); }
460              
461 0         0 sub sorted_changes { ( ( defined $_[0]->{earliest} ? $_[0]->{earliest} : () ),
462 0         0 sort { $a->utc_start_datetime <=> $b->utc_start_datetime }
463 0 0   0   0 @{ $_[0]->{changes} } ) }
464              
465 0     0   0 sub infinite_rules { values %{ $_[0]->{infinite_rules} } }
  0         0  
466              
467              
468             package DateTimeX::Lite::OlsonDB::Observance;
469              
470 56     56   446 use strict;
  56         124  
  56         2307  
471              
472 56     56   317 use DateTimeX::Lite::Util;
  56         113  
  56         170477  
473              
474             sub new
475             {
476 0     0   0 my ($class, %p) = @_;
477              
478 0   0     0 $p{until} ||= '';
479 0   0     0 $p{offset_from_std} ||= 0;
480 0   0     0 $p{last_offset_from_utc} ||= 0;
481 0   0     0 $p{last_offset_from_std} ||= 0;
482              
483 0         0 my $offset_from_utc = DateTimeX::Lite::Util::offset_as_seconds( $p{gmtoff} );
484 0         0 my $offset_from_std = DateTimeX::Lite::Util::offset_as_seconds( $p{offset_from_std} );
485              
486 0         0 my $last_offset_from_utc = delete $p{last_offset_from_utc};
487 0         0 my $last_offset_from_std = delete $p{last_offset_from_std};
488              
489 0         0 my $self = bless { %p,
490             offset_from_utc => $offset_from_utc,
491             offset_from_std => $offset_from_std,
492             until => [ split /\s+/, $p{until} ],
493             }, $class;
494              
495 0         0 my $local_start_datetime;
496 0 0       0 if ( $p{utc_start_datetime} )
497             {
498 0         0 $self->{first_rule} =
499             $self->_first_rule( $last_offset_from_utc, $last_offset_from_std );
500              
501 0 0       0 $offset_from_std += $self->{first_rule}->offset_from_std if $self->{first_rule};
502              
503 0         0 $local_start_datetime = $p{utc_start_datetime}->clone;
504              
505 0         0 $local_start_datetime +=
506             DateTimeX::Lite::Duration->new( seconds => $offset_from_utc + $offset_from_std );
507              
508 0         0 $self->{local_start_datetime} = $local_start_datetime;
509             }
510              
511 0         0 return $self;
512             }
513              
514 2162     2162   8741 sub offset_from_utc { $_[0]->{offset_from_utc} }
515 3243     3243   14818 sub offset_from_std { $_[0]->{offset_from_std} }
516 1081     1081   3557 sub total_offset { $_[0]->offset_from_utc + $_[0]->offset_from_std }
517              
518 0     0   0 sub rules { @{ $_[0]->{rules} } }
  0         0  
519 0     0   0 sub first_rule { $_[0]->{first_rule} }
520              
521 1081     1081   5595 sub format { $_[0]->{format} }
522              
523 0     0   0 sub utc_start_datetime { $_[0]->{utc_start_datetime} }
524 0     0   0 sub local_start_datetime { $_[0]->{local_start_datetime} }
525              
526             sub expand_from_rules
527             {
528 0     0   0 my $self = shift;
529 0         0 my $zone = shift;
530             # real max is year + 1 so we include max year
531 0         0 my $max_year = (shift) + 1;
532              
533 0         0 my $min_year;
534              
535 0 0       0 if ( $self->utc_start_datetime )
536             {
537 0         0 $min_year = $self->utc_start_datetime->year;
538             }
539             else
540             {
541             # There is at least one time zone that has an infinite
542             # observance, but that observance has rules that only start at
543             # a certain point - Pacific/Chatham
544              
545             # In this case we just find the earliest rule and start there
546              
547 0         0 $min_year = ( sort { $a <=> $b } map { $_->min_year } $self->rules )[0];
  0         0  
  0         0  
548             }
549              
550 0         0 my $until = $self->until( $zone->last_change->offset_from_std );
551 0 0       0 if ($until)
552             {
553 0         0 $max_year = $until->year;
554             }
555             else
556             {
557             # Some zones, like Asia/Tehran, have a predefined fixed set of
558             # rules that go well into the future (2037 for Asia/Tehran)
559 0         0 my $max_rule_year = 0;
560 0         0 foreach my $rule ( $self->rules )
561             {
562 0 0 0     0 $max_rule_year = $rule->max_year
563             if $rule->max_year && $rule->max_year > $max_rule_year;
564             }
565              
566 0 0       0 $max_year = $max_rule_year if $max_rule_year > $max_year;
567             }
568              
569 0         0 foreach my $year ( $min_year .. $max_year )
570             {
571 0         0 my @rules = $self->_sorted_rules_for_year($year);
572              
573 0         0 foreach my $rule (@rules)
574             {
575 0         0 my $dt =
576             $rule->utc_start_datetime_for_year
577             ( $year, $self->offset_from_utc, $zone->last_change->offset_from_std );
578              
579 0 0 0     0 next if $self->utc_start_datetime && $dt <= $self->utc_start_datetime;
580              
581 0         0 my $until = $self->until( $zone->last_change->offset_from_std );
582              
583 0 0 0     0 next if $until && $dt >= $until;
584              
585 0         0 my $change =
586             DateTimeX::Lite::OlsonDB::Change->new
587             ( type => 'rule',
588             utc_start_datetime => $dt,
589             local_start_datetime =>
590             $dt +
591             DateTimeX::Lite::Duration->new
592             ( seconds => $self->total_offset + $rule->offset_from_std ),
593             short_name => sprintf( $self->{format}, $rule->letter ),
594             observance => $self,
595             rule => $rule,
596             );
597              
598 0         0 if (DateTimeX::Lite::OlsonDB::DEBUG)
599             {
600             warn "Adding rule change ...\n";
601              
602             $change->_debug_output;
603             }
604              
605 0         0 $zone->add_change($change);
606             }
607             }
608             }
609              
610             sub _sorted_rules_for_year
611             {
612 0     0   0 my $self = shift;
613 0         0 my $year = shift;
614              
615             return
616 0         0 ( map { $_->[0] }
  0         0  
617 0         0 sort { $a->[1] <=> $b->[1] }
618 0 0 0     0 map { my $dt = $_->utc_start_datetime_for_year( $year, $self->offset_from_utc, 0 );
619 0         0 [ $_, $dt ] }
620 0         0 grep { $_->min_year <= $year && ( ( ! $_->max_year ) || $_->max_year >= $year ) }
621             $self->rules
622             );
623             }
624              
625             sub until
626             {
627 0     0   0 my $self = shift;
628 0   0     0 my $offset_from_std = shift || $self->offset_from_std;
629              
630 0 0       0 return unless defined $self->until_year;
631              
632 0         0 my $utc =
633             DateTimeX::Lite::OlsonDB::utc_datetime_for_time_spec
634             ( spec => $self->until_time_spec,
635             year => $self->until_year,
636             month => $self->until_month,
637             day => $self->until_day,
638             offset_from_utc => $self->offset_from_utc,
639             offset_from_std => $offset_from_std,
640             );
641              
642 0         0 return $utc;
643             }
644              
645 0     0   0 sub until_year { $_[0]->{until}[0] }
646              
647             sub until_month
648             {
649 0 0   0   0 ( defined $_[0]->{until}[1] ?
650             $DateTimeX::Lite::OlsonDB::MONTHS{ $_[0]->{until}[1] } :
651             1
652             );
653             }
654              
655             sub until_day
656             {
657 0 0   0   0 ( defined $_[0]->{until}[2]
658             ? DateTimeX::Lite::OlsonDB::parse_day_spec
659             ( $_[0]->{until}[2], $_[0]->until_month, $_[0]->until_year )
660             : 1
661             );
662             }
663              
664             sub until_time_spec
665             {
666 0 0   0   0 defined $_[0]->{until}[3] ? $_[0]->{until}[3] : '00:00:00';
667             }
668              
669             sub _first_rule
670             {
671 0     0   0 my $self = shift;
672 0         0 my $last_offset_from_utc = shift;
673 0         0 my $last_offset_from_std = shift;
674              
675 0 0       0 return unless $self->utc_start_datetime;
676 0 0       0 return unless $self->rules;
677              
678 0         0 my $date = $self->utc_start_datetime;
679              
680 0         0 my @rules = $self->rules;
681              
682 0         0 my %possible_rules;
683              
684 0         0 my $year = $date->year;
685 0         0 foreach my $rule (@rules)
686             {
687             # We need to look at what the year _would_ be if we added the
688             # rule's offset to the UTC date. Otherwise we can end up with
689             # a UTC date in year X, and a rule that starts in _local_ year
690             # X + 1, where that rule really does apply to that UTC date.
691 0         0 my $temp_year =
692             $date->clone->add
693             ( seconds => $self->offset_from_utc + $rule->offset_from_std )->year;
694              
695             # Save the highest value
696 0 0       0 $year = $temp_year if $temp_year > $year;
697              
698 0 0       0 next if $rule->min_year > $temp_year;
699              
700 0         0 $possible_rules{$rule} = $rule;
701             }
702              
703 0 0       0 return unless keys %possible_rules;
704              
705 0         0 my $earliest_year = $year - 1;
706 0         0 foreach my $rule (@rules)
707             {
708 0 0       0 $earliest_year = $rule->min_year
709             if $rule->min_year < $earliest_year;
710             }
711              
712             # figure out what date each rule would start on _if_ that rule
713             # were applied to this current observance. this could be a rule
714             # that started much earlier, but is only now active because of an
715             # observance switch. An obnoxious example of this is
716             # America/Phoenix in 1944, which applies the US rule in April,
717             # thus (re-)instating the "war time" rule from 1942. Can you say
718             # ridiculous crack-smoking stupidity?
719 0         0 my @rule_dates;
720 0         0 foreach my $y ( $earliest_year .. $year )
721             {
722             RULE:
723 0         0 foreach my $rule ( values %possible_rules )
724             {
725             # skip rules that can't have applied the year before the
726             # observance started.
727 0 0       0 if ( $rule->min_year > $y )
728             {
729 0         0 warn "Skipping rule beginning in ", $rule->min_year, ". Year is $y.\n"
730             if DateTimeX::Lite::OlsonDB::DEBUG;
731              
732 0         0 next RULE;
733             }
734              
735 0 0 0     0 if ( $rule->max_year && $rule->max_year < $y )
736             {
737 0         0 warn "Skipping rule ending in ", $rule->max_year, ". Year is $y.\n"
738             if DateTimeX::Lite::OlsonDB::DEBUG;
739              
740 0         0 next RULE;
741             }
742              
743 0         0 my $rule_start =
744             $rule->utc_start_datetime_for_year
745             ( $y, $last_offset_from_utc, $last_offset_from_std );
746              
747 0         0 push @rule_dates, [ $rule_start, $rule ];
748             }
749             }
750              
751 0 0       0 return unless @rule_dates;
752              
753 0         0 @rule_dates = sort { $a->[0] <=> $b->[0] } @rule_dates;
  0         0  
754              
755 0         0 warn "Looking for first rule ...\n" if DateTimeX::Lite::OlsonDB::DEBUG;
756 0         0 warn " Observance starts: ", $date->datetime, "\n\n"
757             if DateTimeX::Lite::OlsonDB::DEBUG;
758              
759             # ... look through the rules to see if any are still in
760             # effect at the beginning of the observance
761 0         0 for ( my $x = 0; $x < @rule_dates; $x++ )
762             {
763 0         0 my ( $dt, $rule ) = @{ $rule_dates[$x] };
  0         0  
764 0         0 my ( $next_dt, $next_rule ) =
765 0 0       0 $x < @rule_dates - 1 ? @{ $rule_dates[ $x + 1 ] } : undef;
766              
767 0 0 0     0 next if $next_dt && $next_dt < $date;
768              
769 0         0 warn " This rule starts: ", $dt->datetime, "\n"
770             if DateTimeX::Lite::OlsonDB::DEBUG;
771              
772 0 0 0     0 warn " Next rule starts: ", $next_dt->datetime, "\n"
773             if $next_dt && DateTimeX::Lite::OlsonDB::DEBUG;
774              
775 0 0 0     0 warn " No next rule\n\n"
776             if ! $next_dt && DateTimeX::Lite::OlsonDB::DEBUG;
777              
778 0 0       0 if ( $dt <= $date )
779             {
780 0 0       0 if ($next_dt)
781             {
782 0 0       0 return $rule if $date < $next_dt;
783 0 0       0 return $next_rule if $date == $next_dt;
784             }
785             else
786             {
787 0         0 return $rule;
788             }
789             }
790             }
791              
792 0         0 return;
793             }
794              
795              
796             package DateTimeX::Lite::OlsonDB::Rule;
797              
798 56     56   500 use strict;
  56         175  
  56         2217  
799              
800 56     56   360 use DateTimeX::Lite::Util;
  56         108  
  56         3230  
801 56     56   360 use DateTimeX::Lite::Duration;
  56         184  
  56         51006  
802              
803             sub new
804             {
805 0     0   0 my ($class, %p) = @_;
806 0   0     0 $p{type} ||= undef;
807 0   0     0 $p{letter} ||= '';
808              
809 0         0 my $save = $p{save};
810              
811 0 0       0 if ($save)
812             {
813 0         0 $p{offset_from_std} = DateTimeX::Lite::Util::offset_as_seconds($save);
814             }
815             else
816             {
817 0         0 $p{offset_from_std} = 0;
818             }
819              
820 0         0 return bless \%p, $class;
821             }
822              
823 0     0   0 sub name { $_[0]->{name} }
824 4361     4361   16430 sub offset_from_std { $_[0]->{offset_from_std} }
825 1081     1081   15377 sub letter { $_[0]->{letter} }
826 0     0   0 sub min_year { $_[0]->{from} }
827              
828 0 0   0   0 sub max_year { $_[0]->{to} eq 'only' ? $_[0]->min_year :
    0          
829             $_[0]->{to} eq 'max' ? undef : $_[0]->{to} }
830              
831 0 0   0   0 sub is_infinite { $_[0]->{to} eq 'max' ? 1 : 0 }
832              
833 2236     2236   12010 sub month { $DateTimeX::Lite::OlsonDB::MONTHS{ $_[0]->{in} } }
834 1118     1118   4097 sub on { $_[0]->{on} }
835 1118     1118   5300 sub at { $_[0]->{at} }
836              
837             sub utc_start_datetime_for_year
838             {
839 1118     1118   1869 my $self = shift;
840 1118         2087 my $year = shift;
841 1118         1530 my $offset_from_utc = shift;
842             # should be the offset of the _previous_ rule
843 1118         1355 my $offset_from_std = shift;
844              
845 1118         3204 my $day =
846             DateTimeX::Lite::OlsonDB::parse_day_spec( $self->on, $self->month, $year );
847              
848 1118         5248 my $utc =
849             DateTimeX::Lite::OlsonDB::utc_datetime_for_time_spec
850             ( spec => $self->at,
851             year => $year,
852             month => $self->month,
853             day => $day,
854             offset_from_utc => $offset_from_utc,
855             offset_from_std => $offset_from_std,
856             );
857              
858 1118         3893 return $utc;
859             }
860              
861              
862             package DateTimeX::Lite::OlsonDB::Change;
863              
864 56     56   823 use strict;
  56         129  
  56         57282  
865              
866             sub new
867             {
868 1081     1081   9109 my ($class, %p) = @_;
869 1081   50     3442 $p{rule} ||= undef;
870 1081 50       8922 if ($p{type} !~ /^(?:observance|rule)$/ ) {
871 0         0 die "whoa $p{type}";
872             }
873              
874             # These are almost always mutually exclusive, except when adding
875             # an observance change and the last rule has no offset, but the
876             # new observance has an anonymous rule. In that case, prefer the
877             # offset from std defined in the observance to that in the
878             # previous rule (what a mess!).
879 1081 50       3239 if ( $p{type} eq 'observance' )
880             {
881 0 0       0 $p{offset_from_std} = $p{rule}->offset_from_std if defined $p{rule};
882 0 0       0 $p{offset_from_std} = $p{observance}->offset_from_std
883             if $p{observance}->offset_from_std;
884 0   0     0 $p{offset_from_std} ||= 0;
885             }
886             else
887             {
888 1081         3537 $p{offset_from_std} = $p{observance}->offset_from_std;
889 1081 50       5115 $p{offset_from_std} = $p{rule}->offset_from_std if defined $p{rule};
890             }
891              
892 1081         3581 $p{offset_from_utc} = $p{observance}->offset_from_utc;
893              
894 1081         2408 $p{is_dst} = 0;
895 1081 100 66     4608 $p{is_dst} = 1 if $p{rule} && $p{rule}->offset_from_std;
896 1081 50       2957 $p{is_dst} = 1 if $p{observance}->offset_from_std;
897              
898 1081 50       3952 if ( $p{short_name} =~ m{(\w+)/(\w+)} )
899             {
900 0 0       0 $p{short_name} = $p{is_dst} ? $2 : $1;
901             }
902              
903 1081         14631 return bless \%p, $class;
904             }
905              
906 7015     7015   21200 sub utc_start_datetime { $_[0]->{utc_start_datetime} }
907 1065     1065   4822 sub local_start_datetime { $_[0]->{local_start_datetime} }
908 1065     1065   3527 sub short_name { $_[0]->{short_name} }
909 1065     1065   6886 sub is_dst { $_[0]->{is_dst} }
910 0     0   0 sub observance { $_[0]->{observance} }
911 0     0   0 sub rule { $_[0]->{rule} }
912 3179     3179   8035 sub offset_from_utc { $_[0]->{offset_from_utc} }
913 3179     3179   7701 sub offset_from_std { $_[0]->{offset_from_std} }
914 3179     3179   6425 sub total_offset { $_[0]->offset_from_utc + $_[0]->offset_from_std }
915              
916             sub two_changes_as_span
917             {
918 1065     1065   1536 my ( $c1, $c2, $last_total_offset ) = @_;
919              
920 1065         1138 my ( $utc_start, $local_start );
921              
922 1065 50       1926 if ( defined $c1->utc_start_datetime )
923             {
924 1065         2389 $utc_start = $c1->utc_start_datetime->utc_rd_as_seconds;
925 1065         2178 $local_start = $c1->local_start_datetime->utc_rd_as_seconds;
926             }
927             else
928             {
929 0         0 $utc_start = $local_start = '-inf';
930             }
931              
932 1065         2233 my $utc_end = $c2->utc_start_datetime->utc_rd_as_seconds;
933 1065         2134 my $local_end = $utc_end + $c1->total_offset;
934              
935 1065         2208 return { utc_start => $utc_start,
936             utc_end => $utc_end,
937             local_start => $local_start,
938             local_end => $local_end,
939             short_name => $c1->short_name,
940             offset => $c1->total_offset,
941             is_dst => $c1->is_dst,
942             };
943             }
944              
945             sub _debug_output
946             {
947 0     0     my $self = shift;
948              
949 0           my $obs = $self->observance;
950              
951 0 0         if ( $self->utc_start_datetime )
952             {
953 0           warn " UTC: ", $self->utc_start_datetime->datetime, "\n";
954 0           warn " Local: ", $self->local_start_datetime->datetime, "\n";
955             }
956             else
957             {
958 0           warn " First change (starts at -inf)\n";
959             }
960              
961 0           warn " Short name: ", $self->short_name, "\n";
962 0           warn " UTC offset: ", $obs->offset_from_utc, "\n";
963              
964 0 0 0       if ( $obs->offset_from_std || $self->rule )
965             {
966 0 0         if ( $obs->offset_from_std )
967             {
968 0           warn " Std offset: ", $obs->offset_from_std, "\n";
969             }
970              
971 0 0         if ( $self->rule )
972             {
973 0           warn " Std offset: ", $self->rule->offset_from_std, ' - ',
974             $self->rule->name, " rule\n";
975             }
976             }
977             else
978             {
979 0           warn " Std offset: 0 - no rule\n";
980             }
981              
982 0           warn "\n";
983             }
984              
985             1;
986              
987             __END__