File Coverage

blib/lib/Time/Str/Util.pm
Criterion Covered Total %
statement 34 79 43.0
branch 8 50 16.0
condition 9 27 33.3
subroutine 9 11 81.8
pod 4 4 100.0
total 64 171 37.4


line stmt bran cond sub pod time code
1             package Time::Str::Util;
2 6     6   299589 use strict;
  6         8  
  6         182  
3 6     6   21 use warnings;
  6         6  
  6         206  
4 6     6   54 use v5.10.1;
  6         16  
5              
6 6     6   22 use Carp qw[croak];
  6         10  
  6         313  
7 6     6   23 use Exporter qw[import];
  6         9  
  6         894  
8              
9             BEGIN {
10 6     6   18 our $VERSION = '0.91';
11 6         13 our @EXPORT_OK = qw[ binary_search
12             lower_bound
13             range_bounds
14             upper_bound ];
15 6         16 our %EXPORT_TAGS = ( all => \@EXPORT_OK );
16 6         9 our @CARP_NOT = qw[Time::Str::PP::Util];
17              
18 6         1082 require Time::Str;
19 6         20 unless (Time::Str::IMPLEMENTATION() eq 'XS') {
20             require Time::Str::PP; Time::Str::PP::Util->import(@EXPORT_OK);
21             }
22            
23 3         2743 push @EXPORT_OK, qw[ find_tzdb_directory
24             find_local_timezone
25             valid_tzdb_timezone
26             valid_posix_timezone ];
27             }
28              
29             {
30             # Directories to probe, in order of preference.
31             # Covers Linux, macOS, FreeBSD, Solaris, and Cygwin.
32             my @TZDB_CANDIDATES = qw(
33             /usr/share/zoneinfo
34             /usr/lib/zoneinfo
35             /usr/share/lib/zoneinfo
36             /etc/zoneinfo
37             /usr/share/zoneinfo.default
38             );
39              
40             sub find_tzdb_directory {
41 6 50   6 1 291036 @_ == 0 or croak q/Usage: find_tzdb_directory()/;
42              
43 9 50 33     6225 return $ENV{TZDIR} if defined $ENV{TZDIR} && -d $ENV{TZDIR};
44              
45 6         11 foreach my $dir (@TZDB_CANDIDATES) {
46 30 50 33     331 return $dir if -d $dir && -f "$dir/UTC";
47             }
48              
49             # macOS: /var/db/timezone/zoneinfo is a symlink to the active version
50 6         11 my $macos = '/var/db/timezone/zoneinfo';
51 6 50 33     60 return $macos if -d $macos && -f "$macos/UTC";
52              
53 6         15 return undef;
54             }
55             }
56              
57             {
58             my $ValidName_Rx = qr{
59             (?(DEFINE)
60             (? [A-Za-z])
61             (? [A-Za-z0-9])
62             (? (?&NameInitial) (?&NameChar)* (?: [_+-] (?&NameChar)+ )* )
63             (? (?&NamePart) (?: [/] (?&NamePart) )* )
64             )
65             \A (?&Name) \z
66             }x;
67              
68             sub valid_tzdb_timezone {
69 34 100   34 1 183969 @_ == 1 or croak q/Usage: valid_tzdb_timezone(string)/;
70 32         41 my ($string) = @_;
71 32   100     841 return (defined $string && $string =~ $ValidName_Rx);
72             }
73             }
74              
75             {
76             my $ValidPOSIX_Rx = qr{
77             (?(DEFINE)
78             (? [A-Za-z]{3,} | [<][A-Za-z0-9+-]{3,}[>] )
79             (? [+-]? [0-9]{1,2} (?: [:][0-9]{2} (?: [:][0-9]{2} )? )? )
80             (?
81             (? M [0-9]{1,2} [.] [0-9] [.] [0-9]
82             | J [0-9]{1,3}
83             | [0-9]{1,3} )
84             )
85              
86             \A
87             (?&Name) (?&Offset)
88             (?:
89             (?&Name) (?: (?&Offset) )?
90             [,] (?&Rule) (?: [/] (?&Time) )?
91             [,] (?&Rule) (?: [/] (?&Time) )?
92             )?
93             \z
94             }x;
95              
96             sub valid_posix_timezone {
97 28 100   28 1 298 @_ == 1 or croak q/Usage: valid_posix_timezone(string)/;
98 26         42 my ($string) = @_;
99 26   100     346 return (defined $string && $string =~ $ValidPOSIX_Rx);
100             }
101             }
102              
103             sub _tzif_from_zoneinfo_path {
104 0     0     my ($path, $tzdb_directory) = @_;
105              
106 0 0         defined $tzdb_directory
107             or return;
108              
109 0           my $pos = rindex $path, 'zoneinfo/';
110 0 0         $pos >= 0
111             or return;
112              
113 0           my $name = substr $path, $pos + length('zoneinfo/');
114              
115 0 0         valid_tzdb_timezone($name)
116             or return;
117              
118 0           my $file = "$tzdb_directory/$name";
119 0 0         -f $file
120             or return;
121              
122 0           return Time::TZif->new(path => $file, name => $name);
123             }
124              
125             sub find_local_timezone {
126 0 0   0 1   @_ <= 1 or croak q/Usage: find_local_timezone([tzdb_directory])/;
127 0           my ($tzdb_directory) = @_;
128              
129 0   0       $tzdb_directory //= find_tzdb_directory();
130              
131 0           require Time::TZif;
132 0           require Time::TZif::POSIX;
133              
134 0 0         if (defined $ENV{TZ}) {
135 0           my $tz = $ENV{TZ};
136              
137             # Convention on BSD/GNU: empty TZ means UTC
138 0 0         unless (length $tz) {
139 0 0 0       if (defined $tzdb_directory && -f "$tzdb_directory/UTC") {
140 0           return Time::TZif->new(
141             path => "$tzdb_directory/UTC",
142             name => 'UTC',
143             );
144             }
145 0           return Time::TZif::POSIX->new(
146             tz_string => 'UTC0',
147             name => 'UTC',
148             );
149             }
150              
151             # Try as a tzdb zone name first (matches libc: file before POSIX rule)
152 0 0 0       if (defined $tzdb_directory && valid_tzdb_timezone($tz)) {
153 0           my $path = "$tzdb_directory/$tz";
154 0 0         if (-f $path) {
155 0           return Time::TZif->new(path => $path, name => $tz);
156             }
157             }
158              
159             # Try as a POSIX TZ rule
160 0 0         if (valid_posix_timezone($tz)) {
161 0           return Time::TZif::POSIX->new(tz_string => $tz);
162             }
163              
164             # Collapse multiple slashes for path handling
165 0           $tz =~ s|/{2,}|/|g;
166              
167             # Absolute or relative path containing zoneinfo/
168 0           my $tzif = _tzif_from_zoneinfo_path($tz, $tzdb_directory);
169 0 0         return $tzif if defined $tzif;
170              
171             # Remove leading colon (implementation-defined path convention)
172 0           $tz =~ s|\A:||;
173              
174             # After colon removal, try as tzdb zone name
175 0 0 0       if (defined $tzdb_directory && valid_tzdb_timezone($tz)) {
176 0           my $path = "$tzdb_directory/$tz";
177 0 0         if (-f $path) {
178 0           return Time::TZif->new(path => $path, name => $tz);
179             }
180             }
181              
182             # Last resort: literal file path
183 0 0         if (-f $tz) {
184 0           return Time::TZif->new(path => $tz);
185             }
186              
187 0           return undef;
188             }
189              
190             # TZ not set: use /etc/localtime
191 0 0         if (defined $tzdb_directory) {
192 0           my $resolved = readlink '/etc/localtime';
193 0 0         if (defined $resolved) {
194 0           my $tzif = _tzif_from_zoneinfo_path($resolved, $tzdb_directory);
195 0 0         return $tzif if defined $tzif;
196             }
197             }
198              
199 0 0         if (-f '/etc/localtime') {
200 0           return Time::TZif->new(path => '/etc/localtime');
201             }
202              
203 0           return undef;
204             }
205              
206             1;