File Coverage

blib/lib/Time/Precise.pm
Criterion Covered Total %
statement 105 132 79.5
branch 43 88 48.8
condition 16 57 28.0
subroutine 22 29 75.8
pod 8 10 80.0
total 194 316 61.3


line stmt bran cond sub pod time code
1             package Time::Precise;
2            
3             require Exporter;
4 2     2   44237 use Carp;
  2         5  
  2         155  
5 2     2   10 use Config;
  2         4  
  2         71  
6 2     2   9 use strict;
  2         7  
  2         50  
7 2     2   1706 use Time::HiRes;
  2         3253  
  2         9  
8            
9 2     2   206 use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK $PRECISION );
  2         2  
  2         168  
10 2     2   1581 use subs qw(localtime gmtime time sleep );
  2         79  
  2         11  
11             $VERSION = '1.0006';
12            
13             @ISA = qw(Exporter);
14             @EXPORT = qw(time localtime gmtime sleep timegm timelocal is_valid_date is_leap_year time_hashref gmtime_hashref get_time_from get_gmtime_from);
15            
16             $PRECISION = 7;
17             my @MonthDays = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
18             my $month_duration = {
19             1 => 31,
20             2 => 28,
21             3 => 31,
22             4 => 30,
23             5 => 31,
24             6 => 30,
25             7 => 31,
26             8 => 31,
27             9 => 30,
28             10 => 31,
29             11 => 30,
30             12 => 31,
31             };
32            
33             # Determine breakpoint for rolling century
34             #my $ThisYear = ( localtime() )[5];
35             #my $Breakpoint = ( $ThisYear + 50 ) % 100;
36             #my $NextCentury = $ThisYear - $ThisYear % 100;
37             #$NextCentury += 100 if $Breakpoint < 50;
38             #my $Century = $NextCentury - 100;
39             my $SecOff = 0;
40            
41             my ( %Options, %Cheat );
42            
43 2     2   265 use constant SECS_PER_MINUTE => 60;
  2         4  
  2         206  
44 2     2   10 use constant SECS_PER_HOUR => 3600;
  2         4  
  2         91  
45 2     2   12 use constant SECS_PER_DAY => 86400;
  2         3  
  2         4924  
46            
47             my $MaxDay;
48             if ($] < 5.012000) {
49             my $MaxInt;
50             if ( $^O eq 'MacOS' ) {
51             # time_t is unsigned...
52             $MaxInt = ( 1 << ( 8 * $Config{ivsize} ) ) - 1;
53             }
54             else {
55             $MaxInt = ( ( 1 << ( 8 * $Config{ivsize} - 2 ) ) - 1 ) * 2 + 1;
56             }
57            
58             $MaxDay = int( ( $MaxInt - ( SECS_PER_DAY / 2 ) ) / SECS_PER_DAY ) - 1;
59             }
60             else {
61             # recent localtime()'s limit is the year 2**31
62             $MaxDay = 365 * (2**47); # Supported at least on 5.014 x64
63             }
64            
65             # Determine the EPOC day for this machine
66             my $Epoc = 0;
67             if ( $^O eq 'vos' ) {
68             # work around posix-977 -- VOS doesn't handle dates in the range
69             # 1970-1980.
70             $Epoc = _daygm( 0, 0, 0, 1, 0, 70, 4, 0 );
71             }
72             elsif ( $^O eq 'MacOS' ) {
73             $MaxDay *=2 if $^O eq 'MacOS'; # time_t unsigned ... quick hack?
74             # MacOS time() is seconds since 1 Jan 1904, localtime
75             # so we need to calculate an offset to apply later
76             $Epoc = 693901;
77             $SecOff = timelocal( localtime(0)) - timelocal( gmtime(0) ) ;
78             $Epoc += _daygm( gmtime(0) );
79             }
80             else {
81             $Epoc = _daygm( gmtime(0) );
82             }
83            
84             %Cheat = (); # clear the cache as epoc has changed
85            
86             sub time () {
87 4     4   129 sprintf '%0.'.$PRECISION.'f', Time::HiRes::time();
88             }
89            
90             sub _localtime {
91 9     9   14 my $gm = shift;
92 9         16 my $arg = $_[0];
93 9 50       30 $arg = time unless defined $arg;
94 9         98 $arg = sprintf '%.'.$PRECISION.'f', $arg;
95 9         39 my ($seconds, $microseconds) = split /\./, $arg;
96 9 100       26 if (wantarray) {
97 5 50       72 my @lt = $gm ? CORE::gmtime($arg) : CORE::localtime($arg);
98 5 50       28 $lt[0] .= ".$microseconds" if $PRECISION;
99 5         12 $lt[5] += 1900;
100 5         35 return @lt;
101             } else {
102 4 50       42 my $str = $gm ? scalar CORE::gmtime($arg) : scalar CORE::localtime($arg);
103 4 50       25 $str =~ s/(\d{2}:\d{2}:\d{2}) (\d{4})/$PRECISION ? "$1.$microseconds $2" : "$1 $2"/e;
  4         25  
104 4         19 $str;
105             }
106             }
107            
108             sub localtime (;$) { # Precise localtime: always use full year format.
109 0     0   0 unshift @_, 0;
110 0         0 goto &_localtime;
111             }
112            
113             sub gmtime (;$) { # Precise localtime: always use full year format.
114 9     9   1756 unshift @_, 1;
115 9         34 goto &_localtime;
116             }
117            
118             sub sleep {
119 1     1   6 my $t = shift;
120 1         500219 Time::HiRes::sleep($t);
121             }
122            
123             sub _daygm {
124            
125             # This is written in such a byzantine way in order to avoid
126             # lexical variables and sub calls, for speed
127             return $_[3] + (
128 4   66 4   43 $Cheat{ pack( 'ss', @_[ 4, 5 ] ) } ||= do {
129 3         10 my $month = ( $_[4] + 10 ) % 12;
130 3         10 my $year = $_[5] - int($month / 10);
131             (
132 3         28 ( 365 * $year )
133             + int( $year / 4 )
134             - int( $year / 100 )
135             + int( $year / 400 )
136             + int( ( ( $month * 306 ) + 5 ) / 10 )
137             ) - $Epoc;
138             }
139             );
140             }
141            
142             sub _timegm {
143 0     0   0 my $sec =
144             $SecOff + $_[0] + ( SECS_PER_MINUTE * $_[1] ) + ( SECS_PER_HOUR * $_[2] );
145            
146 0         0 return $sec + ( SECS_PER_DAY * &_daygm );
147             }
148            
149             sub timegm {
150 2     2 1 13 my ( $sec, $min, $hour, $mday, $month, $year ) = @_;
151 2         24 ($sec, my $microsec) = split /\./, sprintf '%.'.$PRECISION.'f', $sec;
152 2 50       12 unless ( $Options{no_range_check} ) {
153 2 50 33     18 croak "Month '$month' out of range 0..11"
154             if $month > 11
155             or $month < 0;
156            
157 2         11 my $md = $MonthDays[$month];
158 2 50 33     11 ++$md
159             if $month == 1 && _is_leap_year( $year );
160            
161 2 50 33     16 croak "Day '$mday' out of range 1..$md" if $mday > $md or $mday < 1;
162 2 50 33     15 croak "Hour '$hour' out of range 0..23" if $hour > 23 or $hour < 0;
163 2 50 33     14 croak "Minute '$min' out of range 0..59" if $min > 59 or $min < 0;
164 2 50 33     17 croak "Second '$sec' out of range 0..59" if $sec > 59 or $sec < 0;
165             }
166            
167 2         7 my $days = _daygm( undef, undef, undef, $mday, $month, $year );
168            
169 2 50 33     18 unless ($Options{no_range_check} or abs($days) < $MaxDay) {
170 0         0 my $msg = '';
171 0 0       0 $msg .= "Day too big - $days > $MaxDay\n" if $days > $MaxDay;
172            
173 0         0 $msg .= "Cannot handle date ($sec, $min, $hour, $mday, $month, $year)";
174            
175 0         0 croak $msg;
176             }
177            
178 2         4 my $fix = 0;
179 2 50 33     16 $fix -= 60*60*24 if ($year < 0 and not _is_leap_year($year));
180 2         24 return ($sec
181             + $SecOff
182             + ( SECS_PER_MINUTE * $min )
183             + ( SECS_PER_HOUR * $hour )
184             + ( SECS_PER_DAY * $days )
185             + $fix).".$microsec";
186             }
187            
188             sub _is_leap_year {
189 4 100   4   46 return 0 if $_[0] % 4;
190 1 50       5 return 1 if $_[0] % 100;
191 1 50       5 return 0 if $_[0] % 400;
192 1         5 return 1;
193             }
194            
195             sub timegm_nocheck {
196 0     0 0 0 local $Options{no_range_check} = 1;
197 0         0 return &timegm;
198             }
199            
200             sub timelocal {
201 0     0 1 0 my ($ref_t, $microsec) = split /\./, &timegm;
202 0 0 0     0 $ref_t += 60*60*24 if ($_[5] < 0 and not _is_leap_year($_[5]));
203 0         0 my $loc_for_ref_t = _timegm( localtime($ref_t) );
204            
205 0 0       0 my $zone_off = $loc_for_ref_t - $ref_t
206             or return "$loc_for_ref_t.$microsec";
207            
208             # Adjust for timezone
209 0         0 my $loc_t = $ref_t - $zone_off;
210            
211             # Are we close to a DST change or are we done
212 0         0 my $dst_off = $ref_t - _timegm( localtime($loc_t) );
213            
214             # If this evaluates to true, it means that the value in $loc_t is
215             # the _second_ hour after a DST change where the local time moves
216             # backward.
217 0 0 0     0 if ( ! $dst_off &&
218             ( ( $ref_t - SECS_PER_HOUR ) - _timegm( localtime( $loc_t - SECS_PER_HOUR ) ) < 0 )
219             ) {
220 0         0 return ''.($loc_t - SECS_PER_HOUR).".$microsec";
221             }
222            
223             # Adjust for DST change
224 0         0 $loc_t += $dst_off;
225            
226 0 0       0 return "$loc_t.$microsec" if $dst_off > 0;
227            
228             # If the original date was a non-extent gap in a forward DST jump,
229             # we should now have the wrong answer - undo the DST adjustment
230 0         0 my ( $s, $m, $h ) = localtime($loc_t);
231 0 0 0     0 $loc_t -= $dst_off if $s != $_[0] || $m != $_[1] || $h != $_[2];
      0        
232            
233 0         0 return "$loc_t.$microsec";
234             }
235            
236             sub timelocal_nocheck {
237 0     0 0 0 local $Options{no_range_check} = 1;
238 0         0 return &timelocal;
239             }
240            
241             sub is_valid_date {
242 6     6 1 22 my ($year, $month, $day) = @_;
243 6 50 33     122 return 0 unless ($year =~ /^\d+$/ and $month =~ /^\d+$/ and $day =~ /^\d+$/);
      33        
244 6         15 $year += 0;
245 6         12 $month += 0;
246 6         14 $day += 0;
247 6 50       21 return 0 unless $year;
248 6 50 33     39 return 0 if ($month < 1 or $month > 12);
249 6 50       20 return 0 if $day < 1;
250 6 100       20 if ($month == 2) {
251 2 100       11 if (is_leap_year($year)) {
252 1 50       18 return 0 if $day > 29;
253             } else {
254 1 50       11 return 0 if $day > 28;
255             }
256             } else {
257 4 100       35 return 0 if $day > $month_duration->{$month};
258             }
259 3         19 return 1;
260             }
261            
262             sub is_leap_year {
263 4     4 1 15 _is_leap_year(shift);
264             }
265            
266             sub time_hashref (;$) {
267 0     0 1 0 _time_hashref(shift);
268             }
269            
270             sub gmtime_hashref {
271 2     2 1 11 _time_hashref(shift, 1);
272             }
273            
274             sub _time_hashref {
275 2     2   6 my $time = shift;
276 2         4 my $gmt = shift;
277 2 100       13 $time = time() unless defined $time;
278 2 50       19 my @lt = $gmt ? gmtime(int $time) : localtime(int $time);
279 2         34 (my $microseconds = sprintf '%0.'.$PRECISION.'f', ($time - int $time)) =~ s/^.+\.//;
280             return {
281 2         32 second => sprintf("%02d.$microseconds", $lt[0]),
282             minute => sprintf("%02d", $lt[1]),
283             hour => sprintf("%02d", $lt[2]),
284             day => sprintf("%02d", $lt[3]),
285             month => sprintf("%02d", ($lt[4] + 1)),
286             year => sprintf("%04d", $lt[5]),
287             wday => $lt[6],
288             yday => $lt[7],
289             isdst => $lt[8],
290             is_leap_year => is_leap_year($lt[5]),
291             };
292             }
293            
294             sub get_time_from {
295 0     0 1 0 _get_time_from('', @_);
296             }
297            
298             sub get_gmtime_from {
299 1     1 1 3249 _get_time_from(1, @_);
300             }
301            
302             sub _get_time_from {
303 1     1   7 my @call = caller;
304 1         2 my $gm = shift;
305 1 50       6 die("get_time_from expects name => value optional parameters (day, month, year, hour, minute, second) at $call[1] line $call[2]\n") if @_ % 2;
306 1 50       5 my $time = $gm ? gmtime_hashref : time_hashref;
307             my $p = {
308             day => $time->{day},
309             month => $time->{month},
310             year => $time->{year},
311 1         7 minute => 0,
312             hour => 0,
313             second => 0,
314             @_,
315             };
316 1         4 for my $i (qw(day month year minute hour second)) {
317 6 50       34 die("Parameter $i must be numeric at $call[1] line $call[2]\n") unless $p->{$i} =~ /^(-){0,1}\d+(\.\d+){0,1}$/;
318             }
319 1 50 33     21 die("Invalid parameter month, out of range 1..12 at $call[1] line $call[2]\n") if ($p->{month} < 1 or $p->{month} > 12);
320 1         2 for my $i (qw(minute hour second)) {
321 3 50 33     19 die("Invalid parameter $i, out of range '>= 0' and '< 60' at $call[1] line $call[2]\n") unless $p->{$i} >= 0 and $p->{$i} < 60;
322             }
323 1 0       5 my $max_day = $month_duration->{int $p->{month}} + ((int($p->{month}) == 2) ? is_leap_year($p->{year}) ? 1 : 0 : 0);
    50          
324 1 50 33     7 die("Invalid parameter day, out of range 1-$max_day at $call[1] line $call[2]\n") unless $p->{day} >= 1 and $p->{day} <= $max_day;
325 1 50       7 $gm ? timegm($p->{second}, $p->{minute}, $p->{hour}, $p->{day}, $p->{month}-1, $p->{year}) : timelocal($p->{second}, $p->{minute}, $p->{hour}, $p->{day}, $p->{month}-1, $p->{year});
326             }
327            
328             1;
329            
330             __END__