File Coverage

blib/lib/DateTimeX/Lite/TimeZone.pm
Criterion Covered Total %
statement 220 230 95.6
branch 78 96 81.2
condition 27 33 81.8
subroutine 49 50 98.0
pod 19 26 73.0
total 393 435 90.3


line stmt bran cond sub pod time code
1             package DateTimeX::Lite::TimeZone;
2 56     56   311332 use strict;
  56         119  
  56         2006  
3 56     56   307 use warnings;
  56         99  
  56         1814  
4              
5 56     56   371 use Carp ();
  56         102  
  56         1049  
6 56     56   73846 use DateTimeX::Lite::TimeZone::Catalog;
  56         191  
  56         3163  
7 56     56   51753 use DateTimeX::Lite::TimeZone::Floating;
  56         152  
  56         2032  
8 56     56   41979 use DateTimeX::Lite::TimeZone::Local;
  56         174  
  56         2271  
9 56     56   459 use DateTimeX::Lite::TimeZone::OffsetOnly;
  56         257  
  56         2233  
10 56     56   315 use DateTimeX::Lite::TimeZone::UTC;
  56         103  
  56         10538  
11 56     56   43962 use DateTimeX::Lite::OlsonDB;
  56         181  
  56         3921  
12 56     56   55322 use File::ShareDir qw(dist_file);
  56         472259  
  56         7395  
13              
14             our %CachedTimeZones;
15              
16             # the offsets for each span element
17 56     56   573 use constant UTC_START => 0;
  56         150  
  56         4100  
18 56     56   581 use constant UTC_END => 1;
  56         137  
  56         3109  
19 56     56   311 use constant LOCAL_START => 2;
  56         200  
  56         3053  
20 56     56   304 use constant LOCAL_END => 3;
  56         108  
  56         3734  
21 56     56   307 use constant OFFSET => 4;
  56         108  
  56         3253  
22 56     56   295 use constant IS_DST => 5;
  56         134  
  56         2848  
23 56     56   351 use constant SHORT_NAME => 6;
  56         110  
  56         280000  
24              
25             my %SpecialName = map { $_ => 1 } qw( EST MST HST CET EET MET WET EST5EDT CST6CDT MST7MDT PST8PDT );
26              
27             sub load {
28 12001     12001 0 226098 my ($class, %p) = @_;
29              
30 12001         20617 my $name = $p{name};
31 12001         28778 my $conf;
32             my $zone;
33 12001 100       31558 if (defined $name) {
34 12000         21414 my $links = \%DateTimeX::Lite::TimeZone::Catalog::LINKS;
35 12000 100       67046 if ( exists $links->{ $name } ) {
    50          
36 276         694 $name = $links->{ $name };
37             } elsif ( exists $links->{ uc $name } ) {
38 0         0 $name = $links->{ uc $name };
39             }
40             }
41              
42 12001 100       27145 if (defined $name) {
43 12000 100       73278 return $CachedTimeZones{$name} if $CachedTimeZones{$name};
44 1033 100 100     22393 unless ( $name =~ m,/, || $SpecialName{ $name }) {
45 90 100       353 if ( $name eq 'floating' ) {
46 26         387 return $CachedTimeZones{$name} = DateTimeX::Lite::TimeZone::Floating->new;
47             }
48 64 100       509 if ( $name eq 'local' ) {
49 1         9 return $CachedTimeZones{$name} = DateTimeX::Lite::TimeZone::Local->TimeZone();
50             }
51            
52 63 100 100     385 if ( $name eq 'UTC' || $name eq 'Z' ) {
53 39         470 return $CachedTimeZones{$name} = DateTimeX::Lite::TimeZone::UTC->new;
54             }
55              
56 24         233 return $CachedTimeZones{$name} = DateTimeX::Lite::TimeZone::OffsetOnly->new( offset => $name );
57             }
58 943         2734 $conf = _load_time_zone($name);
59             }
60              
61 944 100       10103 if (! $conf) {
62 5 50       29 my $x = $ENV{DATETIMEX_LITE_DEBUG} ? \&Carp::confess : \&Carp::croak ;
63 5   100     992 $x->( "The timezone '" . ($p{name} || 'undef') . "' could not be loaded, or is an invalid name.\n" );
64             }
65              
66 939         13827 $zone = $class->new(%$conf);
67              
68 939 50       4515 if ( $zone->is_olson() ) {
69 0 0       0 my $object_version =
70             $zone->can('olson_version')
71             ? $zone->olson_version()
72             : 'unknown';
73 0         0 my $catalog_version = DateTimeX::Lite::TimeZone::Catalog->OlsonVersion();
74              
75 0 0       0 if ( $object_version ne $catalog_version )
76             {
77 0         0 warn "Loaded $name, which is from an older version ($object_version) of the Olson database than this installation of DateTimeX::Lite::TimeZone ($catalog_version).\n";
78             }
79             }
80              
81 939         4275 $CachedTimeZones{$name} = $zone;
82 939         7862 return $zone;
83             }
84              
85 939     939 1 3324 sub new { my $class = shift; bless { @_ }, $class }
  939         7307  
86              
87             sub _load_time_zone {
88 943     943   2684 my $name = shift;
89 943         2848 my $file = "$name.dat";
90 943         2894 $file =~ s/-/_/g;
91              
92             # Quietly fail here, so we can let the proceeding section croak for us
93 943         1629 eval {
94 943         6905 $file = dist_file( 'DateTimeX-Lite', "DateTimeX/Lite/TimeZone/$file");
95             };
96 943 50       4145738 return $file ? do $file : ();
97             }
98              
99 16     16 0 73 sub rules { $_[0]->{rules} }
100 17     17 0 91 sub max_year { $_[0]->{max_year} }
101 1118     1118 0 5524 sub last_offset { $_[0]->{last_offset} }
102 1081     1081 0 3094 sub last_observance { $_[0]->{last_observance} }
103              
104 940     940 1 12257 sub is_olson { $_[0]->{is_olson} }
105              
106             sub is_dst_for_datetime
107             {
108 56     56 0 80 my $self = shift;
109              
110 56         135 my $span = $self->_span_for_datetime( 'utc', $_[0] );
111              
112 56         400 return $span->[IS_DST];
113             }
114              
115             sub offset_for_datetime
116             {
117 417     417 1 571 my $self = shift;
118              
119 417         1045 my $span = $self->_span_for_datetime( 'utc', $_[0] );
120              
121 416         1314 return $span->[OFFSET];
122             }
123              
124             sub offset_for_local_datetime
125             {
126 269     269 1 398 my $self = shift;
127              
128 269         654 my $span = $self->_span_for_datetime( 'local', $_[0] );
129              
130 261         844 return $span->[OFFSET];
131             }
132              
133             sub short_name_for_datetime
134             {
135 56     56 1 140 my $self = shift;
136              
137 56         179 my $span = $self->_span_for_datetime( 'utc', $_[0] );
138              
139 56         479 return $span->[SHORT_NAME];
140             }
141              
142             sub _span_for_datetime
143             {
144 798     798   1033 my $self = shift;
145 798         1272 my $type = shift;
146 798         948 my $dt = shift;
147              
148 798         1622 my $method = $type . '_rd_as_seconds';
149              
150 798 100       1874 my $end = $type eq 'utc' ? UTC_END : LOCAL_END;
151              
152 798         877 my $span;
153 798         2589 my $seconds = $dt->$method();
154 798 100       1922 if ( $seconds < $self->max_span->[$end] )
155             {
156 783         1869 $span = $self->_spans_binary_search( $type, $seconds );
157             }
158             else
159             {
160 15         70 my $until_year = $dt->utc_year + 1;
161 15         71 $span = $self->_generate_spans_until_match( $until_year, $seconds, $type );
162             }
163              
164             # This means someone gave a local time that doesn't exist
165             # (like during a transition into savings time)
166 798 100       2175 unless ( defined $span )
167             {
168 9         20 my $err = 'Invalid local time for date';
169 9 100       39 $err .= ' ' . $dt->iso8601 if $type eq 'utc';
170 9         42 $err .= " in time zone: " . $self->name;
171 9         19 $err .= "\n";
172              
173 9         129 die $err;
174             }
175              
176 789         1672 return $span;
177             }
178              
179             sub _spans_binary_search
180             {
181 783     783   1075 my $self = shift;
182 783         1147 my ( $type, $seconds ) = @_;
183              
184 783         1425 my ( $start, $end ) = _keys_for_type($type);
185              
186 783         1010 my $min = 0;
187 783         906 my $max = scalar @{ $self->{spans} } + 1;
  783         1617  
188 783         1552 my $i = int( $max / 2 );
189             # special case for when there are only 2 spans
190 783 100 100     3602 $i++ if $max % 2 && $max != 3;
191              
192 783 50       842 $i = 0 if @{ $self->{spans} } == 1;
  783         2351  
193              
194 783         1069 while (1)
195             {
196 5256         8054 my $current = $self->{spans}[$i];
197 5256 100       16182 if ( $seconds < $current->[$start] )
    100          
198             {
199 1988         2129 $max = $i;
200 1988         2890 my $c = int( ( $i - $min ) / 2 );
201 1988   100     3402 $c ||= 1;
202              
203 1988         2169 $i -= $c;
204              
205 1988 50       4072 return if $i < $min;
206             }
207             elsif ( $seconds >= $current->[$end] )
208             {
209 2492         2674 $min = $i;
210 2492         3861 my $c = int( ( $max - $i ) / 2 );
211 2492   100     4511 $c ||= 1;
212              
213 2492         2777 $i += $c;
214              
215 2492 100       5506 return if $i >= $max;
216             }
217             else
218             {
219             # Special case for overlapping ranges because of DST and
220             # other weirdness (like Alaska's change when bought from
221             # Russia by the US). Always prefer latest span.
222 776 100 100     2693 if ( $current->[IS_DST] && $type eq 'local' )
223             {
224 106         237 my $next = $self->{spans}[$i + 1];
225             # Sometimes we will get here and the span we're
226             # looking at is the last that's been generated so far.
227             # We need to try to generate one more or else we run
228             # out.
229 106   66     373 $next ||= $self->_generate_next_span;
230              
231 106 50       241 die "No next span in $self->{max_year}" unless defined $next;
232              
233 106 50 33     683 if ( ( ! $next->[IS_DST] )
      33        
234             && $next->[$start] <= $seconds
235             && $seconds <= $next->[$end]
236             )
237             {
238 0         0 return $next;
239             }
240             }
241              
242 776         1924 return $current;
243             }
244             }
245             }
246              
247             sub _generate_next_span
248             {
249 1     1   3 my $self = shift;
250              
251 1         2 my $last_idx = $#{ $self->{spans} };
  1         3  
252              
253 1         5 my $max_span = $self->max_span;
254              
255             # Kind of a hack, but AFAIK there are no zones where it takes
256             # _more_ than a year for a _future_ time zone change to occur, so
257             # by looking two years out we can ensure that we will find at
258             # least one more span. Of course, I will no doubt be proved wrong
259             # and this will cause errors.
260 1         6 $self->_generate_spans_until_match
261             ( $self->{max_year} + 2, $max_span->[UTC_END] + ( 366 * 86400 ), 'utc' );
262              
263 1         8 return $self->{spans}[ $last_idx + 1 ];
264             }
265              
266             sub _generate_spans_until_match
267             {
268 16     16   30 my $self = shift;
269 16         30 my $generate_until_year = shift;
270 16         27 my $seconds = shift;
271 16         29 my $type = shift;
272              
273 16         30 my @changes;
274 16         30 my @rules = @{ $self->rules };
  16         62  
275 16         66 foreach my $year ( $self->max_year .. $generate_until_year )
276             {
277 559         1769 for ( my $x = 0; $x < @rules; $x++ )
278             {
279 1118         1889 my $last_offset_from_std;
280              
281 1118 50       2756 if ( @rules == 2 )
    0          
282             {
283 1118 100       5212 $last_offset_from_std =
284             $x ? $rules[0]->offset_from_std : $rules[1]->offset_from_std;
285             }
286             elsif ( @rules == 1 )
287             {
288 0         0 $last_offset_from_std = $rules[0]->offset_from_std;
289             }
290             else
291             {
292 0         0 my $count = scalar @rules;
293 0         0 die "Cannot generate future changes for zone with $count infinite rules\n";
294             }
295              
296 1118         2123 my $rule = $rules[$x];
297              
298 1118         3683 my $next =
299             $rule->utc_start_datetime_for_year
300             ( $year, $self->last_offset, $last_offset_from_std );
301              
302             # don't bother with changes we've seen already
303 1118 100       5753 next if $next->utc_rd_as_seconds < $self->max_span->[UTC_END];
304              
305 1081         3811 my $last_observance = $self->last_observance;
306 1081         9852 push @changes,
307             DateTimeX::Lite::OlsonDB::Change->new
308             ( type => 'rule',
309             utc_start_datetime => $next,
310             local_start_datetime =>
311             $next +
312             DateTimeX::Lite::Duration->new
313             ( seconds => $last_observance->total_offset +
314             $rule->offset_from_std ),
315             short_name =>
316             sprintf( $last_observance->format, $rule->letter ),
317             observance => $last_observance,
318             rule => $rule,
319             );
320             }
321             }
322              
323 16         58 $self->{max_year} = $generate_until_year;
324              
325 16         205 my @sorted = sort { $a->utc_start_datetime <=> $b->utc_start_datetime } @changes;
  1910         5654  
326              
327 16         78 my ( $start, $end ) = _keys_for_type($type);
328              
329 16         33 my $match;
330 16         76 for ( my $x = 1; $x < @sorted; $x++ )
331             {
332 1065 100       4088 my $last_total_offset =
333             $x == 1 ? $self->max_span->[OFFSET] : $sorted[ $x - 2 ]->total_offset;
334              
335 1065         3142 my $span =
336             DateTimeX::Lite::OlsonDB::Change::two_changes_as_span
337             ( @sorted[ $x - 1, $x ], $last_total_offset );
338              
339 1065         2181 $span = _span_as_array($span);
340              
341 1065         2125 push @{ $self->{spans} }, $span;
  1065         2088  
342              
343 1065 100 100     7114 $match = $span
344             if $seconds >= $span->[$start] && $seconds < $span->[$end];
345             }
346              
347 16         10139 return $match;
348             }
349              
350 1933     1933 0 9418 sub max_span { $_[0]->{spans}[-1] }
351              
352             sub _keys_for_type
353             {
354 799 100   799   2532 $_[0] eq 'utc' ? ( UTC_START, UTC_END ) : ( LOCAL_START, LOCAL_END );
355             }
356              
357             sub _span_as_array
358             {
359 1065     1065   1396 [ @{ $_[0] }{ qw( utc_start utc_end local_start local_end offset is_dst short_name ) } ];
  1065         4340  
360             }
361              
362 33278     33278 1 114032 sub is_floating { 0 }
363              
364 772     772 1 4131 sub is_utc { 0 }
365              
366 55     55 1 301 sub has_dst_changes { $_[0]->{has_dst_changes} }
367              
368 1110     1110 1 471823 sub name { $_[0]->{name} }
369 0     0 1 0 sub category { (split /\//, $_[0]->{name}, 2)[0] }
370              
371             sub is_valid_name
372             {
373 13     13 1 16594 my $tz;
374             {
375 13         24 local $@;
  13         17  
376 13         24 $tz = eval { $_[0]->load( name => $_[1] ) };
  13         49  
377             }
378              
379 13 100 66     169 return $tz && $tz->isa('DateTimeX::Lite::TimeZone') ? 1 : 0
380             }
381              
382             #
383             # Functions
384             #
385             sub offset_as_seconds {
386             {
387 224     224 1 4909 local $@;
  224         277  
388 224 100       562 shift if eval { $_[0]->isa('DateTimeX::Lite::TimeZone') };
  224         2196  
389             }
390 224         778 DateTimeX::Lite::Util::offset_as_seconds(@_);
391             }
392              
393             sub offset_as_string {
394             {
395 74     74 1 4905 local $@;
  74         112  
396 74 100       122 shift if eval { $_[0]->isa('DateTimeX::Lite::TimeZone') };
  74         664  
397             }
398 74         286 DateTimeX::Lite::Util::offset_as_string(@_);
399             }
400              
401             # These methods all operate on data contained in the DateTime/TimeZone/Catalog.pm file.
402              
403             sub all_names {
404 4 100   4 1 17642 return wantarray ? @DateTimeX::Lite::TimeZone::Catalog::ALL : [@DateTimeX::Lite::TimeZone::Catalog::ALL];
405             }
406              
407             sub categories {
408             return wantarray
409             ? @DateTimeX::Lite::TimeZone::Catalog::CATEGORY_NAMES
410 2 100   2 1 3579 : [@DateTimeX::Lite::TimeZone::Catalog::CATEGORY_NAMES];
411             }
412              
413             sub links
414             {
415             return
416 5 100   5 1 2616 wantarray ? %DateTimeX::Lite::TimeZone::Catalog::LINKS : {%DateTimeX::Lite::TimeZone::Catalog::LINKS};
417             }
418              
419             sub names_in_category
420             {
421 3 100   3 1 1214 shift if $_[0]->isa('DateTimeX::Lite::TimeZone');
422 3 50       10 return unless exists $DateTimeX::Lite::TimeZone::Catalog::CATEGORIES{ $_[0] };
423              
424             return
425             wantarray
426 3 100       9 ? @{ $DateTimeX::Lite::TimeZone::Catalog::CATEGORIES{ $_[0] } }
  2         48  
427             : [ $DateTimeX::Lite::TimeZone::Catalog::CATEGORIES{ $_[0] } ];
428             }
429              
430             sub countries
431             {
432             wantarray
433 1 50   1 1 931 ? ( sort keys %DateTimeX::Lite::TimeZone::Catalog::ZONES_BY_COUNTRY )
434             : [ sort keys %DateTimeX::Lite::TimeZone::Catalog::ZONES_BY_COUNTRY ];
435             }
436              
437             sub names_in_country
438             {
439 5 100   5 1 3863 shift if $_[0]->isa('DateTimeX::Lite::TimeZone');
440              
441 5 50       17 return unless exists $DateTimeX::Lite::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] };
442              
443             return
444             wantarray
445 5 100       15 ? @{ $DateTimeX::Lite::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] } }
  4         25  
446             : $DateTimeX::Lite::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] };
447             }
448              
449             1;
450              
451             __END__