File Coverage

blib/lib/Time/Zone.pm
Criterion Covered Total %
statement 44 88 50.0
branch 22 60 36.6
condition 8 12 66.6
subroutine 7 13 53.8
pod 0 5 0.0
total 81 178 45.5


line stmt bran cond sub pod time code
1              
2             package Time::Zone;
3              
4              
5             require 5.006;
6              
7             require Exporter;
8 26     26   87586 use Carp;
  26         56  
  26         1917  
9 26     26   157 use strict;
  26         43  
  26         811  
10 26     26   15335 use POSIX qw(tzset tzname);
  26         248230  
  26         227  
11              
12             our $VERSION = '2.35'; # VERSION: generated
13             # ABSTRACT: miscellaneous timezone manipulations routines
14             our @ISA = qw(Exporter);
15             our @EXPORT = qw(tz2zone tz_local_offset tz_offset tz_name);
16              
17             # Parts stolen from code by Paul Foley
18              
19             my %tzn_cache;
20             sub tz2zone (;$$$)
21             {
22 0     0 0 0 my($TZ, $time, $isdst) = @_;
23              
24 0 0       0 $TZ = defined($ENV{'TZ'}) ? ( $ENV{'TZ'} ? $ENV{'TZ'} : 'GMT' ) : ''
    0          
    0          
25             unless $TZ;
26              
27             # Hack to deal with 'PST8PDT' format of TZ
28             # Note that this can't deal with all the esoteric forms, but it
29             # does recognize the most common: [:]STDoff[DST[off][,rule]]
30              
31 0 0       0 if (! defined $isdst) {
32 0         0 my $j;
33 0 0       0 $time = time() unless $time;
34 0         0 ($j, $j, $j, $j, $j, $j, $j, $j, $isdst) = localtime($time);
35             }
36              
37 0 0       0 if (defined $tzn_cache{$TZ}->[$isdst]) {
38 0         0 return $tzn_cache{$TZ}->[$isdst];
39             }
40              
41             # Handle IANA timezone names (e.g., "America/Chicago", "Europe/Paris")
42 0 0       0 if ($TZ =~ m{/}) {
43 0         0 my ($std, $dst_name) = _iana_tzname($TZ);
44 0         0 $tzn_cache{$TZ} = [ $std, $dst_name ];
45 0 0       0 return $isdst ? $dst_name : $std;
46             }
47              
48 0 0       0 if ($TZ =~ /^
49             ( [^:\d+\-,] {3,} )
50             ( [+-] ?
51             \d {1,2}
52             ( : \d {1,2} ) {0,2}
53             )
54             ( [^\d+\-,] {3,} )?
55             /x
56             ) {
57 0 0       0 my $dsttz = defined($4) ? $4 : $1;
58 0 0       0 $TZ = $isdst ? $dsttz : $1;
59 0         0 $tzn_cache{$TZ} = [ $1, $dsttz ];
60             } else {
61 0         0 $tzn_cache{$TZ} = [ $TZ, $TZ ];
62             }
63 0         0 return $TZ;
64             }
65              
66             my @tz_local;
67             sub tz_local_offset (;$)
68             {
69 178     178 0 366 my ($time) = @_;
70              
71 178 100       428 $time = time() unless $time;
72 178         2481 my (@l) = localtime($time);
73 178         332 my $isdst = $l[8];
74              
75 178 100       490 if (defined($tz_local[$isdst])) {
76 171         592 return $tz_local[$isdst];
77             }
78              
79 7         27 $tz_local[$isdst] = &calc_off($time);
80              
81 7         38 return $tz_local[$isdst];
82             }
83              
84             sub calc_off
85             {
86 7     7 0 19 my ($time) = @_;
87              
88 7         67 my (@l) = localtime($time);
89 7         23 my (@g) = gmtime($time);
90              
91 7         13 my $off;
92              
93 7         28 $off = $l[0] - $g[0]
94             + ($l[1] - $g[1]) * 60
95             + ($l[2] - $g[2]) * 3600;
96              
97             # subscript 7 is yday.
98              
99 7 50       28 if ($l[7] == $g[7]) {
    0          
    0          
    0          
100             # done
101             } elsif ($l[7] == $g[7] + 1) {
102 0         0 $off += 86400;
103             } elsif ($l[7] == $g[7] - 1) {
104 0         0 $off -= 86400;
105             } elsif ($l[7] < $g[7]) {
106             # crossed over a year boundary!
107             # localtime is beginning of year, gmt is end
108             # therefore local is ahead
109 0         0 $off += 86400;
110             } else {
111 0         0 $off -= 86400;
112             }
113              
114 7         26 return $off;
115             }
116              
117             # Helper: temporarily set TZ to an IANA name, run a block, restore original TZ.
118             sub _with_iana_tz (&$) {
119 0     0   0 my ($code, $tz) = @_;
120 0         0 my $had_tz = exists $ENV{TZ};
121 0         0 my $saved = $ENV{TZ};
122 0         0 $ENV{TZ} = $tz;
123 0         0 tzset();
124 0         0 my @result = $code->();
125 0 0       0 if ($had_tz) { $ENV{TZ} = $saved } else { delete $ENV{TZ} }
  0         0  
  0         0  
126 0         0 tzset();
127 0         0 return @result;
128             }
129              
130             # Return (std_name, dst_name) for an IANA timezone string.
131             sub _iana_tzname {
132 0     0   0 my ($tz) = @_;
133 0     0   0 my ($std, $dst) = _with_iana_tz { tzname() } $tz;
  0         0  
134 0 0       0 $dst = $std unless defined $dst;
135 0         0 return ($std, $dst);
136             }
137              
138             # Return the UTC offset in seconds for an IANA timezone string at a given time.
139             sub _iana_offset {
140 0     0   0 my ($tz, $time) = @_;
141 0 0       0 $time = time() unless $time;
142 0     0   0 my ($off) = _with_iana_tz { calc_off($time) } $tz;
  0         0  
143 0         0 return $off;
144             }
145              
146             # constants
147              
148             my (%dstZone, %zoneOff, %dstZoneOff, %Zone);
149              
150             CONFIG: {
151             my @dstZone = (
152             "ndt" => -2*3600-1800, # Newfoundland Daylight
153             "brst" => -2*3600, # Brazil Summer Time (East Daylight)
154             "adt" => -3*3600, # Atlantic Daylight
155             "edt" => -4*3600, # Eastern Daylight
156             "cdt" => -5*3600, # Central Daylight
157             "mdt" => -6*3600, # Mountain Daylight
158             "pdt" => -7*3600, # Pacific Daylight
159             "akdt" => -8*3600, # Alaska Daylight
160             "ydt" => -8*3600, # Yukon Daylight
161             "hdt" => -9*3600, # Hawaii Daylight
162             "bst" => +1*3600, # British Summer
163             "cest" => +2*3600, # Central European Summer (preferred)
164             "mest" => +2*3600, # Middle European Summer (alias, kept for compat)
165             "metdst" => +2*3600, # Middle European DST
166             "sst" => +2*3600, # Swedish Summer
167             "fst" => +2*3600, # French Summer
168             "eest" => +3*3600, # Eastern European Summer
169             "msd" => +4*3600, # Moscow Daylight (historical; Russia abolished DST permanently in Oct 2014)
170             "wadt" => +8*3600, # West Australian Daylight
171             "kdt" => +10*3600, # Korean Daylight
172             # "cadt" => +10*3600+1800, # Central Australian Daylight
173             "aedt" => +11*3600, # Eastern Australian Daylight
174             "eadt" => +11*3600, # Eastern Australian Daylight
175             "nzd" => +13*3600, # New Zealand Daylight
176             "nzdt" => +13*3600, # New Zealand Daylight
177             );
178              
179             my @Zone = (
180             "gmt" => 0, # Greenwich Mean
181             "ut" => 0, # Universal (Coordinated)
182             "utc" => 0,
183             "wet" => 0, # Western European
184             "wat" => -1*3600, # West Africa
185             "at" => -2*3600, # Azores
186             "fnt" => -2*3600, # Brazil Time (Extreme East - Fernando Noronha)
187             "brt" => -3*3600, # Brazil Time (East Standard - Brasilia)
188             # For completeness. BST is also British Summer, and GST is also Guam Standard.
189             # "bst" => -3*3600, # Brazil Standard
190             # "gst" => -3*3600, # Greenland Standard
191             "nft" => -3*3600-1800,# Newfoundland
192             "nst" => -3*3600-1800,# Newfoundland Standard
193             "mnt" => -4*3600, # Brazil Time (West Standard - Manaus)
194             "ewt" => -4*3600, # U.S. Eastern War Time
195             "ast" => -4*3600, # Atlantic Standard
196             "est" => -5*3600, # Eastern Standard
197             "act" => -5*3600, # Brazil Time (Extreme West - Acre)
198             "cst" => -6*3600, # Central Standard
199             "mst" => -7*3600, # Mountain Standard
200             "pst" => -8*3600, # Pacific Standard
201             "akst" => -9*3600, # Alaska Standard
202             "yst" => -9*3600, # Yukon Standard
203             "hst" => -10*3600, # Hawaii Standard
204             "cat" => -10*3600, # Central Alaska
205             "ahst" => -10*3600, # Alaska-Hawaii Standard
206             "nt" => -11*3600, # Nome
207             "idlw" => -12*3600, # International Date Line West
208             "cet" => +1*3600, # Central European
209             "mez" => +1*3600, # Central European (German)
210             "ect" => +1*3600, # Central European (French)
211             "met" => +1*3600, # Middle European
212             "mewt" => +1*3600, # Middle European Winter
213             "swt" => +1*3600, # Swedish Winter
214             "set" => +1*3600, # Seychelles
215             "fwt" => +1*3600, # French Winter
216             "eet" => +2*3600, # Eastern Europe, USSR Zone 1
217             "ukr" => +2*3600, # Ukraine
218             "bt" => +3*3600, # Baghdad, USSR Zone 2
219             "msk" => +3*3600, # Moscow (UTC+3; was UTC+4 in 2011-2014 when Russia used permanent DST, reverted Oct 2014)
220             # "it" => +3*3600+1800,# Iran
221             "zp4" => +4*3600, # USSR Zone 3
222             "zp5" => +5*3600, # USSR Zone 4
223             "ist" => +5*3600+1800,# Indian Standard
224             "zp6" => +6*3600, # USSR Zone 5
225             # For completeness. NST is also Newfoundland Stanard, and SST is also Swedish Summer.
226             # "nst" => +6*3600+1800,# North Sumatra
227             # "sst" => +7*3600, # South Sumatra, USSR Zone 6
228             "ict" => +7*3600, # Indochina
229             # "jt" => +7*3600+1800,# Java (3pm in Cronusland!)
230             "ict" => +7*3600, # Indochina Time
231             "wst" => +8*3600, # West Australian Standard
232             "pht" => +8*3600, # Philippine
233             "hkt" => +8*3600, # Hong Kong
234             "pht" => +8*3600, # Philippine Time
235             "cct" => +8*3600, # China Coast, USSR Zone 7
236             "jst" => +9*3600, # Japan Standard, USSR Zone 8
237             "kst" => +9*3600, # Korean Standard
238             # "cast" => +9*3600+1800,# Central Australian Standard
239             "aest" => +10*3600, # Eastern Australian Standard
240             "east" => +10*3600, # Eastern Australian Standard
241             "gst" => +10*3600, # Guam Standard, USSR Zone 9
242             "nzt" => +12*3600, # New Zealand
243             "nzst" => +12*3600, # New Zealand Standard
244             "idle" => +12*3600, # International Date Line East
245             );
246              
247             %Zone = @Zone;
248             %dstZone = @dstZone;
249             %zoneOff = reverse(@Zone);
250             %dstZoneOff = reverse(@dstZone);
251              
252             }
253              
254             sub tz_offset (;$$)
255             {
256 1107     1107 0 1539831 my ($zone, $time) = @_;
257              
258 1107 100       2345 return &tz_local_offset($time) unless($zone);
259              
260 1105 100       2357 $time = time() unless $time;
261 1105         32855 my(@l) = localtime($time);
262 1105         2332 my $dst = $l[8];
263              
264             # Handle IANA timezone names (e.g., "America/Chicago") before lowercasing
265 1105 50       3445 if ($zone =~ m{/}) {
266 0         0 return _iana_offset($zone, $time);
267             }
268              
269 1105         2158 $zone = lc $zone;
270              
271 1105 100 33     5412 if($zone =~ /^(([\-\+])\d\d?)(\d\d)$/) {
    100 66        
    100          
272 15         71 my $v = $2 . $3;
273 15         91 return $1 * 3600 + $v * 60;
274             } elsif (exists $dstZone{$zone} && ($dst || !exists $Zone{$zone})) {
275 14         125 return $dstZone{$zone};
276             } elsif(exists $Zone{$zone}) {
277 1074         12339 return $Zone{$zone};
278             }
279 2         48 undef;
280             }
281              
282             sub tz_name (;$$)
283             {
284 13     13 0 4148 my ($off, $dst) = @_;
285              
286 13 50       37 $off = tz_offset()
287             unless(defined $off);
288              
289 13 50       35 $dst = (localtime(time))[8]
290             unless(defined $dst);
291              
292 13 100 66     72 if (exists $dstZoneOff{$off} && ($dst || !exists $zoneOff{$off})) {
    100 100        
293 3         15 return $dstZoneOff{$off};
294             } elsif (exists $zoneOff{$off}) {
295 6         39 return $zoneOff{$off};
296             }
297             # $off is in seconds; format as +HHMM / -HHMM.
298             # Using abs() for the minutes component handles negative fractional-hour
299             # offsets correctly (e.g. -9000s = -2h30m → "-0230", not "-02-30").
300 4         34 sprintf("%+03d%02d", int($off / 3600), abs(int($off / 60)) % 60);
301             }
302              
303             1;
304              
305             __END__