File Coverage

blib/lib/Date/Tie.pm
Criterion Covered Total %
statement 246 255 96.4
branch 141 156 90.3
condition 51 61 83.6
subroutine 12 12 100.0
pod 0 1 0.0
total 450 485 92.7


line stmt bran cond sub pod time code
1             #
2             # Copyright (c) 2001,2002 Flavio Soibelmann Glock.
3             # All rights reserved.
4             # This program is free software; you can redistribute it and/or
5             # modify it under the same terms as Perl itself.
6              
7             package Date::Tie;
8 5     5   3364 use strict;
  5         7  
  5         185  
9 5     5   4835 use Tie::Hash;
  5         5116  
  5         132  
10 5     5   64 use Exporter;
  5         13  
  5         181  
11 5     5   4856 use POSIX; # floor()
  5         38134  
  5         36  
12 5     5   24023 use Time::Local qw( timegm );
  5         24787  
  5         623  
13 5     5   38 use vars qw( @ISA %Frac %Max %Min %Mult $Infinity $VERSION $Resolution );
  5         11  
  5         16515  
14             @ISA = qw( Tie::StdHash );
15             $VERSION = '0.20';
16             $Infinity = 999_999_999_999;
17              
18             %Frac = ( frac_hour => 60 * 60, frac_minute => 60,
19             frac_second => 1, frac_epoch => 1 );
20              
21             %Mult = ( day => 24 * 60 * 60, hour => 60 * 60, minute => 60,
22             second => 1, epoch => 1,
23             monthday => 24 * 60 * 60, weekday => 24 * 60 * 60, yearday => 24 * 60 * 60,
24             week => 7 * 24 * 60 * 60, tzhour => 60 * 60, tzminute => 60 );
25              
26             %Max = ( year => $Infinity, yearday => 365, month => 12,
27             monthday => 28, day => 28, week => 52,
28             weekday => 7, hour => 23, minute => 59,
29             second => 59, weekyear => $Infinity, epoch => $Infinity );
30              
31             %Min = ( year => -$Infinity, yearday => 1, month => 1,
32             monthday => 1, day => 1, week => 1,
33             weekday => 1, hour => 0, minute => 0,
34             second => 0, weekyear => -$Infinity, epoch => -$Infinity );
35              
36             sub STORE {
37 218     218   557 my ($self, $key, $value) = @_;
38 218         223 my ($delta);
39 218 50       470 $key = 'day' if $key eq 'monthday';
40 218         528 $value =~ tr/\,/\./; # translate comma to dot
41 218         364 $value += 0;
42              
43 218         504 my $i_value = POSIX::floor($value); # get integer part
44              
45 218 100       687 if ($value =~ /e/i) {
46             # SCIENTIFIC NOTATION!
47 2         26 ($value) = sprintf("%0.20f", $value) =~ /(.*?)0*$/; # without trailing zeroes
48             }
49              
50             # TODO: make 3 separate 'if's
51 218 100 100     1292 if (($i_value != $value) or ($key eq 'frac') or (exists $Frac{$key})) {
      100        
52             # has fractional part
53              
54 48         230 my ($frac) = $value =~ /\.(.*)/; # get fractional part as an 'integer'
55 48 100       107 $frac = 0 unless defined $frac; # or get zero
56              
57 48 100       96 if ($key eq 'frac') {
58 31 100 100     149 if (($value < 0) or ($value >= 1)) {
59             # fractional overflow
60 15         32 $self->STORE('second', $self->FETCH('second') + $i_value);
61             # make sure frac is a positive number
62 15         28 my $len_frac = length($frac);
63 15 100 100     44 $frac = ('1' . '0' x $len_frac ) - $frac if ($value < 0) and ($frac != 0);
64 15         38 $frac = '0' x ($len_frac - length($frac)) . $frac;
65             }
66 31         66 $self->{frac} = '.' . $frac;
67 31         74 return;
68             }
69 17 100       39 if (exists $Frac{$key}) {
70              
71 16         60 my ($not_frac_key) = $key =~ /frac_(.*)/;
72 16         37 $self->STORE($not_frac_key, $i_value);
73 16         32 my $mult = $Frac{$key};
74              
75             # make sure frac is a positive number
76 16         21 my $len_frac = length($frac);
77 16 100 66     57 $frac = ('1' . '0' x $len_frac ) - $frac if ($value < 0) and ($frac != 0);
78 16         39 $frac = '0' x ($len_frac - length($frac)) . $frac;
79 16         27 $frac = '.' . $frac;
80              
81             # round last digit if the number is a fraction of '3': 1/3 1/9 ...
82             # 9 digits is enough for nano-second resolution...
83 16 100       39 if (length($frac) > 9) {
84 2         7 my ($last_frac, $last_mult) = ($frac, $mult);
85              
86 2         7 foreach(0..3) {
87              
88 5 50       11 if ( $_ == 3 ) {
89             # give-up rounding --- go back to original values ???
90 0         0 ($frac, $mult) = ($last_frac, $last_mult);
91 0         0 last;
92             }
93              
94             # 000.$
95 5 100       34 if ($frac =~ /000.$/) {
    100          
96 1         8 $frac =~ s/.$//;
97 1         3 last;
98             }
99             elsif ($frac =~ /999.$/) {
100 1         6 my ($zeroes, $digit) = $frac =~ /\.(.*)(.)$/;
101 1         6 $digit = '0.' . '0' x (length($zeroes)-1) . sprintf("%02d", 10 - $digit);
102 1         3 $frac += $digit;
103 1         2 last;
104             }
105             else {
106 3         10 $frac *= 3;
107 3         6 $mult /= 3;
108             }
109              
110             } # foreach
111             } # round 1/3 1/9 ...
112              
113             # zero units below this
114 16 100       35 if ($not_frac_key eq 'hour') {
115 4         17 $self->STORE('minute', 0);
116 4         9 $self->STORE('second', 0);
117             }
118 16 100       31 if ($not_frac_key eq 'minute') {
119 6         13 $self->STORE('second', 0);
120             }
121 16         63 $self->STORE('frac', $mult * $frac);
122              
123 16         53 return;
124             }
125              
126             # error - this unit does not allow a fractional part
127 1         2 $key =~ s/frac_//;
128 1         5 $value = POSIX::floor($value + 0.5); # round to integer
129              
130             } # end: has fractional part
131              
132 171 100       324 if ($key eq 'tz') {
133             # note: this must be "int", not "floor" !!
134 12         73 STORE($self, 'tzminute', $value - 40 * int($value / 100)); # 60 - 100 !
135 12         33 return;
136             }
137 159 100 100     662 if (($key eq 'tzhour') or ($key eq 'tzminute')) {
138 16 100       40 $self->{tz100} = 0 unless exists $self->{tz100};
139 16 100       31 if ($key eq 'tzhour') {
140 3         6 $delta = $value * 3600 - $self->{tz100};
141             }
142             else {
143 13         35 $delta = $value * 60 - $self->{tz100};
144             }
145 16         29 $self->{tz100} += $delta;
146              
147 16         34 $self->STORE('epoch', FETCH($self, 'epoch') + $delta);
148 16         31 return;
149             }
150              
151 143 100       271 if ($key eq 'utc_epoch') {
152 1   50     7 %{$self} = ( utc_epoch => $value, epoch => $value + ($self->{tz100} || 0),
  1         6  
153             tz100 => $self->{tz100}, frac => $self->{frac} );
154 1         5 return;
155             }
156              
157 142 100       246 if ($key eq 'epoch') {
158 27         61 $self->{epoch} = $value;
159             # remove all other keys (now invalid)
160 27         53 %{$self} = ( epoch => $self->{epoch}, tz100 => $self->{tz100}, frac => $self->{frac} );
  27         161  
161 27         69 return;
162             }
163 115 100       215 if ($key eq 'month') {
164 21 50 66     119 return if (exists $self->{month}) and ($self->{month} == $value);
165 21 100       57 $self->FETCH('day') unless exists $self->{day}; # save 'day' before deleting epoch!
166              
167 21         47 delete $self->{epoch};
168 21         29 delete $self->{utc_epoch};
169 21         33 delete $self->{weekday};
170 21         31 delete $self->{yearday};
171 21         25 delete $self->{week};
172 21         25 delete $self->{weekyear};
173              
174 21 100 100     175 if (($value >= $Min{$key}) and ($value <= $Max{$key})) {
175 17         51 $self->{$key} = $value;
176             }
177             else {
178 4         47 $value -= 1;
179 4         13 $self->{year} += POSIX::floor( $value / 12);
180 4         7 $self->{month} = 1 + $value % 12;
181             }
182              
183 21 100       60 if ($self->{day} >= 29) {
184 5         16 my ($tmp_month) = $self->FETCH('month');
185             # check for day overflow
186 5         31 $self->STORE('day',$self->{day});
187 5         12 $self->FETCH('month');
188 5 50       18 if ($tmp_month != $self->{month}) {
189 0         0 $self->STORE('day', 0);
190             }
191             }
192              
193 21         60 return;
194             }
195 94 100       184 if ($key eq 'year') {
196 13 50 66     69 return if (exists $self->{year}) and ($self->{year} == $value);
197 13 100       41 $self->FETCH('day') unless exists $self->{day}; # save 'day' before deleting epoch!
198              
199 13         26 delete $self->{epoch};
200 13         23 delete $self->{utc_epoch};
201 13         26 delete $self->{weekday};
202 13         29 delete $self->{yearday};
203 13         19 delete $self->{week};
204 13         62 delete $self->{weekyear};
205              
206 13         30 $self->{year} = $value;
207              
208 13 100       35 if ($self->{day} >= 29) {
209 2         6 my ($tmp_month) = $self->FETCH('month');
210             # check for day overflow
211 2         8 $self->STORE('day',$self->{day});
212 2         9 $self->FETCH('month');
213 2 50       9 if ($tmp_month != $self->{month}) {
214 0         0 $self->STORE('day', 0);
215             }
216             }
217              
218 13         39 return;
219             }
220 81 100       159 if ($key eq 'weekyear') {
221 1 50       5 my $week = exists $self->{week} ? $self->{week} : FETCH($self, 'week');
222 1 50       5 my $weekyear = exists $self->{weekyear} ? $self->{weekyear} : FETCH($self, 'weekyear');
223 1 50       4 FETCH($self, 'epoch') unless exists $self->{epoch};
224 1         4 $self->{epoch} += 52 * $Mult{week} * ($value - $weekyear);
225 1         3 %{$self} = ( epoch => $self->{epoch}, tz100 => $self->{tz100}, frac => $self->{frac} );
  1         7  
226 1         3 my $week2 = FETCH($self, 'week');
227 1         5 while ($week2 != $week) {
228 0         0 STORE($self, 'week', $week2 + ($value <=> $weekyear) );
229 0         0 $week2 = FETCH($self, 'week');
230             }
231 1         4 return;
232             }
233             # all other keys
234              
235 80 100       180 unless ( exists $self->{$key} ) {
236 5         12 FETCH($self, $key);
237             }
238 80         125 $delta = $value - $self->{$key};
239              
240 80 100 100     763 if (($value >= $Min{$key}) and ($value <= $Max{$key}) and
      100        
      66        
      100        
241             ($key ne 'weekday') and ($key ne 'yearday') and ($key ne 'week')) {
242 55 100       123 if (exists $self->{epoch}) {
243 33         68 $self->{epoch} += $delta * $Mult{$key};
244 33         72 delete $self->{utc_epoch};
245             }
246 55         108 $self->{$key} = $value;
247             # update dependencies
248 55 100       115 if ($key eq 'day') {
249 10         15 delete $self->{weekday};
250 10         16 delete $self->{yearday};
251 10         20 delete $self->{weekyear};
252 10         21 delete $self->{week};
253             }
254 55         123 return;
255             }
256             # handle overflow
257             # init epoch key
258 25 100       66 unless ( exists $self->{epoch} ) {
259 11         25 FETCH($self, 'epoch');
260             }
261 25         55 $self->{epoch} += $delta * $Mult{$key};
262             # remove all other keys (now invalid)
263 25         49 %{$self} = ( epoch => $self->{epoch}, tz100 => $self->{tz100}, frac => $self->{frac} );
  25         141  
264 25         64 return;
265             }
266              
267             sub FETCH {
268 600     600   1332 my ($self, $key) = @_;
269 600         599 my ($value);
270 600 50       1109 $key = 'day' if $key eq 'monthday';
271              
272 600 100       998 if ($key eq 'frac') {
273 50         253 return $self->{frac};
274             }
275 550 100       1210 if (exists $Frac{$key}) {
276 18         83 my ($not_frac_key) = $key =~ /frac_(.*)/;
277 18         55 $value = $self->FETCH($not_frac_key);
278 18 100       66 return $value . $self->{frac} if ($Frac{$key} == 1); # no rounding
279             # units below this
280 15 100       32 if ($not_frac_key eq 'hour') {
281 10         21 $value += $self->FETCH('minute') / 60.0;
282 10         24 $value += $self->FETCH('second') / 3600.0;
283             }
284 15 100       34 if ($not_frac_key eq 'minute') {
285 5         10 $value += $self->FETCH('second') / 60.0;
286             }
287 15         32 $value += $self->FETCH('frac') / $Frac{$key};
288 15 100 66     84 $value = '0' . $value if ($value >= 0) and ($value < 10); # format output
289 15 100       82 $value = $value . '.0' unless ($value =~ /\./); # format output
290 15         128 return $value;
291             }
292 532 100       913 if ($key eq 'tz') {
293 13         24 my ($h, $m) = (FETCH($self, 'tzhour'), FETCH($self, 'tzminute'));
294 13 100       35 my $s = $self->{tz100} < 0 ? '-' : '+';
295 13         79 return $s . substr($h,1,2) . sprintf("%02d", abs($m));
296             }
297 519 100       891 if ($key eq 'tzhour') {
298 19 100       48 my $s = $self->{tz100} < 0 ? '-' : '+';
299             # note: this must be "int", not "floor" !!
300 19         32 $value = int($self->{tz100} / 3600);
301 19         278 return $s . sprintf("%02d", abs($value));
302             }
303 500 100       829 if ($key eq 'tzminute') {
304 17 100       39 my $s = $self->{tz100} < 0 ? '-' : '+';
305             # note: this must be "int", not "floor" !!
306 17         33 $value = int( ( $self->{tz100} - 3600 * int($self->{tz100} / 3600) ) / 60 );
307 17         69 return $s . sprintf("%02d", abs($value));
308             }
309              
310 483 100       1002 unless (exists($self->{$key}) ) {
311             # create key if possible
312 78 100 100     328 if (( $key eq 'epoch') or not exists $self->{epoch} ) {
313 26         31 my ($year, $month, $day, $hour, $minute, $second);
314 26 100       79 $day = exists $self->{day} ? $self->{day} : 1;
315 26 100       80 $month = exists $self->{month} ? $self->{month} - 1 : 0;
316 26 100       63 $year = exists $self->{year} ? $self->{year} - 1900 : 0;
317 26 100       63 $hour = exists $self->{hour} ? $self->{hour} : 0;
318 26 100       67 $minute = exists $self->{minute} ? $self->{minute} : 0;
319 26 100       74 $second = exists $self->{second} ? $self->{second} : 0;
320              
321             # TODO: test for month overflow (error when using perl 5.8.0)
322             # Day '31' out of range 1..30 at lib/Date/Tie.pm line 383
323 26         44 eval { $self->{epoch} = timegm( $second, $minute, $hour, $day, $month, $year ); };
  26         94  
324             # warn $@ if $@;
325 26         1488 while ($@ =~ /Day \'\d+\' out of range/ ) {
326 6         555 $day = $self->{day}--;
327 6         12 eval { $self->{epoch} = timegm( $second, $minute, $hour, $day, $month, $year ); };
  6         19  
328             # warn $@ if $@;
329             }
330 26 100       224 return $self->{epoch} if $key eq 'epoch'; # ???
331             }
332 56         427 ( $self->{second}, $self->{minute}, $self->{hour},
333             $self->{day}, $self->{month}, $self->{year},
334             $self->{weekday}, $self->{yearday} ) = gmtime($self->{epoch});
335 56         132 $self->{year} += 1900;
336 56         71 $self->{month}++;
337 56 100       129 $self->{weekday} = 7 unless $self->{weekday};
338 56         82 $self->{yearday}++;
339 56   100     228 $self->{utc_epoch} = $self->{epoch} - ( $self->{tz100} || 0 );
340              
341 56 100 100     271 if ( $key eq 'week' || $key eq 'weekyear' ) {
342 6         25 $self->{week} = POSIX::floor( ($self->{yearday} - $self->{weekday} + 10) / 7 );
343 6 100       20 if ($self->{yearday} > 361) {
344             # find out next year's jan-04 weekday
345 2         10 tie my %tmp, 'Date::Tie', year => ($self->{year} + 1), month => '01', day => '04';
346             # jan-04 weekday: 1 2 3 4 5 6 7
347 2         7 my @wk1 = qw( 29 32 32 32 32 31 30 29 );
348 2         12 my $last_day = $wk1[$tmp{weekday}];
349 2 50       25 $self->{week} = 1 if ($self->{day} >= $last_day);
350             }
351 6 50       16 if ( $self->{week} == 0 ) {
352 0         0 my @t = gmtime( timegm( 0,0,0, 31,11,($self->{year} - 1) ) );
353 0         0 $self->{week} = POSIX::floor( ($t[7] - $t[6] + 11) / 7 );
354             }
355              
356 6         14 $self->{weekyear} = $self->{year};
357 6 100 66     30 $self->{weekyear}++ if ($self->{week} < 2) and ($self->{month} > 10);
358 6 50 33     25 $self->{weekyear}-- if ($self->{week} > 50) and ($self->{month} < 2);
359             }
360             } # create keys
361              
362 461         664 $value = $self->{$key};
363 461 100       837 return $value if $key eq 'weekday';
364 454 100       777 return $value if $key eq 'utc_epoch';
365 449 50       2778 return sprintf("%02d", $value) if $key ne 'yearday';
366 0         0 return sprintf("%03d", $value);
367             }
368              
369             sub TIEHASH {
370 18     18   140 my $self = bless {}, shift;
371 18         25 my ($tmp1, $tmp2);
372 18         161 $self->{frac} = '.0';
373 18         37 $self->{tz100} = 0;
374 18         136 ( $self->{second}, $self->{minute}, $self->{hour},
375             $self->{day}, $self->{month}, $self->{year},
376             $self->{weekday}, $self->{yearday} ) = gmtime();
377 18         48 $self->{year} += 1900;
378 18         28 $self->{month}++;
379 18 50       50 $self->{weekday} = 7 unless $self->{weekday};
380 18         25 $self->{yearday}++;
381 18         70 while ($#_ > -1) {
382 34         69 ($tmp1, $tmp2) = (shift, shift);
383 34         63 STORE ($self, $tmp1, $tmp2);
384             }
385 18         73 return $self;
386             }
387              
388             sub new {
389 6     6 0 21 my $class = shift;
390 6         8 my @parent;
391 6 100       29 @parent = %$class if ref $class;
392 6         16 push @parent, @_;
393 6   66     31 my $self = bless {}, ref $class || $class;
394 6         19 tie %$self, 'Date::Tie', @parent;
395 6         21 return $self;
396             }
397              
398             # FIRSTKEY added to support recommended assignment order: set timezone, then epoch and fractional seconds
399             # tie my %b, 'Date::Tie', tz => $d{tz}, epoch => $d{epoch}, frac => $d{frac};
400              
401             sub FIRSTKEY {
402 4     4   12 my ($self) = @_;
403 4         17 return 'tz';
404             }
405              
406             sub NEXTKEY {
407 12     12   15 my ($self, $lastkey) = @_;
408 12 100       33 return 'epoch' if $lastkey eq 'tz';
409 8 100       25 return 'frac' if $lastkey eq 'epoch';
410 4         24 return undef;
411             }
412              
413             # This is for debugging only !
414             # sub iso { my $self = shift; return $self->{year} . '-' . $self->{month} . '-' . $self->{day} . " $self->{weekyear}-W$self->{week}-$self->{weekday}"; }
415             # sub debug { return; my $self = shift; return join(':',%{$self}); }
416              
417             1;
418              
419             __END__