File Coverage

blib/lib/Time/Zone/Olson.pm
Criterion Covered Total %
statement 766 825 92.8
branch 198 258 76.7
condition 46 91 50.5
subroutine 109 110 99.0
pod 16 16 100.0
total 1135 1300 87.3


line stmt bran cond sub pod time code
1             package Time::Zone::Olson;
2              
3 3     3   40539 use 5.010;
  3         8  
4 3     3   10 use strict;
  3         2  
  3         43  
5 3     3   7 use warnings;
  3         6  
  3         54  
6              
7 3     3   1160 use FileHandle();
  3         21524  
  3         55  
8 3     3   12 use File::Spec();
  3         3  
  3         33  
9 3     3   9 use Config;
  3         3  
  3         71  
10 3     3   9 use Carp();
  3         3  
  3         38  
11 3     3   1312 use English qw( -no_match_vars );
  3         7911  
  3         13  
12 3     3   1916 use DirHandle();
  3         968  
  3         41  
13 3     3   1274 use POSIX();
  3         12448  
  3         18672  
14              
15             our $VERSION = '0.12';
16              
17 1948     1948   5169 sub _SIZE_OF_TZ_HEADER { return 44 }
18 1461     1461   2803 sub _SIZE_OF_TRANSITION_TIME_V1 { return 4 }
19 974     974   1607 sub _SIZE_OF_TRANSITION_TIME_V2 { return 8 }
20 974     974   1939 sub _SIZE_OF_TTINFO { return 6 }
21 487     487   962 sub _SIZE_OF_LEAP_SECOND_V1 { return 4 }
22 487     487   797 sub _SIZE_OF_LEAP_SECOND_V2 { return 8 }
23 974     974   1863 sub _PAIR { return 2 }
24 7175     7175   8545 sub _STAT_MTIME_IDX { return 9 }
25 974     974   1716 sub _MAX_LENGTH_FOR_TRAILING_TZ_DEFINITION { return 256 }
26 108     108   74 sub _MONTHS_IN_ONE_YEAR { return 12 }
27 108     108   94 sub _HOURS_IN_ONE_DAY { return 24 }
28 1106     1106   1790 sub _MINUTES_IN_ONE_HOUR { return 60 }
29 28889     28889   22666 sub _SECONDS_IN_ONE_MINUTE { return 60 }
30 8834     8834   6419 sub _SECONDS_IN_ONE_HOUR { return 3_600 }
31 161667     161667   116268 sub _SECONDS_IN_ONE_DAY { return 86_400 }
32 676     676   653 sub _NEGATIVE_ONE { return -1 }
33 2113     2113   2890 sub _LOCALTIME_ISDST_INDEX { return 8 }
34 26     26   40 sub _LOCALTIME_DAY_OF_WEEK_INDEX { return 6 }
35 36446     36446   40592 sub _LOCALTIME_YEAR_INDEX { return 5 }
36 6697     6697   7656 sub _LOCALTIME_MONTH_INDEX { return 4 }
37 14593     14593   14716 sub _LOCALTIME_DAY_INDEX { return 3 }
38 7523     7523   8166 sub _LOCALTIME_HOUR_INDEX { return 2 }
39 26052     26052   25960 sub _LOCALTIME_MINUTE_INDEX { return 1 }
40 844     844   1507 sub _LOCALTIME_SECOND_INDEX { return 0 }
41 3012     3012   3425 sub _LOCALTIME_BASE_YEAR { return 1900 }
42 3869     3869   4565 sub _EPOCH_YEAR { return 1970 }
43 2999     2999   2529 sub _EPOCH_WDAY { return 4 }
44 3025     3025   5725 sub _DAYS_IN_JANUARY { return 31 }
45 1676     1676   2017 sub _DAYS_IN_FEBRUARY_LEAP_YEAR { return 29 }
46 1349     1349   1509 sub _DAYS_IN_FEBRUARY_NON_LEAP { return 28 }
47 3025     3025   3545 sub _DAYS_IN_MARCH { return 31 }
48 3025     3025   3192 sub _DAYS_IN_APRIL { return 30 }
49 3025     3025   2995 sub _DAYS_IN_MAY { return 31 }
50 3025     3025   3446 sub _DAYS_IN_JUNE { return 30 }
51 3025     3025   3843 sub _DAYS_IN_JULY { return 31 }
52 3025     3025   3105 sub _DAYS_IN_AUGUST { return 31 }
53 3025     3025   2913 sub _DAYS_IN_SEPTEMBER { return 30 }
54 3025     3025   3543 sub _DAYS_IN_OCTOBER { return 31 }
55 3025     3025   2753 sub _DAYS_IN_NOVEMBER { return 30 }
56 3025     3025   5944 sub _DAYS_IN_DECEMBER { return 31 }
57 30567     30567   30769 sub _DAYS_IN_A_LEAP_YEAR { return 366 }
58 92492     92492   91123 sub _DAYS_IN_A_NON_LEAP_YEAR { return 365 }
59 26     26   45 sub _LAST_WEEK_VALUE { return 5 }
60 0     0   0 sub _LOCALTIME_WEEKDAY_HIGHEST_VALUE { return 6 }
61 3116     3116   2748 sub _DAYS_IN_ONE_WEEK { return 7 }
62 122978     122978   206865 sub _EVERY_FOUR_HUNDRED_YEARS { return 400 }
63 120745     120745   194638 sub _EVERY_FOUR_YEARS { return 4 }
64 28860     28860   55637 sub _EVERY_ONE_HUNDRED_YEARS { return 100 }
65 445     445   904 sub _DEFAULT_DST_START_HOUR { return 2 }
66 337     337   472 sub _DEFAULT_DST_END_HOUR { return 2 }
67              
68             sub _TIMEZONE_FULL_NAME_REGEX {
69 18869     18869   29151 return qr/(?\w+)(?:\/(?[\w\-\/+]+))?/smx;
70             }
71              
72             my $_default_zoneinfo_directory = '/usr/share/zoneinfo';
73             if ( -e $_default_zoneinfo_directory ) {
74             }
75             else {
76             if ( -e '/usr/lib/zoneinfo' ) {
77             $_default_zoneinfo_directory = '/usr/lib/zoneinfo';
78             }
79             }
80             my $_zonetab_cache = {};
81             my $_tzdata_cache = {};
82              
83 79     79   304 sub _DEFAULT_ZONEINFO_DIRECTORY { return $_default_zoneinfo_directory }
84              
85             sub new {
86 79     79 1 561400 my ( $class, $params ) = @_;
87 79         303 my $self = {};
88 79         139 bless $self, $class;
89             $self->directory( $params->{directory}
90             || $ENV{TZDIR}
91 79   33     700 || _DEFAULT_ZONEINFO_DIRECTORY() );
92 79 100       168 if ( defined $params->{offset} ) {
93 1         3 $self->offset( $params->{offset} );
94             }
95             else {
96 78   66     265 $self->timezone( $params->{timezone} || $ENV{TZ} );
97             }
98 79         206 return $self;
99             }
100              
101             sub directory {
102 7820     7820 1 10248 my ( $self, $new ) = @_;
103 7820         8831 my $old = $self->{directory};
104 7820 100       11896 if ( @_ > 1 ) {
105 79         146 $self->{directory} = $new;
106             }
107 7820         75384 return $old;
108             }
109              
110             sub offset {
111 4166     4166 1 4261 my ( $self, $new ) = @_;
112 4166         4286 my $old = $self->{offset};
113 4166 100       7334 if ( @_ > 1 ) {
114 2         7 $self->{offset} = $new;
115 2         5 delete $self->{tz};
116             }
117 4166         8472 return $old;
118             }
119              
120             sub equiv {
121 3     3 1 19 my ( $self, $time_zone, $from_time ) = @_;
122 3   66     14 $from_time //= time;
123 3         8 my $class = ref $self;
124 3         19 my $compare = $class->new( { 'timezone' => $time_zone } );
125 3         7 my %offsets_compare;
126 3         9 foreach my $transition_time ( $compare->transition_times() ) {
127 319 100       593 if ( $transition_time >= $from_time ) {
128 179         263 $offsets_compare{$transition_time} =
129             $compare->local_offset($transition_time);
130             }
131             }
132 3         88 my %offsets_self;
133 3         8 foreach my $transition_time ( $self->transition_times() ) {
134 426 100       729 if ( $transition_time >= $from_time ) {
135 219         290 $offsets_self{$transition_time} =
136             $self->local_offset($transition_time);
137             }
138             }
139 3 100       127 if ( scalar keys %offsets_compare == scalar keys %offsets_self ) {
140 1         46 foreach my $transition_time ( sort { $a <=> $b } keys %offsets_compare )
  181         115  
141             {
142 43 50 33     157 if (
143             ( defined $offsets_self{$transition_time} )
144             && ( $offsets_self{$transition_time} ==
145             $offsets_compare{$transition_time} )
146             )
147             {
148             }
149             else {
150 0         0 return;
151             }
152             }
153 1         21 return 1;
154             }
155 2         125 return;
156             }
157              
158             sub _timezones {
159 46     46   51 my ($self) = @_;
160 46         100 my $path = File::Spec->catfile( $self->directory(), 'zone.tab' );
161 46 50       235 my $handle = FileHandle->new($path)
162             or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
163 46 50       2853 my @stat = stat $handle
164             or Carp::croak("Failed to stat $path:$EXTENDED_OS_ERROR");
165 46         110 my $last_modified = $stat[ _STAT_MTIME_IDX() ];
166 46 100 66     254 if ( ( $self->{_zonetab_last_modified} )
    100 66        
167             && ( $self->{_zonetab_last_modified} == $last_modified ) )
168             {
169             }
170             elsif (( $_zonetab_cache->{_zonetab_last_modified} )
171             && ( $_zonetab_cache->{_zonetab_last_modified} == $last_modified ) )
172             {
173              
174 2         6 foreach my $key (qw(_zonetab_last_modified _comments _zones)) {
175 6         17 $self->{$key} = $_zonetab_cache->{$key};
176             }
177             }
178             else {
179 3         8 $self->{_zones} = [];
180 3         6 $self->{_comments} = {};
181 3         5832 while ( my $line = <$handle> ) {
182 1320 100       2104 next if ( $line =~ /^[#]/smx );
183 1248         1085 chomp $line;
184 1248         3258 my ( $country_code, $coordinates, $timezone, $comment ) =
185             split /\t/smx, $line;
186 1248         1183 push @{ $self->{_zones} }, $timezone;
  1248         1508  
187 1248         8402 $self->{_comments}->{$timezone} = $comment;
188             }
189 3 50       32 close $handle
190             or Carp::croak("Failed to close $path:$EXTENDED_OS_ERROR");
191 3         5 $self->{_zonetab_last_modified} = $last_modified;
192 3         8 foreach my $key (qw(_zonetab_last_modified _comments _zones)) {
193 9         18 $_zonetab_cache->{$key} = $self->{$key};
194             }
195             }
196 46         42 my @sorted_zones = sort { $a cmp $b } @{ $self->{_zones} };
  139656         132504  
  46         557  
197 46         4744 return @sorted_zones;
198             }
199              
200             sub areas {
201 22     22 1 93546687 my ($self) = @_;
202 22         29 my %areas;
203 22         47 foreach my $timezone ( $self->_timezones() ) {
204 9152         8136 my $timezone_full_name_regex = _TIMEZONE_FULL_NAME_REGEX();
205 9152 50       27638 if ( $timezone =~ /^$timezone_full_name_regex$/smx ) {
206 9152         27980 $areas{ $LAST_PAREN_MATCH{area} } = 1;
207             }
208             else {
209 0         0 Carp::croak(
210             "'$timezone' does not have a valid format for a TZ timezone");
211             }
212             }
213 22         585 my @sorted_areas = sort { $a cmp $b } keys %areas;
  467         352  
214 22         120 return @sorted_areas;
215             }
216              
217             sub locations {
218 22     22 1 1016 my ( $self, $area ) = @_;
219 22 50       48 if ( !length $area ) {
220 0         0 return ();
221             }
222 22         25 my %locations;
223 22         53 foreach my $timezone ( $self->_timezones() ) {
224 9152         8015 my $timezone_full_name_regex = _TIMEZONE_FULL_NAME_REGEX();
225 9152 50       27905 if ( $timezone =~ /^$timezone_full_name_regex$/smx ) {
226 9152 50 66     33451 if ( ( $area eq $LAST_PAREN_MATCH{area} )
227             && ( $LAST_PAREN_MATCH{location} ) )
228             {
229 856         3004 $locations{ $LAST_PAREN_MATCH{location} } = 1;
230             }
231             }
232             else {
233 0         0 Carp::croak(
234             "'$timezone' does not have a valid format for a TZ timezone");
235             }
236             }
237 22         673 my @sorted_locations = sort { $a cmp $b } keys %locations;
  4139         2652  
238 22         217 return @sorted_locations;
239             }
240              
241             sub comment {
242 2     2 1 5 my ( $self, $tz ) = @_;
243 2   33     6 $tz ||= $self->timezone();
244 2         9 $self->_timezones();
245 2 50       16 if ( defined $self->{_comments}->{$tz} ) {
246 2         14 return $self->{_comments}->{$tz};
247             }
248             else {
249 0         0 return;
250             }
251             }
252              
253             sub area {
254 3     3 1 9 my ($self) = @_;
255 3         14 return $self->{area};
256             }
257              
258             sub location {
259 6     6 1 1042 my ($self) = @_;
260 6         30 return $self->{location};
261             }
262              
263             sub timezone {
264 16043     16043 1 1104738 my ( $self, $new ) = @_;
265 16043         17352 my $old = $self->{tz};
266 16043 100       23978 if ( @_ > 1 ) {
267 567 100       1261 if ( defined $new ) {
268 565         1688 my $timezone_full_name_regex = _TIMEZONE_FULL_NAME_REGEX();
269 565 50       7043 if ( $new !~ /^$timezone_full_name_regex$/smx ) {
270 0         0 Carp::croak(
271             "'$new' does not have a valid format for a TZ timezone");
272             }
273 565         6119 $self->{area} = $LAST_PAREN_MATCH{area};
274 565         2273 $self->{location} = $LAST_PAREN_MATCH{location};
275 565         1801 my $path = File::Spec->catfile( $self->directory(), $new );
276 565 50       13108 if ( !-f $path ) {
277 0         0 Carp::croak(
278             "'$new' is not an timezone in the existing Olson database");
279             }
280             }
281 567         1030 $self->{tz} = $new;
282 567         588 delete $self->{offset};
283             }
284 16043         20696 return $old;
285             }
286              
287             sub _is_leap_year {
288 122978     122978   86967 my ( $self, $year ) = @_;
289 122978         72915 my $leap_year;
290 122978 100 100     104907 if (
      66        
291             ( $year % _EVERY_FOUR_HUNDRED_YEARS() == 0 )
292             || ( ( $year % _EVERY_FOUR_YEARS() == 0 )
293             && ( $year % _EVERY_ONE_HUNDRED_YEARS() != 0 ) )
294             )
295             {
296 31081         22507 $leap_year = 1;
297             }
298             else {
299 91897         63381 $leap_year = 0;
300             }
301 122978         112908 return $leap_year;
302             }
303              
304             sub _in_dst_according_to_tz {
305 13     13   20 my ( $self, $check_time, $tz_definition ) = @_;
306              
307 13 50 33     182 if ( ( defined $tz_definition->{start_day} )
      33        
      33        
      33        
      33        
308             && ( defined $tz_definition->{end_day} )
309             && ( defined $tz_definition->{start_week} )
310             && ( defined $tz_definition->{end_week} )
311             && ( defined $tz_definition->{start_month} )
312             && ( defined $tz_definition->{end_month} ) )
313             {
314 13         32 my $check_year =
315             ( $self->_gm_time($check_time) )[ _LOCALTIME_YEAR_INDEX() ] +
316             _LOCALTIME_BASE_YEAR();
317             my $dst_start_time = $self->_get_time_for_wday_week_month_year(
318             $tz_definition->{start_day}, $tz_definition->{start_week},
319             $tz_definition->{start_month}, $check_year
320             ) +
321             ( $tz_definition->{start_hour} *
322             _SECONDS_IN_ONE_MINUTE() *
323             _MINUTES_IN_ONE_HOUR() ) +
324             ( $tz_definition->{start_minute} * _SECONDS_IN_ONE_MINUTE() ) +
325             $tz_definition->{start_second} -
326 13         38 $tz_definition->{std_offset_in_seconds};
327             my $dst_end_time = $self->_get_time_for_wday_week_month_year(
328             $tz_definition->{end_day}, $tz_definition->{end_week},
329             $tz_definition->{end_month}, $check_year
330             ) +
331             ( $tz_definition->{end_hour} *
332             _SECONDS_IN_ONE_MINUTE() *
333             _MINUTES_IN_ONE_HOUR() ) +
334             ( $tz_definition->{end_minute} * _SECONDS_IN_ONE_MINUTE() ) +
335             $tz_definition->{end_second} -
336 13         33 $tz_definition->{dst_offset_in_seconds};
337              
338 13 50       35 if ( $dst_start_time < $dst_end_time ) {
339 0 0 0     0 if ( ( $dst_start_time < $check_time )
340             && ( $check_time < $dst_end_time ) )
341             {
342 0         0 return 1;
343             }
344             }
345             else {
346 13 50 33     92 if ( ( $check_time < $dst_start_time )
347             || ( $dst_end_time < $check_time ) )
348             {
349 13         37 return 1;
350             }
351             }
352             }
353              
354 0         0 return 0;
355             }
356              
357             sub _get_time_for_wday_week_month_year {
358 26     26   50 my ( $self, $wday, $week, $month, $year ) = @_;
359              
360 26         34 my $check_year = _EPOCH_YEAR();
361 26         25 my $time = 0;
362 26         18 my $increment = 0;
363 26         23 my $leap_year = 1;
364 26         45 while ( $check_year < $year ) {
365 1742         1040 $check_year += 1;
366 1742 100       1568 if ( $self->_is_leap_year($check_year) ) {
367 442         282 $leap_year = 1;
368 442         376 $increment = _DAYS_IN_A_LEAP_YEAR() * _SECONDS_IN_ONE_DAY();
369             }
370             else {
371 1300         784 $leap_year = 0;
372 1300         1137 $increment = _DAYS_IN_A_NON_LEAP_YEAR() * _SECONDS_IN_ONE_DAY();
373             }
374 1742         2249 $time += $increment;
375             }
376              
377 26         38 $increment = 0;
378 26         26 my $check_month = 1;
379 26         40 my @days_in_month = $self->_days_in_month($leap_year);
380 26         66 while ( $check_month < $month ) {
381              
382 156         154 $increment = $days_in_month[ $check_month - 1 ] * _SECONDS_IN_ONE_DAY();
383 156         102 $time += $increment;
384 156         187 $check_month += 1;
385             }
386              
387 26 50       40 if ( $week == _LAST_WEEK_VALUE() ) {
388 0         0 $time +=
389             ( $days_in_month[ $check_month - 1 ] - 1 ) * _SECONDS_IN_ONE_DAY();
390 0         0 my $check_day_of_week =
391             ( $self->_gm_time($time) )[ _LOCALTIME_DAY_OF_WEEK_INDEX() ];
392              
393 0         0 while ( $check_day_of_week != $wday ) {
394              
395 0         0 $time -= _SECONDS_IN_ONE_DAY;
396 0         0 $check_day_of_week -= 1;
397 0 0       0 if ( $check_day_of_week < 0 ) {
398 0         0 $check_day_of_week = _LOCALTIME_WEEKDAY_HIGHEST_VALUE();
399             }
400             }
401             }
402             else {
403 26         35 my $check_day_of_week =
404             ( $self->_gm_time($time) )[ _LOCALTIME_DAY_OF_WEEK_INDEX() ];
405 26         29 my $check_week = 1;
406 26         36 $increment = _DAYS_IN_ONE_WEEK() * _SECONDS_IN_ONE_DAY();
407 26         56 while ( $check_week < $week ) {
408 0         0 $check_week += 1;
409 0         0 $time += $increment;
410             }
411              
412 26         49 while ( $check_day_of_week != $wday ) {
413              
414 91         88 $time += _SECONDS_IN_ONE_DAY();
415 91         63 $check_day_of_week += 1;
416 91         81 $check_day_of_week = $check_day_of_week % _DAYS_IN_ONE_WEEK();
417             }
418             }
419              
420 26         63 return $time;
421             }
422              
423             sub _get_tz_offset_according_to_v2_tz_rule {
424 799     799   1048 my ( $self, $time ) = @_;
425 799 50       1113 if ( defined $self->offset() ) {
426 0         0 return ( 0, $self->offset() * _SECONDS_IN_ONE_MINUTE(), q[] );
427             }
428 799         1134 my $tz = $self->timezone();
429 799         678 my ( $isdst, $gmtoff, $abbr );
430 799         941 my $tz_definition = $self->{_tzdata}->{$tz}->{tz_definition};
431 799 50       1247 if ( defined $tz_definition->{std_name} ) {
432 799 100       1220 if ( defined $tz_definition->{dst_name} ) {
433 13 50       35 if ( $self->_in_dst_according_to_tz( $time, $tz_definition ) ) {
434 13         13 $isdst = 1;
435 13         16 $gmtoff = $tz_definition->{dst_offset_in_seconds};
436 13         19 $abbr = $tz_definition->{dst_name};
437             }
438             else {
439 0         0 $isdst = 0;
440 0         0 $gmtoff = $tz_definition->{std_offset_in_seconds};
441 0         0 $abbr = $tz_definition->{std_name};
442             }
443             }
444             else {
445 786         600 $isdst = 0;
446 786         625 $gmtoff = $tz_definition->{std_offset_in_seconds};
447 786         885 $abbr = $tz_definition->{std_name};
448             }
449             }
450 799         2181 return ( $isdst, $gmtoff, $abbr );
451             }
452              
453             sub _negative_gm_time {
454 81     81   82 my ( $self, $time ) = @_;
455 81         204 my $year = _EPOCH_YEAR() - 1;
456 81         88 my $wday = _EPOCH_WDAY() - 1;
457 81         97 my $check_time = 0;
458 81         57 my $number_of_days = 0;
459 81         65 my $leap_year;
460 81         48 YEAR: while (1) {
461 3105         2802 $leap_year = $self->_is_leap_year($year);
462 3105         3024 $number_of_days = $self->_number_of_days_in_a_year($leap_year);
463 3105         2788 my $increment = $number_of_days * _SECONDS_IN_ONE_DAY();
464 3105 100       3909 if ( $check_time - $increment > $time ) {
465 3024         1975 $check_time -= $increment;
466 3024         1771 $wday -= $number_of_days;
467 3024         2372 $year -= 1;
468             }
469             else {
470 81         110 last YEAR;
471             }
472             }
473 81         85 my $yday = $self->_number_of_days_in_a_year($leap_year);
474 81         101 $year -= _LOCALTIME_BASE_YEAR();
475              
476 81         91 my $month = _MONTHS_IN_ONE_YEAR();
477 81         123 my @days_in_month = $self->_days_in_month($leap_year);
478 81         76 MONTH: while (1) {
479              
480 741         495 $number_of_days = $days_in_month[ $month - 1 ];
481 741         620 my $increment = $number_of_days * _SECONDS_IN_ONE_DAY();
482 741 100       871 if ( $check_time - $increment > $time ) {
483 660         509 $check_time -= $increment;
484 660         366 $wday -= $number_of_days;
485 660         398 $yday -= $number_of_days;
486 660         520 $month -= 1;
487             }
488             else {
489 81         78 last MONTH;
490             }
491             }
492 81         50 $month -= 1;
493              
494 81         70 my $day = $days_in_month[$month];
495 81         87 my $increment = _SECONDS_IN_ONE_DAY();
496 81         50 DAY: while (1) {
497 1206 100       1360 if ( $check_time - $increment > $time ) {
498 1125         722 $check_time -= $increment;
499 1125         672 $day -= 1;
500 1125         681 $yday -= 1;
501 1125         767 $wday -= 1;
502             }
503             else {
504 81         79 last DAY;
505             }
506             }
507              
508 81         81 $wday = abs $wday % _DAYS_IN_ONE_WEEK();
509              
510 81         78 my $hour = _HOURS_IN_ONE_DAY() - 1;
511 81         80 $increment = _SECONDS_IN_ONE_HOUR();
512 81         48 HOUR: while (1) {
513 1770 100       1880 if ( $check_time - $increment > $time ) {
514 1689         1105 $check_time -= $increment;
515 1689         1094 $hour -= 1;
516             }
517             else {
518 81         71 last HOUR;
519             }
520             }
521 81         86 my $minute = _MINUTES_IN_ONE_HOUR() - 1;
522 81         77 $increment = _SECONDS_IN_ONE_MINUTE();
523 81         100 MINUTE: while (1) {
524 3318 100       3470 if ( $check_time - $increment > $time ) {
525 3237         2132 $check_time -= $increment;
526 3237         2059 $minute -= 1;
527             }
528             else {
529 81         63 last MINUTE;
530             }
531             }
532 81         77 my $seconds = _SECONDS_IN_ONE_MINUTE() - ( $check_time - $time );
533              
534 81         367 return ( $seconds, $minute, $hour, $day, $month, "$year", $wday, $yday, 0 );
535             }
536              
537             sub _positive_gm_time {
538 2074     2074   2435 my ( $self, $time ) = @_;
539 2074         5783 my $year = _EPOCH_YEAR();
540 2074         2545 my $wday = _EPOCH_WDAY();
541 2074         2002 my $check_time = 0;
542 2074         1680 my $number_of_days = 0;
543 2074         1582 my $leap_year;
544 2074         1527 YEAR: while (1) {
545 84230         81389 $leap_year = $self->_is_leap_year($year);
546 84230         84143 $number_of_days = $self->_number_of_days_in_a_year($leap_year);
547 84230         75866 my $increment = $number_of_days * _SECONDS_IN_ONE_DAY();
548 84230 100       108912 if ( $check_time + $increment <= $time ) {
549 82156         58735 $check_time += $increment;
550 82156         49523 $wday += $number_of_days;
551 82156         64623 $year += 1;
552             }
553             else {
554 2074         3262 last YEAR;
555             }
556             }
557 2074         3161 $year -= _LOCALTIME_BASE_YEAR();
558              
559 2074         1706 my $month = 0;
560 2074         2897 my @days_in_month = $self->_days_in_month($leap_year);
561 2074         2108 my $yday = 0;
562 2074         1551 MONTH: while (1) {
563              
564 15918         10880 $number_of_days = $days_in_month[$month];
565 15918         13346 my $increment = $number_of_days * _SECONDS_IN_ONE_DAY();
566 15918 100       19857 if ( $check_time + $increment <= $time ) {
567 13844         9659 $check_time += $increment;
568 13844         8782 $wday += $number_of_days;
569 13844         7906 $yday += $number_of_days;
570 13844         10697 $month += 1;
571             }
572             else {
573 2074         2797 last MONTH;
574             }
575             }
576 2074         1355 my $day = 1;
577 2074         2097 my $increment = _SECONDS_IN_ONE_DAY();
578 2074         1566 DAY: while (1) {
579 33876 100       37882 if ( $check_time + $increment <= $time ) {
580 31802         21133 $check_time += $increment;
581 31802         18348 $day += 1;
582 31802         17854 $yday += 1;
583 31802         20807 $wday += 1;
584             }
585             else {
586 2074         2442 last DAY;
587             }
588             }
589              
590 2074         2067 $wday = $wday % _DAYS_IN_ONE_WEEK();
591              
592 2074         1653 my $hour = 0;
593 2074         2070 $increment = _SECONDS_IN_ONE_HOUR();
594 2074         1600 HOUR: while (1) {
595 15399 100       18141 if ( $check_time + $increment <= $time ) {
596 13325         9910 $check_time += $increment;
597 13325         8888 $hour += 1;
598             }
599             else {
600 2074         2320 last HOUR;
601             }
602             }
603 2074         1535 my $minute = 0;
604 2074         2064 $increment = _SECONDS_IN_ONE_MINUTE();
605 2074         2300 MINUTE: while (1) {
606 58205 100       63813 if ( $check_time + $increment <= $time ) {
607 56131         39010 $check_time += $increment;
608 56131         36718 $minute += 1;
609             }
610             else {
611 2074         1965 last MINUTE;
612             }
613             }
614 2074         3734 my $seconds = $time - $check_time;
615              
616 2074         11272 return ( $seconds, $minute, $hour, $day, $month, "$year", $wday, $yday, 0 );
617             }
618              
619             sub _gm_time {
620 2155     2155   2545 my ( $self, $time ) = @_;
621 2155         1813 my @gmtime;
622 2155 100       4608 if ( $time < 0 ) {
623 81         162 @gmtime = $self->_negative_gm_time($time);
624             }
625             else {
626 2074         4983 @gmtime = $self->_positive_gm_time($time);
627             }
628 2155 100       3403 if (wantarray) {
629 2152         6482 return @gmtime;
630             }
631             else {
632 3         51 return POSIX::strftime( '%a %b %e %H:%M:%S %Y', @gmtime );
633             }
634             }
635              
636             sub time_local {
637 844     844 1 4573 my ( $self, @localtime ) = @_;
638 844         1033 my $time = 0;
639 844         1095 $localtime[ _LOCALTIME_YEAR_INDEX() ] += _LOCALTIME_BASE_YEAR();
640 844 100       1180 if ( $localtime[ _LOCALTIME_YEAR_INDEX() ] >= _EPOCH_YEAR() ) {
641 817         1708 return $self->_positive_time_local(@localtime);
642             }
643             else {
644 27         43 return $self->_negative_time_local(@localtime);
645             }
646             }
647              
648             sub _positive_time_local {
649 817     817   1705 my ( $self, @localtime ) = @_;
650 817         842 my $check_year = _EPOCH_YEAR();
651 817         1162 my $wday = _EPOCH_WDAY();
652 817         882 my $time = 0;
653 817         621 my $leap_year = 0;
654 817         574 YEAR: while (1) {
655              
656 33710 100       30339 if ( $check_year < $localtime[ _LOCALTIME_YEAR_INDEX() ] ) {
657 32893         31201 $time += $self->_number_of_days_in_a_year($leap_year) *
658             _SECONDS_IN_ONE_DAY();
659 32893         21991 $check_year += 1;
660 32893         30167 $leap_year = $self->_is_leap_year($check_year);
661             }
662             else {
663 817         1568 last YEAR;
664             }
665             }
666              
667 817         651 my $check_month = 0;
668 817         1067 my @days_in_month = $self->_days_in_month($leap_year);
669 817         1399 MONTH: while (1) {
670              
671 6450 100       5763 if ( $check_month < $localtime[ _LOCALTIME_MONTH_INDEX() ] ) {
672 5633         5312 $time += $days_in_month[$check_month] * _SECONDS_IN_ONE_DAY();
673 5633         4140 $check_month += 1;
674             }
675             else {
676 817         778 last MONTH;
677             }
678             }
679 817         667 my $check_day = 1;
680 817         598 DAY: while (1) {
681 14191 100       12060 if ( $check_day < $localtime[ _LOCALTIME_DAY_INDEX() ] ) {
682 13374         11235 $time += _SECONDS_IN_ONE_DAY();
683 13374         9293 $check_day += 1;
684             }
685             else {
686 817         730 last DAY;
687             }
688             }
689              
690 817         955 $wday = $wday % _DAYS_IN_ONE_WEEK();
691              
692 817         572 my $check_hour = 0;
693 817         574 HOUR: while (1) {
694 6933 100       5877 if ( $check_hour < $localtime[ _LOCALTIME_HOUR_INDEX() ] ) {
695 6116         5383 $time += _SECONDS_IN_ONE_HOUR();
696 6116         4326 $check_hour += 1;
697             }
698             else {
699 817         714 last HOUR;
700             }
701             }
702 817         646 my $check_minute = 0;
703 817         511 MINUTE: while (1) {
704 24919 100       19976 if ( $check_minute < $localtime[ _LOCALTIME_MINUTE_INDEX() ] ) {
705 24102         19357 $time += _SECONDS_IN_ONE_MINUTE();
706 24102         18109 $check_minute += 1;
707             }
708             else {
709 817         675 last MINUTE;
710             }
711             }
712 817         1063 $time += $localtime[ _LOCALTIME_SECOND_INDEX() ];
713 817         2259 my ( $isdst, $gmtoff, $abbr ) =
714             $self->_get_isdst_gmtoff_abbr_calculating_for_time_local($time);
715 817         1676 $time -= $gmtoff;
716              
717 817         4581 return $time;
718             }
719              
720             sub _days_in_month {
721 3025     3025   2745 my ( $self, $leap_year ) = @_;
722             return (
723 3025 100       3783 _DAYS_IN_JANUARY(),
724             (
725             $leap_year
726             ? _DAYS_IN_FEBRUARY_LEAP_YEAR()
727             : _DAYS_IN_FEBRUARY_NON_LEAP()
728             ),
729             _DAYS_IN_MARCH(),
730             _DAYS_IN_APRIL(),
731             _DAYS_IN_MAY(),
732             _DAYS_IN_JUNE(),
733             _DAYS_IN_JULY(),
734             _DAYS_IN_AUGUST(),
735             _DAYS_IN_SEPTEMBER(),
736             _DAYS_IN_OCTOBER(),
737             _DAYS_IN_NOVEMBER(),
738             _DAYS_IN_DECEMBER(),
739             );
740             }
741              
742             sub _number_of_days_in_a_year {
743 121317     121317   84426 my ( $self, $leap_year ) = @_;
744 121317 100       117152 if ($leap_year) {
745 30125         27775 return _DAYS_IN_A_LEAP_YEAR();
746             }
747             else {
748 91192         81674 return _DAYS_IN_A_NON_LEAP_YEAR();
749             }
750             }
751              
752             sub _negative_time_local {
753 27     27   47 my ( $self, @localtime ) = @_;
754 27         27 my $check_year = _EPOCH_YEAR() - 1;
755 27         28 my $wday = _EPOCH_WDAY();
756 27         19 my $time = 0;
757 27         18 my $leap_year;
758 27         19 YEAR: while (1) {
759              
760 1035 100       918 if ( $check_year > $localtime[ _LOCALTIME_YEAR_INDEX() ] ) {
761 1008         874 $time -= $self->_number_of_days_in_a_year($leap_year) *
762             _SECONDS_IN_ONE_DAY();
763 1008         627 $check_year -= 1;
764 1008         869 $leap_year = $self->_is_leap_year($check_year);
765             }
766             else {
767 27         58 last YEAR;
768             }
769             }
770              
771 27         29 my $check_month = _MONTHS_IN_ONE_YEAR() - 1;
772 27         45 my @days_in_month = $self->_days_in_month($leap_year);
773 27         25 MONTH: while (1) {
774              
775 247 100       220 if ( $check_month > $localtime[ _LOCALTIME_MONTH_INDEX() ] ) {
776 220         210 $time -= $days_in_month[$check_month] * _SECONDS_IN_ONE_DAY();
777 220         158 $check_month -= 1;
778             }
779             else {
780 27         25 last MONTH;
781             }
782             }
783 27         24 my $check_day = $days_in_month[$check_month];
784 27         17 DAY: while (1) {
785 402 100       308 if ( $check_day > $localtime[ _LOCALTIME_DAY_INDEX() ] ) {
786 375         294 $time -= _SECONDS_IN_ONE_DAY();
787 375         253 $check_day -= 1;
788             }
789             else {
790 27         24 last DAY;
791             }
792             }
793              
794 27         28 $wday = $wday % _DAYS_IN_ONE_WEEK();
795              
796 27         25 my $check_hour = _HOURS_IN_ONE_DAY() - 1;
797 27         22 HOUR: while (1) {
798 590 100       454 if ( $check_hour > $localtime[ _LOCALTIME_HOUR_INDEX() ] ) {
799 563         452 $time -= _SECONDS_IN_ONE_HOUR();
800 563         375 $check_hour -= 1;
801             }
802             else {
803 27         23 last HOUR;
804             }
805             }
806 27         28 my $check_minute = _MINUTES_IN_ONE_HOUR();
807 27         18 MINUTE: while (1) {
808 1133 100       962 if ( $check_minute > $localtime[ _LOCALTIME_MINUTE_INDEX() ] ) {
809 1106         895 $time -= _SECONDS_IN_ONE_MINUTE();
810 1106         870 $check_minute -= 1;
811             }
812             else {
813 27         27 last MINUTE;
814             }
815             }
816 27         29 $time += $localtime[ _LOCALTIME_SECOND_INDEX() ];
817 27         77 my ( $isdst, $gmtoff, $abbr ) =
818             $self->_get_isdst_gmtoff_abbr_calculating_for_time_local($time);
819 27         40 $time -= $gmtoff;
820              
821 27         143 return $time;
822             }
823              
824             sub _get_first_standard_time_type {
825 3353     3353   3875 my ( $self, $tz ) = @_;
826 3353         2375 my $first_standard_time_type;
827 3353 50       6938 if ( defined $self->{_tzdata}->{$tz}->{local_time_types}->[0] ) {
828             $first_standard_time_type =
829 3353         4311 $self->{_tzdata}->{$tz}->{local_time_types}->[0];
830             }
831             FIRST_STANDARD_TIME_TYPE:
832 3353         2573 foreach
833 3353         8014 my $local_time_type ( @{ $self->{_tzdata}->{$tz}->{local_time_types} } )
834             {
835 3353 50       6614 if ( $local_time_type->{isdst} ) {
836             }
837             else {
838 3353         3020 $first_standard_time_type = $local_time_type;
839 3353         4488 last FIRST_STANDARD_TIME_TYPE;
840             }
841             }
842 3353         4521 return $first_standard_time_type;
843             }
844              
845             sub _get_isdst_gmtoff_abbr_calculating_for_time_local {
846 844     844   1158 my ( $self, $time ) = @_;
847 844 100       2668 if ( defined $self->offset() ) {
848 2         8 return ( 0, $self->offset() * _SECONDS_IN_ONE_MINUTE(), q[] );
849             }
850 842         699 my ( $isdst, $gmtoff, $abbr );
851 842         1225 my $tz = $self->timezone();
852 842         1390 $self->_read_tzfile();
853 842         1462 my $first_standard_time_type = $self->_get_first_standard_time_type($tz);
854 842         669 my $transition_index = 0;
855 842         625 my $transition_time_found;
856 842         1447 my $previous_offset = $first_standard_time_type->{gmtoff};
857 842         1602 my $first_transition_time;
858             TRANSITION_TIME:
859              
860 842         1352 foreach my $transition_time_in_gmt ( $self->transition_times() ) {
861              
862 51211 100       67953 if ( !defined $first_transition_time ) {
863 842         2411 $first_transition_time = $transition_time_in_gmt;
864             }
865             my $local_time_index =
866 51211         61297 $self->{_tzdata}->{$tz}->{local_time_indexes}->[$transition_index];
867             my $local_time_type =
868 51211         49648 $self->{_tzdata}->{$tz}->{local_time_types}->[$local_time_index];
869 51211 100       63277 if ( $local_time_type->{gmtoff} < $previous_offset ) {
870 25048 100 100     79283 if (
    100          
871             ( $transition_time_in_gmt > $time - $previous_offset )
872             && ( $transition_time_in_gmt <=
873             $time - $local_time_type->{gmtoff} )
874             )
875             {
876 210         234 $transition_time_found = 1;
877 210         519 last TRANSITION_TIME;
878             }
879             elsif (
880             $transition_time_in_gmt > $time - $local_time_type->{gmtoff} )
881             {
882 276         259 $transition_time_found = 1;
883 276         601 last TRANSITION_TIME;
884             }
885             }
886             else {
887 26163 100       46262 if ( $transition_time_in_gmt > $time - $local_time_type->{gmtoff} )
888             {
889 92         96 $transition_time_found = 1;
890 92         182 last TRANSITION_TIME;
891             }
892             }
893 50633         36854 $transition_index += 1;
894 50633         60761 $previous_offset = $local_time_type->{gmtoff};
895             }
896 842         5826 my $offset_found;
897 842 100 33     8674 if (
    100 66        
898             ( defined $first_transition_time )
899             && ($first_standard_time_type)
900             && ( $time <
901             $first_transition_time + $first_standard_time_type->{gmtoff} )
902             )
903             {
904 1         3 $gmtoff = $first_standard_time_type->{gmtoff};
905 1         2 $isdst = $first_standard_time_type->{isdst};
906 1         3 $abbr = $first_standard_time_type->{abbr};
907 1         1 $offset_found = 1;
908             }
909             elsif ( !$transition_time_found ) {
910 264         659 ( $isdst, $gmtoff, $abbr ) =
911             $self->_get_tz_offset_according_to_v2_tz_rule($time);
912 264 50       523 if ( defined $gmtoff ) {
913 264         296 $offset_found = 1;
914             }
915             }
916 842 100       2479 if ($offset_found) {
    50          
917             }
918             elsif (
919             defined $self->{_tzdata}->{$tz}->{local_time_indexes}
920             ->[ $transition_index - 1 ] )
921             {
922             my $local_time_index = $self->{_tzdata}->{$tz}->{local_time_indexes}
923 577         1029 ->[ $transition_index - 1 ];
924             my $local_time_type =
925 577         910 $self->{_tzdata}->{$tz}->{local_time_types}->[$local_time_index];
926 577         655 $gmtoff = $local_time_type->{gmtoff};
927 577         796 $isdst = $local_time_type->{isdst};
928 577         1200 $abbr = $local_time_type->{abbr};
929             }
930             else {
931 0         0 $gmtoff = $first_standard_time_type->{gmtoff};
932 0         0 $isdst = $first_standard_time_type->{isdst};
933 0         0 $abbr = $first_standard_time_type->{abbr};
934             }
935 842         3782 return ( $isdst, $gmtoff, $abbr );
936             }
937              
938             sub _get_isdst_gmtoff_abbr_calculating_for_local_time {
939 2515     2515   2656 my ( $self, $time ) = @_;
940 2515         1936 my ( $isdst, $gmtoff, $abbr );
941 2515 100       5014 if ( defined $self->offset() ) {
942 4         7 return ( 0, $self->offset() * _SECONDS_IN_ONE_MINUTE(), q[] );
943             }
944 2511         4736 my $tz = $self->timezone();
945 2511         6468 $self->_read_tzfile();
946 2511         2087 my $transition_index = 0;
947 2511         2302 my $transition_time_found;
948             my $first_transition_time;
949             TRANSITION_TIME:
950 2511         4855 foreach my $transition_time_in_gmt ( $self->transition_times() ) {
951              
952 171417 100       217578 if ( !defined $first_transition_time ) {
953 2511         6042 $first_transition_time = $transition_time_in_gmt;
954             }
955 171417 100       231899 if ( $transition_time_in_gmt > $time ) {
956 1976         1705 $transition_time_found = 1;
957 1976         3543 last TRANSITION_TIME;
958             }
959 169441         152714 $transition_index += 1;
960             }
961 2511         22312 my $first_standard_time_type = $self->_get_first_standard_time_type($tz);
962 2511         1981 my $offset_found;
963 2511 100 66     17127 if ( ( defined $first_transition_time )
    100          
964             && ( $time < $first_transition_time ) )
965             {
966 3         10 $gmtoff = $first_standard_time_type->{gmtoff};
967 3         5 $isdst = $first_standard_time_type->{isdst};
968 3         6 $abbr = $first_standard_time_type->{abbr};
969 3         4 $offset_found = 1;
970             }
971             elsif ( !$transition_time_found ) {
972 535         1173 ( $isdst, $gmtoff, $abbr ) =
973             $self->_get_tz_offset_according_to_v2_tz_rule($time);
974 535 50       887 if ( defined $gmtoff ) {
975 535         576 $offset_found = 1;
976             }
977             }
978 2511 100       6625 if ($offset_found) {
    50          
979             }
980             elsif (
981             defined $self->{_tzdata}->{$tz}->{local_time_indexes}
982             ->[ $transition_index - 1 ] )
983             {
984             my $local_time_index = $self->{_tzdata}->{$tz}->{local_time_indexes}
985 1973         3448 ->[ $transition_index - 1 ];
986             my $local_time_type =
987 1973         2911 $self->{_tzdata}->{$tz}->{local_time_types}->[$local_time_index];
988 1973         2360 $gmtoff = $local_time_type->{gmtoff};
989 1973         2204 $isdst = $local_time_type->{isdst};
990 1973         3573 $abbr = $local_time_type->{abbr};
991             }
992             else {
993 0         0 $gmtoff = $first_standard_time_type->{gmtoff};
994 0         0 $isdst = $first_standard_time_type->{isdst};
995 0         0 $abbr = $first_standard_time_type->{abbr};
996             }
997 2511         9294 return ( $isdst, $gmtoff, $abbr );
998             }
999              
1000             sub local_offset {
1001 399     399 1 549 my ( $self, $time ) = @_;
1002 399 50       681 if ( !defined $time ) {
1003 0         0 $time = time;
1004             }
1005 399         519 my ( $isdst, $gmtoff, $abbr ) =
1006             $self->_get_isdst_gmtoff_abbr_calculating_for_local_time($time);
1007 399         702 return int( $gmtoff / _SECONDS_IN_ONE_MINUTE() );
1008             }
1009              
1010             sub local_time {
1011 2116     2116 1 2738354 my ( $self, $time ) = @_;
1012 2116 50       6096 if ( !defined $time ) {
1013 0         0 $time = time;
1014             }
1015              
1016 2116         5980 my ( $isdst, $gmtoff, $abbr ) =
1017             $self->_get_isdst_gmtoff_abbr_calculating_for_local_time($time);
1018 2116         3328 $time += $gmtoff;
1019              
1020 2116 100       3145 if (wantarray) {
1021 2113         3868 my (@local_time) = $self->_gm_time($time);
1022 2113         3108 $local_time[ _LOCALTIME_ISDST_INDEX() ] = $isdst;
1023 2113         17081 return @local_time;
1024             }
1025             else {
1026 3         10 return $self->_gm_time($time);
1027             }
1028             }
1029              
1030             sub transition_times {
1031 3360     3360 1 3166 my ($self) = @_;
1032 3360         4886 my $tz = $self->timezone();
1033 3360         4701 $self->_read_tzfile();
1034 3360         2504 return @{ $self->{_tzdata}->{$tz}->{transition_times} };
  3360         82981  
1035             }
1036              
1037             sub leap_seconds {
1038 416     416 1 143080 my ($self) = @_;
1039 416         869 my $tz = $self->timezone();
1040 416         768 $self->_read_tzfile();
1041             my @leap_seconds =
1042 416         471 sort { $a <=> $b } keys %{ $self->{_tzdata}->{$tz}->{leap_seconds} };
  0         0  
  416         1620  
1043 416         978 return @leap_seconds;
1044             }
1045              
1046             sub _read_header {
1047 974     974   1239 my ( $self, $handle, $path ) = @_;
1048 974         2167 my $result = $handle->read( my $buffer, _SIZE_OF_TZ_HEADER() );
1049 974 50       10957 if ( defined $result ) {
1050 974 50       1397 if ( $result != _SIZE_OF_TZ_HEADER() ) {
1051 0         0 Carp::croak(
1052             "Failed to read entire header from $path. $result bytes were read instead of the expected "
1053             . _SIZE_OF_TZ_HEADER() );
1054             }
1055             }
1056             else {
1057 0         0 Carp::croak("Failed to read header from $path:$EXTENDED_OS_ERROR");
1058             }
1059 974         8324 my ( $magic, $version, $ttisgmtcnt, $ttisstdcnt, $leapcnt, $timecnt,
1060             $typecnt, $charcnt )
1061             = unpack 'A4A1x15N!N!N!N!N!N!', $buffer;
1062 974 50       4525 ( $magic eq 'TZif' ) or Carp::croak("$path is not a TZ file");
1063 974         7146 my $header = {
1064             magic => $magic,
1065             version => $version,
1066             ttisgmtcnt => $ttisgmtcnt,
1067             ttisstdcnt => $ttisstdcnt,
1068             leapcnt => $leapcnt,
1069             timecnt => $timecnt,
1070             typecnt => $typecnt,
1071             charcnt => $charcnt
1072             };
1073              
1074 974         2340 return $header;
1075             }
1076              
1077             sub _read_transition_times {
1078 974     974   1379 my ( $self, $handle, $path, $timecnt, $sizeof_transition_time ) = @_;
1079 974         1663 my $sizeof_transition_times = $timecnt * $sizeof_transition_time;
1080 974         2059 my $result = $handle->read( my $buffer, $sizeof_transition_times );
1081 974 50       6660 if ( defined $result ) {
1082 974 50       2772 if ( $result != $sizeof_transition_times ) {
1083 0         0 Carp::croak(
1084             "Failed to read all the transition times from $path. $result bytes were read instead of the expected "
1085             . $sizeof_transition_times );
1086             }
1087             }
1088             else {
1089 0         0 Carp::croak(
1090             "Failed to read transition times from $path:$EXTENDED_OS_ERROR");
1091             }
1092 974         1034 my @transition_times;
1093 974 100       1275 if ( $sizeof_transition_time == _SIZE_OF_TRANSITION_TIME_V1() ) {
    50          
1094 487         18435 @transition_times = unpack 'l>' . $timecnt, $buffer;
1095             }
1096             elsif ( $sizeof_transition_time == _SIZE_OF_TRANSITION_TIME_V2() ) {
1097 487 50       1178 eval { @transition_times = unpack 'q>' . $timecnt, $buffer; 1; } or do {
  487         12230  
  487         3177  
1098 0         0 require Math::Int64;
1099             @transition_times =
1100 0         0 map { Math::Int64::net_to_int64($_) } unpack '(a8)' . $timecnt,
  0         0  
1101             $buffer;
1102             };
1103             }
1104 974         6116 return \@transition_times;
1105             }
1106              
1107             sub _read_local_time_indexes {
1108 974     974   1491 my ( $self, $handle, $path, $timecnt ) = @_;
1109 974         2331 my $result = $handle->read( my $buffer, $timecnt );
1110 974 50       5867 if ( defined $result ) {
1111 974 50       2247 if ( $result != $timecnt ) {
1112 0         0 Carp::croak(
1113             "Failed to read all the local time indexes from $path. $result bytes were read instead of the expected "
1114             . $timecnt );
1115             }
1116             }
1117             else {
1118 0         0 Carp::croak(
1119             "Failed to read local time indexes from $path:$EXTENDED_OS_ERROR");
1120             }
1121 974         24275 my @local_time_indexes = unpack 'C' . $timecnt, $buffer;
1122 974         7405 return \@local_time_indexes;
1123             }
1124              
1125             sub _read_local_time_types {
1126 974     974   1385 my ( $self, $handle, $path, $typecnt ) = @_;
1127 974         1520 my $sizeof_local_time_types = $typecnt * _SIZE_OF_TTINFO();
1128 974         1920 my $result = $handle->read( my $buffer, $sizeof_local_time_types );
1129 974 50       5747 if ( defined $result ) {
1130 974 50       2187 if ( $result != $sizeof_local_time_types ) {
1131 0         0 Carp::croak(
1132             "Failed to read all the local time types from $path. $result bytes were read instead of the expected "
1133             . $sizeof_local_time_types );
1134             }
1135             }
1136             else {
1137 0         0 Carp::croak(
1138             "Failed to read local time types from $path:$EXTENDED_OS_ERROR");
1139             }
1140 974         758 my @local_time_types;
1141 974         5291 foreach my $local_time_type ( unpack '(a6)' . $typecnt, $buffer ) {
1142 5262         16734 my ( $c1, $c2, $c3 ) = unpack 'a4aa', $local_time_type;
1143 5262         10474 my $gmtoff = unpack 'l>', $c1;
1144 5262         7575 my $isdst = unpack 'C', $c2;
1145 5262         7178 my $abbrind = unpack 'C', $c3;
1146 5262         17184 push @local_time_types,
1147             { gmtoff => $gmtoff, isdst => $isdst, abbrind => $abbrind };
1148             }
1149 974         2907 return \@local_time_types;
1150             }
1151              
1152             sub _read_time_zone_abbreviation_strings {
1153 974     974   1339 my ( $self, $handle, $path, $charcnt ) = @_;
1154 974         1878 my $result = $handle->read( my $time_zone_abbreviation_strings, $charcnt );
1155 974 50       5790 if ( defined $result ) {
1156 974 50       2629 if ( $result != $charcnt ) {
1157 0         0 Carp::croak(
1158             "Failed to read all the time zone abbreviations from $path. $result bytes were read instead of the expected "
1159             . $charcnt );
1160             }
1161             }
1162             else {
1163 0         0 Carp::croak(
1164             "Failed to read time zone abbreviations from $path:$EXTENDED_OS_ERROR"
1165             );
1166             }
1167 974         3145 return $time_zone_abbreviation_strings;
1168             }
1169              
1170             sub _read_leap_seconds {
1171 974     974   1438 my ( $self, $handle, $path, $leapcnt, $sizeof_leap_second ) = @_;
1172 974         1257 my $sizeof_leap_seconds = $leapcnt * _PAIR() * $sizeof_leap_second;
1173 974         2226 my $result = $handle->read( my $buffer, $sizeof_leap_seconds );
1174 974 50       6038 if ( defined $result ) {
1175 974 50       2347 if ( $result != $sizeof_leap_seconds ) {
1176 0         0 Carp::croak(
1177             "Failed to read all the leap seconds from $path. $result bytes were read instead of the expected "
1178             . $sizeof_leap_seconds );
1179             }
1180             }
1181             else {
1182 0         0 Carp::croak(
1183             "Failed to read leap seconds from $path:$EXTENDED_OS_ERROR");
1184             }
1185 974         2498 my @paired_leap_seconds = unpack 'L>' . $leapcnt, $buffer;
1186 974         873 my %leap_seconds;
1187 974         1919 while (@paired_leap_seconds) {
1188 0         0 my $time_leap_second_occurs = shift @paired_leap_seconds;
1189 0         0 my $total_number_of_leap_seconds = shift @paired_leap_seconds;
1190 0         0 $leap_seconds{$time_leap_second_occurs} = $total_number_of_leap_seconds;
1191             }
1192 974         3536 return \%leap_seconds;
1193             }
1194              
1195             sub _read_is_standard_time {
1196 974     974   1424 my ( $self, $handle, $path, $ttisstdcnt ) = @_;
1197 974         1945 my $result = $handle->read( my $buffer, $ttisstdcnt );
1198 974 50       4944 if ( defined $result ) {
1199 974 50       2258 if ( $result != $ttisstdcnt ) {
1200 0         0 Carp::croak(
1201             "Failed to read all the is standard time values from $path. $result bytes were read instead of the expected "
1202             . $ttisstdcnt );
1203             }
1204             }
1205             else {
1206 0         0 Carp::croak(
1207             "Failed to read is standard time values from $path:$EXTENDED_OS_ERROR"
1208             );
1209             }
1210 974         4124 my @is_std_time = unpack 'C' . $ttisstdcnt, $buffer;
1211 974         3089 return \@is_std_time;
1212             }
1213              
1214             sub _read_is_gmt {
1215 974     974   1909 my ( $self, $handle, $path, $ttisgmtcnt ) = @_;
1216 974         1761 my $result = $handle->read( my $buffer, $ttisgmtcnt );
1217 974 50       5114 if ( defined $result ) {
1218 974 50       2136 if ( $result != $ttisgmtcnt ) {
1219 0         0 Carp::croak(
1220             "Failed to read all the is GMT values from $path. $result bytes were read instead of the expected "
1221             . $ttisgmtcnt );
1222             }
1223             }
1224             else {
1225 0         0 Carp::croak(
1226             "Failed to read is GMT values from $path:$EXTENDED_OS_ERROR");
1227             }
1228 974         3783 my @is_gmt_time = unpack 'C' . $ttisgmtcnt, $buffer;
1229 974         3023 return \@is_gmt_time;
1230             }
1231              
1232             sub _read_tz_definition {
1233 487     487   503 my ( $self, $handle, $path ) = @_;
1234 487         819 my $result =
1235             $handle->read( my $buffer, _MAX_LENGTH_FOR_TRAILING_TZ_DEFINITION() );
1236 487 50       4546 if ( defined $result ) {
1237 487 50       698 if ( $result == _MAX_LENGTH_FOR_TRAILING_TZ_DEFINITION() ) {
1238 0         0 Carp::croak(
1239             "The tz defintion at the end of $path could not be read in "
1240             . _MAX_LENGTH_FOR_TRAILING_TZ_DEFINITION()
1241             . ' bytes' );
1242             }
1243             }
1244             else {
1245 0         0 Carp::croak(
1246             "Failed to read tz definition from $path:$EXTENDED_OS_ERROR");
1247             }
1248 487 100       3163 if ( $buffer =~ /^\n([^\n]+)\n*$/smx ) {
1249 486         2029 return $self->_parse_tz_variable( $1, $path );
1250              
1251             }
1252 1         7 return;
1253             }
1254              
1255             sub _parse_tz_variable {
1256 486     486   1583 my ( $self, $tz_variable, $path ) = @_;
1257 486         2172 my $timezone_abbr_name_regex =
1258             qr/(?:[^:\d,+-][^\d,+-]{2,}|[<]\w*[+-]?\d+[>])/smx;
1259 486         2568 my $std_name_regex = qr/(?$timezone_abbr_name_regex)/smx
1260             ; # Name for standard offset from GMT
1261 486         1582 my $std_sign_regex = qr/(?[+-])/smx;
1262 486         866 my $std_hours_regex = qr/(?\d+)/smx;
1263 486         1109 my $std_minutes_regex = qr/(?::(?\d+))/smx;
1264 486         725 my $std_seconds_regex = qr/(?::(?\d+))/smx;
1265 486         2508 my $std_offset_regex =
1266             qr/$std_sign_regex?$std_hours_regex$std_minutes_regex?$std_seconds_regex?/smx
1267             ; # Standard offset from GMT
1268 486         1525 my $dst_name_regex = qr/(?$timezone_abbr_name_regex)/smx
1269             ; # Name for daylight saving offset from GMT
1270 486         828 my $dst_sign_regex = qr/(?[+-])/smx;
1271 486         735 my $dst_hours_regex = qr/(?\d+)/smx;
1272 486         687 my $dst_minutes_regex = qr/(?::(?\d+))/smx;
1273 486         742 my $dst_seconds_regex = qr/(?::(?\d+))/smx;
1274 486         2103 my $dst_offset_regex =
1275             qr/$dst_sign_regex?$dst_hours_regex$dst_minutes_regex?$dst_seconds_regex?/smx
1276             ; # Standard offset from GMT
1277 486         779 my $start_julian_without_feb29_regex =
1278             qr/(?:J(?\d{1,3}))/smx;
1279 486         675 my $start_julian_with_feb29_regex =
1280             qr/(?\d{1,3})/smx;
1281 486         762 my $start_month_regex = qr/(?\d{1,2})/smx;
1282 486         694 my $start_week_regex = qr/(?[1-5])/smx;
1283 486         690 my $start_day_regex = qr/(?[0-6])/smx;
1284 486         1534 my $start_month_week_day_regex =
1285             qr/(?:M$start_month_regex[.]$start_week_regex[.]$start_day_regex)/smx;
1286 486         2074 my $start_date_regex =
1287             qr/(?:$start_julian_without_feb29_regex|$start_julian_with_feb29_regex|$start_month_week_day_regex)/smx;
1288 486         1035 my $start_hour_regex = qr/(?\-?\d+)/smx;
1289 486         1024 my $start_minute_regex = qr/(?::(?\d+))/smx;
1290 486         644 my $start_second_regex = qr/(?::(?\d+))/smx;
1291 486         1573 my $start_time_regex =
1292             qr/[\/]$start_hour_regex$start_minute_regex?$start_second_regex?/smx;
1293 486         1720 my $start_datetime_regex = qr/$start_date_regex(?:$start_time_regex)?/smx;
1294 486         1351 my $end_julian_without_feb29_regex =
1295             qr/(?:J(?\d{1,3}))/smx;
1296 486         781 my $end_julian_with_feb29_regex = qr/(?\d{1,3})/smx;
1297 486         758 my $end_month_regex = qr/(?\d{1,2})/smx;
1298 486         727 my $end_week_regex = qr/(?[1-5])/smx;
1299 486         728 my $end_day_regex = qr/(?[0-6])/smx;
1300 486         1674 my $end_month_week_day_regex =
1301             qr/(?:M$end_month_regex[.]$end_week_regex[.]$end_day_regex)/smx;
1302 486         1688 my $end_date_regex =
1303             qr/(?:$end_julian_without_feb29_regex|$end_julian_with_feb29_regex|$end_month_week_day_regex)/smx;
1304 486         740 my $end_hour_regex = qr/(?\-?\d+)/smx;
1305 486         758 my $end_minute_regex = qr/(?::(?\d+))/smx;
1306 486         721 my $end_second_regex = qr/(?::(?\d+))/smx;
1307 486         1816 my $end_time_regex =
1308             qr/[\/]$end_hour_regex$end_minute_regex?$end_second_regex?/smx;
1309 486         1911 my $end_datetime_regex = qr/$end_date_regex(?:$end_time_regex)?/smx;
1310              
1311 486 50       7957 if ( $tz_variable =~
1312             /^$std_name_regex$std_offset_regex(?:$dst_name_regex(?:$dst_offset_regex)?,$start_datetime_regex,$end_datetime_regex)?$/smx
1313             )
1314             {
1315 486         1519 my $tz_definition = { tz => $tz_variable };
1316 486         1254 foreach my $key (
1317             qw(std_name std_sign std_hours std_minutes std_seconds dst_name dst_sign dst_hours dst_minutes dst_seconds start_julian_without_feb29 end_julian_withou_feb29 start_julian_with_feb29 end_julian_with_feb29 start_month end_month start_week end_week start_day end_day start_hour end_hour start_minute end_minute start_second end_second)
1318             )
1319             {
1320 12636 100       33888 if ( defined $LAST_PAREN_MATCH{$key} ) {
1321 3046         11691 $tz_definition->{$key} = $LAST_PAREN_MATCH{$key};
1322             }
1323             }
1324 486         989 $self->_initialise_undefined_tz_definition_values($tz_definition);
1325             $tz_definition->{std_offset_in_seconds} =
1326 486         812 $self->_std_offset_in_seconds($tz_definition);
1327             $tz_definition->{dst_offset_in_seconds} =
1328 486         761 $self->_dst_offset_in_seconds($tz_definition);
1329 486         5912 return $tz_definition;
1330             }
1331             else {
1332 0         0 Carp::croak(
1333             "Failed to parse the tz defintion of $tz_variable from $path");
1334             }
1335             }
1336              
1337             sub _dst_offset_in_seconds {
1338 486     486   504 my ( $self, $tz_definition ) = @_;
1339 486   50     1560 my $dst_offset_in_seconds = $tz_definition->{dst_seconds} || 0;
1340 486 50       959 if ( defined $tz_definition->{dst_minutes} ) {
1341             $dst_offset_in_seconds +=
1342 0         0 $tz_definition->{dst_minutes} * _SECONDS_IN_ONE_MINUTE();
1343             }
1344 486 100       762 if ( defined $tz_definition->{dst_hours} ) {
1345             $dst_offset_in_seconds +=
1346             $tz_definition->{dst_hours} *
1347 2         9 _MINUTES_IN_ONE_HOUR() *
1348             _SECONDS_IN_ONE_MINUTE();
1349             }
1350 486 100 66     996 if ( ( defined $tz_definition->{dst_sign} )
1351             && ( $tz_definition->{dst_sign} eq q[-] ) )
1352             {
1353             }
1354             else {
1355 484         725 $dst_offset_in_seconds *= _NEGATIVE_ONE();
1356             }
1357 486 100       826 if ( $dst_offset_in_seconds == 0 ) {
1358             $dst_offset_in_seconds = $tz_definition->{std_offset_in_seconds} +
1359 484         699 ( _MINUTES_IN_ONE_HOUR() * _SECONDS_IN_ONE_MINUTE() );
1360             }
1361 486         757 return $dst_offset_in_seconds;
1362             }
1363              
1364             sub _std_offset_in_seconds {
1365 486     486   473 my ( $self, $tz_definition ) = @_;
1366 486   50     2311 my $std_offset_in_seconds = $tz_definition->{std_seconds} || 0;
1367              
1368 486 100       1096 if ( defined $tz_definition->{std_minutes} ) {
1369             $std_offset_in_seconds +=
1370 16         40 $tz_definition->{std_minutes} * _SECONDS_IN_ONE_MINUTE();
1371             }
1372 486 50       910 if ( defined $tz_definition->{std_hours} ) {
1373             $std_offset_in_seconds +=
1374             $tz_definition->{std_hours} *
1375 486         822 _MINUTES_IN_ONE_HOUR() *
1376             _SECONDS_IN_ONE_MINUTE();
1377             }
1378 486 100 66     2446 if ( ( defined $tz_definition->{std_sign} )
1379             && ( $tz_definition->{std_sign} eq q[-] ) )
1380             {
1381             }
1382             else {
1383 192         266 $std_offset_in_seconds *= _NEGATIVE_ONE();
1384             }
1385 486         802 return $std_offset_in_seconds;
1386             }
1387              
1388             sub _initialise_undefined_tz_definition_values {
1389 486     486   562 my ( $self, $tz_definition ) = @_;
1390             $tz_definition->{start_hour} =
1391             defined $tz_definition->{start_hour}
1392             ? $tz_definition->{start_hour}
1393 486 100       1287 : _DEFAULT_DST_START_HOUR();
1394             $tz_definition->{start_minute} =
1395             defined $tz_definition->{start_minute}
1396             ? $tz_definition->{start_minute}
1397 486 100       854 : 0;
1398             $tz_definition->{start_second} =
1399             defined $tz_definition->{start_second}
1400             ? $tz_definition->{start_second}
1401 486 50       830 : 0;
1402             $tz_definition->{end_hour} =
1403             defined $tz_definition->{end_hour}
1404             ? $tz_definition->{end_hour}
1405 486 100       1230 : _DEFAULT_DST_END_HOUR();
1406             $tz_definition->{end_minute} =
1407             defined $tz_definition->{end_minute}
1408             ? $tz_definition->{end_minute}
1409 486 100       1132 : 0;
1410             $tz_definition->{end_second} =
1411             defined $tz_definition->{end_second}
1412             ? $tz_definition->{end_second}
1413 486 50       968 : 0;
1414 486         610 return;
1415             }
1416              
1417             sub _set_abbrs {
1418 974     974   1000 my ( $self, $tz ) = @_;
1419 974         866 my $index = 0;
1420 974         786 foreach
1421 974         2172 my $local_time_type ( @{ $self->{_tzdata}->{$tz}->{local_time_types} } )
1422             {
1423 5262 100       7759 if ( $self->{_tzdata}->{$tz}->{local_time_types}->[ $index + 1 ] ) {
1424             $local_time_type->{abbr} =
1425             substr $self->{_tzdata}->{$tz}->{time_zone_abbreviation_strings},
1426             $local_time_type->{abbrind},
1427             $self->{_tzdata}->{$tz}->{local_time_types}->[ $index + 1 ]
1428 4288         10386 ->{abbrind};
1429             }
1430             else {
1431             $local_time_type->{abbr} =
1432             substr $self->{_tzdata}->{$tz}->{time_zone_abbreviation_strings},
1433 974         2594 $local_time_type->{abbrind};
1434             }
1435 5262         13713 $local_time_type->{abbr} =~ s/\0+$//smx;
1436 5262         5568 $index += 1;
1437             }
1438 974         1233 return;
1439             }
1440              
1441             sub _read_v1_tzfile {
1442 487     487   994 my ( $self, $handle, $path, $header, $tz ) = @_;
1443             $self->{_tzdata}->{$tz}->{transition_times} =
1444             $self->_read_transition_times( $handle, $path, $header->{timecnt},
1445 487         1427 _SIZE_OF_TRANSITION_TIME_V1() );
1446             $self->{_tzdata}->{$tz}->{local_time_indexes} =
1447 487         1180 $self->_read_local_time_indexes( $handle, $path, $header->{timecnt} );
1448             $self->{_tzdata}->{$tz}->{local_time_types} =
1449 487         1325 $self->_read_local_time_types( $handle, $path, $header->{typecnt} );
1450             $self->{_tzdata}->{$tz}->{time_zone_abbreviation_strings} =
1451             $self->_read_time_zone_abbreviation_strings( $handle, $path,
1452 487         1032 $header->{charcnt} );
1453 487         1445 $self->_set_abbrs($tz);
1454             $self->{_tzdata}->{$tz}->{leap_seconds} =
1455             $self->_read_leap_seconds( $handle, $path, $header->{leapcnt},
1456 487         943 _SIZE_OF_LEAP_SECOND_V1() );
1457             $self->{_tzdata}->{$tz}->{is_std} =
1458 487         1194 $self->_read_is_standard_time( $handle, $path, $header->{ttisstdcnt} );
1459             $self->{_tzdata}->{$tz}->{is_gmt} =
1460 487         1236 $self->_read_is_gmt( $handle, $path, $header->{ttisstdcnt} );
1461 487         694 return;
1462             }
1463              
1464             sub _read_v2_tzfile {
1465 487     487   608 my ( $self, $handle, $path, $header, $tz ) = @_;
1466              
1467 487 50 33     13838 if ( ( $header->{version} )
      33        
      33        
1468             && ( $header->{version} >= 2 )
1469             && ( defined $Config{'d_quad'} )
1470             && ( $Config{'d_quad'} eq 'define' ) )
1471             {
1472 487         924 $self->{_tzdata}->{$tz} = {};
1473 487         7374 $header = $self->_read_header( $handle, $path );
1474             $self->{_tzdata}->{$tz}->{transition_times} =
1475             $self->_read_transition_times( $handle, $path, $header->{timecnt},
1476 487         973 _SIZE_OF_TRANSITION_TIME_V2() );
1477             $self->{_tzdata}->{$tz}->{local_time_indexes} =
1478 487         907 $self->_read_local_time_indexes( $handle, $path, $header->{timecnt} );
1479             $self->{_tzdata}->{$tz}->{local_time_types} =
1480 487         824 $self->_read_local_time_types( $handle, $path, $header->{typecnt} );
1481             $self->{_tzdata}->{$tz}->{time_zone_abbreviation_strings} =
1482             $self->_read_time_zone_abbreviation_strings( $handle, $path,
1483 487         809 $header->{charcnt} );
1484 487         964 $self->_set_abbrs($tz);
1485             $self->{_tzdata}->{$tz}->{leap_seconds} =
1486             $self->_read_leap_seconds( $handle, $path, $header->{leapcnt},
1487 487         921 _SIZE_OF_LEAP_SECOND_V2() );
1488             $self->{_tzdata}->{$tz}->{is_std} =
1489             $self->_read_is_standard_time( $handle, $path,
1490 487         789 $header->{ttisstdcnt} );
1491             $self->{_tzdata}->{$tz}->{is_gmt} =
1492 487         914 $self->_read_is_gmt( $handle, $path, $header->{ttisstdcnt} );
1493             $self->{_tzdata}->{$tz}->{tz_definition} =
1494 487         889 $self->_read_tz_definition( $handle, $path );
1495             }
1496 487         1869 return;
1497             }
1498              
1499             sub _read_tzfile {
1500 7129     7129   6436 my ($self) = @_;
1501 7129         8439 my $tz = $self->timezone();
1502 7129         10240 my $path = File::Spec->catfile( $self->directory, $tz );
1503 7129 50       34284 my $handle = FileHandle->new($path)
1504             or Carp::croak("Failed to open $path for reading:$EXTENDED_OS_ERROR");
1505 7129 50       444065 my @stat = stat $handle
1506             or Carp::croak("Failed to stat $path:$EXTENDED_OS_ERROR");
1507 7129         16576 my $last_modified = $stat[ _STAT_MTIME_IDX() ];
1508 7129 100 66     37774 if ( ( $self->{_tzdata}->{$tz}->{last_modified} )
    100 66        
      33        
1509             && ( $self->{_tzdata}->{$tz}->{last_modified} == $last_modified ) )
1510             {
1511             }
1512             elsif (( $_tzdata_cache->{$tz} )
1513             && ( $_tzdata_cache->{$tz}->{last_modified} )
1514             && ( $_tzdata_cache->{$tz}->{last_modified} == $last_modified ) )
1515             {
1516 75         234 $self->{_tzdata}->{$tz} = $_tzdata_cache->{$tz};
1517             }
1518             else {
1519 487         1023 binmode $handle;
1520 487         1355 my $header = $self->_read_header( $handle, $path );
1521 487         1530 $self->_read_v1_tzfile( $handle, $path, $header, $tz );
1522 487         1233 $self->_read_v2_tzfile( $handle, $path, $header, $tz );
1523 487         1214 $self->{_tzdata}->{$tz}->{last_modified} = $last_modified;
1524 487         2272 $_tzdata_cache->{$tz} = $self->{_tzdata}->{$tz};
1525             }
1526 7129 50       40111 close $handle
1527             or Carp::croak("Failed to close $path:$EXTENDED_OS_ERROR");
1528 7129         20032 return;
1529             }
1530              
1531             sub reset_cache {
1532 142     142 1 63277 my ($self) = @_;
1533 142 100       557 if ( ref $self ) {
1534 71         182 foreach my $key (qw(_tzdata _zonetab_last_modified _comments _zones)) {
1535 284         880 $self->{$key} = {};
1536             }
1537             }
1538             else {
1539 71         173 $_tzdata_cache = {};
1540 71         336 $_zonetab_cache = {};
1541             }
1542 142         969 return;
1543             }
1544              
1545             1;
1546             __END__