File Coverage

blib/lib/DateTime/TimeZone/OlsonDB.pm
Criterion Covered Total %
statement 51 130 39.2
branch 13 54 24.0
condition 0 6 0.0
subroutine 8 19 42.1
pod 0 9 0.0
total 72 218 33.0


line stmt bran cond sub pod time code
1             package DateTime::TimeZone::OlsonDB;
2              
3 13     13   70360 use strict;
  13         31  
  13         548  
4 13     13   70 use warnings;
  13         28  
  13         714  
5 13     13   77 use namespace::autoclean;
  13         30  
  13         115  
6              
7             our $VERSION = '2.67';
8              
9 13     13   1311 use DateTime::Duration;
  13         46  
  13         432  
10 13     13   7247 use DateTime::TimeZone::OlsonDB::Rule;
  13         45  
  13         574  
11 13     13   7737 use DateTime::TimeZone::OlsonDB::Zone;
  13         60  
  13         35118  
12              
13             my $x = 1;
14             our %MONTHS = map { $_ => $x++ } qw( Jan Feb Mar Apr May Jun
15             Jul Aug Sep Oct Nov Dec);
16              
17             # 2024b accidentally used "April" instead of "Apr".
18             $MONTHS{April} = $MONTHS{Apr};
19              
20             $x = 1;
21             our %DAYS = map { $_ => $x++ } qw( Mon Tue Wed Thu Fri Sat Sun );
22              
23             our $PLUS_ONE_DAY_DUR = DateTime::Duration->new( days => 1 );
24             our $MINUS_ONE_DAY_DUR = DateTime::Duration->new( days => -1 );
25              
26             sub new {
27 0     0 0 0 my $class = shift;
28              
29 0         0 return bless {
30             rules => {},
31             zones => {},
32             links => {},
33             }, $class;
34             }
35              
36             sub parse_file {
37 0     0 0 0 my $self = shift;
38 0         0 my $file = shift;
39              
40 0 0       0 open my $fh, '<', $file
41             or die "Cannot read $file: $!";
42              
43 0         0 while (<$fh>) {
44 0         0 chomp;
45 0         0 $self->_parse_line($_);
46             }
47              
48 0 0       0 close $fh or die $!;
49             }
50              
51             sub _parse_line {
52 0     0   0 my $self = shift;
53 0         0 my $line = shift;
54              
55 0 0       0 return if $line =~ /^\s+$/;
56 0 0       0 return if $line =~ /^#/;
57              
58             # remove any comments at the end of the line
59 0         0 $line =~ s/\s*#.+$//;
60              
61 0 0 0     0 if ( $self->{in_zone} && $line =~ /^[ \t]/ ) {
62 0         0 $self->_parse_zone( $line, $self->{in_zone} );
63 0         0 return;
64             }
65              
66 0         0 foreach (qw( Rule Zone Link )) {
67 0 0       0 if ( substr( $line, 0, 4 ) eq $_ ) {
68 0         0 my $m = '_parse_' . lc $_;
69 0         0 $self->$m($line);
70             }
71             }
72             }
73              
74             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
75             sub _parse_rule {
76 0     0   0 my $self = shift;
77 0         0 my $rule = shift;
78              
79 0         0 my @items = split /\s+/, $rule, 10;
80              
81 0         0 shift @items;
82 0         0 my $name = shift @items;
83              
84 0         0 my %rule;
85 0         0 @rule{qw( from to type in on at save letter )} = @items;
86 0 0       0 delete $rule{letter} if $rule{letter} eq '-';
87              
88             # As of the 2003a data, there are no rules with a type set
89 0 0       0 delete $rule{type} if $rule{type} eq '-';
90              
91 0         0 push @{ $self->{rules}{$name} },
  0         0  
92             DateTime::TimeZone::OlsonDB::Rule->new( name => $name, %rule );
93              
94 0         0 undef $self->{in_zone};
95             }
96              
97             sub _parse_zone {
98 0     0   0 my $self = shift;
99 0         0 my $zone = shift;
100 0         0 my $name = shift;
101              
102 0 0       0 my $expect = $name ? 5 : 6;
103 0 0       0 my @items = grep { defined && length } split /\s+/, $zone, $expect;
  0         0  
104              
105 0         0 my %obs;
106 0 0       0 unless ($name) {
107 0         0 shift @items; # remove "Zone"
108 0         0 $name = shift @items;
109             }
110              
111 0         0 @obs{qw( gmtoff rules format until )} = @items;
112              
113 0 0       0 if ( $obs{rules} =~ /\d\d?:\d\d/ ) {
114 0         0 $obs{offset_from_std} = delete $obs{rules};
115             }
116             else {
117 0 0       0 delete $obs{rules} if $obs{rules} eq '-';
118             }
119              
120 0 0       0 delete $obs{until} unless defined $obs{until};
121              
122 0         0 push @{ $self->{zones}{$name} }, \%obs;
  0         0  
123              
124 0         0 $self->{in_zone} = $name;
125             }
126              
127             sub _parse_link {
128 0     0   0 my $self = shift;
129 0         0 my $link = shift;
130              
131 0         0 my @items = split /\s+/, $link, 3;
132              
133 0         0 $self->{links}{ $items[2] } = $items[1];
134              
135 0         0 undef $self->{in_zone};
136             }
137             ## use critic
138              
139 0     0 0 0 sub links { %{ $_[0]->{links} } }
  0         0  
140              
141 0     0 0 0 sub zone_names { keys %{ $_[0]->{zones} } }
  0         0  
142              
143             sub zone {
144 0     0 0 0 my $self = shift;
145 0         0 my $name = shift;
146              
147             die "Invalid zone name $name"
148 0 0       0 unless exists $self->{zones}{$name};
149              
150             return DateTime::TimeZone::OlsonDB::Zone->new(
151             name => $name,
152 0         0 observances => $self->{zones}{$name},
153             olson_db => $self,
154             );
155             }
156              
157             sub expanded_zone {
158 0     0 0 0 my $self = shift;
159 0         0 my %p = @_;
160              
161 0   0     0 $p{expand_to_year} ||= (localtime)[5] + 1910;
162              
163 0         0 my $zone = $self->zone( $p{name} );
164              
165 0         0 $zone->expand_observances( $self, $p{expand_to_year} );
166              
167 0         0 return $zone;
168             }
169              
170             sub rules_by_name {
171 0     0 0 0 my $self = shift;
172 0         0 my $name = shift;
173              
174 0 0       0 return unless defined $name;
175              
176             die "Invalid rule name $name"
177 0 0       0 unless exists $self->{rules}{$name};
178              
179 0         0 return @{ $self->{rules}{$name} };
  0         0  
180             }
181              
182             sub parse_day_spec {
183 482     482 0 1231 my ( $day, $month, $year ) = @_;
184              
185 482 50       2811 return ( $month, $day ) if $day =~ /^\d+$/;
186              
187 482 100       3150 if ( $day =~ /^last(\w\w\w)$/ ) {
    50          
188 14         33 my $dow = $DAYS{$1};
189              
190 14         37 my $last_day = DateTime->last_day_of_month(
191             year => $year,
192             month => $month,
193             time_zone => 'floating',
194             );
195              
196 14         287 my $dt = DateTime->new(
197             year => $year,
198             month => $month,
199             day => $last_day->day,
200             time_zone => 'floating',
201             );
202              
203 14         268 while ( $dt->day_of_week != $dow ) {
204 41         1532 $dt -= $PLUS_ONE_DAY_DUR;
205             }
206              
207 14         699 return ( $dt->month, $dt->day );
208             }
209             elsif ( $day =~ /^(\w\w\w)([><])=(\d\d?)$/ ) {
210 468         1821 my $dow = $DAYS{$1};
211              
212 468         2111 my $dt = DateTime->new(
213             year => $year,
214             month => $month,
215             day => $3,
216             time_zone => 'floating',
217             );
218              
219 468 50       14931 my $dur = $2 eq '<' ? $MINUS_ONE_DAY_DUR : $PLUS_ONE_DAY_DUR;
220              
221 468         1534 while ( $dt->day_of_week != $dow ) {
222 1392         73571 $dt += $dur;
223             }
224              
225 468         32611 return ( $dt->month, $dt->day );
226             }
227             else {
228 0         0 die "Invalid on spec for rule: $day\n";
229             }
230             }
231              
232             sub utc_datetime_for_time_spec {
233 482     482 0 2594 my %p = @_;
234              
235             # 'w'all - ignore it, because that's the default
236 482         1655 $p{spec} =~ s/w$//;
237              
238             # 'g'reenwich, 'u'tc, or 'z'ulu
239 482         1933 my $is_utc = $p{spec} =~ s/[guz]$//;
240              
241             # 's'tandard time - ignore DS offset
242 482         1184 my $is_std = $p{spec} =~ s/s$//;
243              
244             ## no critic (NamingConventions::ProhibitAmbiguousNames)
245 482         2331 my ( $hour, $minute, $second ) = split /:/, $p{spec};
246 482 50       1436 $minute = 0 unless defined $minute;
247 482 50       1354 $second = 0 unless defined $second;
248              
249 482         800 my $add_day = 0;
250 482 50       1345 if ( $hour >= 24 ) {
251 0         0 $hour = $hour - 24;
252 0         0 $add_day = 1;
253             }
254              
255 482         745 my $utc;
256 482 100       1031 if ($is_utc) {
257             $utc = DateTime->new(
258             year => $p{year},
259             month => $p{month},
260             day => $p{day},
261 14         42 hour => $hour,
262             minute => $minute,
263             second => $second,
264             time_zone => 'floating',
265             );
266             }
267             else {
268             my $local = DateTime->new(
269             year => $p{year},
270             month => $p{month},
271             day => $p{day},
272 468         2235 hour => $hour,
273             minute => $minute,
274             second => $second,
275             time_zone => 'floating',
276             );
277              
278 468 100       14408 $p{offset_from_std} = 0 if $is_std;
279              
280             my $dur = DateTime::Duration->new(
281 468         2512 seconds => $p{offset_from_utc} + $p{offset_from_std} );
282              
283 468         59802 $utc = $local - $dur;
284             }
285              
286 482 50       33622 $utc->add( days => 1 ) if $add_day;
287              
288 482         2384 return $utc;
289             }
290              
291             1;
292              
293             # ABSTRACT: An object to represent an Olson time zone database
294              
295             __END__
296              
297             =pod
298              
299             =encoding UTF-8
300              
301             =head1 NAME
302              
303             DateTime::TimeZone::OlsonDB - An object to represent an Olson time zone database
304              
305             =head1 VERSION
306              
307             version 2.67
308              
309             =head1 SYNOPSIS
310              
311             none yet
312              
313             =head1 DESCRIPTION
314              
315             This module parses the Olson database time zone definition files and creates
316             various objects representing time zone data.
317              
318             Each time zone is broken down into several parts. The first piece is an
319             observance, which is an offset from UTC and an abbreviation. A single zone may
320             contain many observances, reflecting historical changes in that time zone over
321             time. An observance may also refer to a set of rules.
322              
323             Rules are named, and may apply to many different zones. For example, the "US"
324             rules apply to most of the time zones in the US, unsurprisingly. Rules are
325             made of an offset from standard time and a definition of when that offset
326             changes. Changes can be a one time thing, or they can recur at regular times
327             through a span of years.
328              
329             Each rule may have an associated letter, which is used to generate an
330             abbreviated name for the time zone, along with the offset's abbreviation. For
331             example, if the offset's abbreviation is "C%sT", and the a rule specifies the
332             letter "S", then the abbreviation when that rule is in effect is "CST".
333              
334             =head1 USAGE
335              
336             Not yet documented. This stuff is a mess.
337              
338             =head1 SUPPORT
339              
340             Bugs may be submitted at L<https://github.com/houseabsolute/DateTime-TimeZone/issues>.
341              
342             =head1 SOURCE
343              
344             The source code repository for DateTime-TimeZone can be found at L<https://github.com/houseabsolute/DateTime-TimeZone>.
345              
346             =head1 AUTHOR
347              
348             Dave Rolsky <autarch@urth.org>
349              
350             =head1 COPYRIGHT AND LICENSE
351              
352             This software is copyright (c) 2026 by Dave Rolsky.
353              
354             This is free software; you can redistribute it and/or modify it under
355             the same terms as the Perl 5 programming language system itself.
356              
357             The full text of the license can be found in the
358             F<LICENSE> file included with this distribution.
359              
360             =cut