File Coverage

lib/Time/Precise.pm
Criterion Covered Total %
statement 108 135 80.0
branch 47 94 50.0
condition 16 57 28.0
subroutine 22 29 75.8
pod 8 10 80.0
total 201 325 61.8


line stmt bran cond sub pod time code
1             package Time::Precise;
2            
3             require Exporter;
4 2     2   104060 use Carp;
  2         7  
  2         89  
5 2     2   9 use Config;
  2         2  
  2         45  
6 2     2   6 use strict;
  2         2  
  2         27  
7 2     2   896 use Time::HiRes;
  2         2081  
  2         6  
8            
9 2     2   183 use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK $PRECISION );
  2         2  
  2         126  
10 2     2   854 use subs qw(localtime gmtime time sleep );
  2         36  
  2         8  
11             $VERSION = '1.0010';
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   187 use constant SECS_PER_MINUTE => 60;
  2         2  
  2         159  
44 2     2   10 use constant SECS_PER_HOUR => 3600;
  2         2  
  2         89  
45 2     2   10 use constant SECS_PER_DAY => 86400;
  2         2  
  2         3836  
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   118 sprintf '%0.'.$PRECISION.'f', Time::HiRes::time();
88             }
89            
90             sub _localtime {
91 9     9   11 my $gm = shift;
92 9         11 my $arg = $_[0];
93 9 100       23 if ($arg < 0) {
94 1 50       4 croak "Negative seconds require a Perl version >= 5.012" unless $] >= 5.012;
95             }
96 9 50       18 $arg = time unless defined $arg;
97 9         57 $arg = sprintf '%.'.$PRECISION.'f', $arg;
98 9         25 my ($seconds, $microseconds) = split /\./, $arg;
99 9 100       18 if (wantarray) {
100 5 50       54 my @lt = $gm ? CORE::gmtime($arg) : CORE::localtime($arg);
101 5 50       19 $lt[0] .= ".$microseconds" if $PRECISION;
102 5         9 $lt[5] += 1900;
103 5         18 return @lt;
104             } else {
105 4 50       32 my $str = $gm ? scalar CORE::gmtime($arg) : scalar CORE::localtime($arg);
106 4 50       9 $str = 0 unless defined $str;
107 4 50       24 $str =~ s/(\d{2}:\d{2}:\d{2}) (\d{4})/$PRECISION ? "$1.$microseconds $2" : "$1 $2"/e;
  4         16  
108 4         13 $str;
109             }
110             }
111            
112             sub localtime (;$) { # Precise localtime: always use full year format.
113 0     0   0 unshift @_, 0;
114 0         0 goto &_localtime;
115             }
116            
117             sub gmtime (;$) { # Precise localtime: always use full year format.
118 9     9   2328 unshift @_, 1;
119 9         21 goto &_localtime;
120             }
121            
122             sub sleep {
123 1     1   4 my $t = shift;
124 1         500125 Time::HiRes::sleep($t);
125             }
126            
127             sub _daygm {
128            
129             # This is written in such a byzantine way in order to avoid
130             # lexical variables and sub calls, for speed
131             return $_[3] + (
132 4   66 4   25 $Cheat{ pack( 'ss', @_[ 4, 5 ] ) } ||= do {
133 3         8 my $month = ( $_[4] + 10 ) % 12;
134 3         7 my $year = $_[5] - int($month / 10);
135             (
136 3         16 ( 365 * $year )
137             + int( $year / 4 )
138             - int( $year / 100 )
139             + int( $year / 400 )
140             + int( ( ( $month * 306 ) + 5 ) / 10 )
141             ) - $Epoc;
142             }
143             );
144             }
145            
146             sub _timegm {
147 0     0   0 my $sec =
148             $SecOff + $_[0] + ( SECS_PER_MINUTE * $_[1] ) + ( SECS_PER_HOUR * $_[2] );
149            
150 0         0 return $sec + ( SECS_PER_DAY * &_daygm );
151             }
152            
153             sub timegm {
154 2     2 1 8 my ( $sec, $min, $hour, $mday, $month, $year ) = @_;
155 2         13 ($sec, my $microsec) = split /\./, sprintf '%.'.$PRECISION.'f', $sec;
156 2 50       5 unless ( $Options{no_range_check} ) {
157 2 50 33     12 croak "Month '$month' out of range 0..11"
158             if $month > 11
159             or $month < 0;
160            
161 2         3 my $md = $MonthDays[$month];
162 2 50 33     6 ++$md
163             if $month == 1 && _is_leap_year( $year );
164            
165 2 50 33     8 croak "Day '$mday' out of range 1..$md" if $mday > $md or $mday < 1;
166 2 50 33     7 croak "Hour '$hour' out of range 0..23" if $hour > 23 or $hour < 0;
167 2 50 33     8 croak "Minute '$min' out of range 0..59" if $min > 59 or $min < 0;
168 2 50 33     8 croak "Second '$sec' out of range 0..59" if $sec > 59 or $sec < 0;
169             }
170            
171 2         6 my $days = _daygm( undef, undef, undef, $mday, $month, $year );
172            
173 2 50 33     9 unless ($Options{no_range_check} or abs($days) < $MaxDay) {
174 0         0 my $msg = '';
175 0 0       0 $msg .= "Day too big - $days > $MaxDay\n" if $days > $MaxDay;
176            
177 0         0 $msg .= "Cannot handle date ($sec, $min, $hour, $mday, $month, $year)";
178            
179 0         0 croak $msg;
180             }
181            
182 2         3 my $fix = 0;
183 2 50 33     5 $fix -= 60*60*24 if ($year < 0 and not _is_leap_year($year));
184 2         14 return ($sec
185             + $SecOff
186             + ( SECS_PER_MINUTE * $min )
187             + ( SECS_PER_HOUR * $hour )
188             + ( SECS_PER_DAY * $days )
189             + $fix).".$microsec";
190             }
191            
192             sub _is_leap_year {
193 4 100   4   20 return 0 if $_[0] % 4;
194 1 50       3 return 1 if $_[0] % 100;
195 1 50       10 return 0 if $_[0] % 400;
196 1         3 return 1;
197             }
198            
199             sub timegm_nocheck {
200 0     0 0 0 local $Options{no_range_check} = 1;
201 0         0 return &timegm;
202             }
203            
204             sub timelocal {
205 0     0 1 0 my ($ref_t, $microsec) = split /\./, &timegm;
206 0 0 0     0 $ref_t += 60*60*24 if ($_[5] < 0 and not _is_leap_year($_[5]));
207 0         0 my $loc_for_ref_t = _timegm( localtime($ref_t) );
208            
209 0 0       0 my $zone_off = $loc_for_ref_t - $ref_t
210             or return "$loc_for_ref_t.$microsec";
211            
212             # Adjust for timezone
213 0         0 my $loc_t = $ref_t - $zone_off;
214            
215             # Are we close to a DST change or are we done
216 0         0 my $dst_off = $ref_t - _timegm( localtime($loc_t) );
217            
218             # If this evaluates to true, it means that the value in $loc_t is
219             # the _second_ hour after a DST change where the local time moves
220             # backward.
221 0 0 0     0 if ( ! $dst_off &&
222             ( ( $ref_t - SECS_PER_HOUR ) - _timegm( localtime( $loc_t - SECS_PER_HOUR ) ) < 0 )
223             ) {
224 0         0 return ''.($loc_t - SECS_PER_HOUR).".$microsec";
225             }
226            
227             # Adjust for DST change
228 0         0 $loc_t += $dst_off;
229            
230 0 0       0 return "$loc_t.$microsec" if $dst_off > 0;
231            
232             # If the original date was a non-extent gap in a forward DST jump,
233             # we should now have the wrong answer - undo the DST adjustment
234 0         0 my ( $s, $m, $h ) = localtime($loc_t);
235 0 0 0     0 $loc_t -= $dst_off if $s != $_[0] || $m != $_[1] || $h != $_[2];
      0        
236            
237 0         0 return "$loc_t.$microsec";
238             }
239            
240             sub timelocal_nocheck {
241 0     0 0 0 local $Options{no_range_check} = 1;
242 0         0 return &timelocal;
243             }
244            
245             sub is_valid_date {
246 6     6 1 11 my ($year, $month, $day) = @_;
247 6 50 33     54 return 0 unless ($year =~ /^\d+$/ and $month =~ /^\d+$/ and $day =~ /^\d+$/);
      33        
248 6         10 $year += 0;
249 6         4 $month += 0;
250 6         5 $day += 0;
251 6 50       9 return 0 unless $year;
252 6 50 33     15 return 0 if ($month < 1 or $month > 12);
253 6 50       11 return 0 if $day < 1;
254 6 100       11 if ($month == 2) {
255 2 100       4 if (is_leap_year($year)) {
256 1 50       3 return 0 if $day > 29;
257             } else {
258 1 50       6 return 0 if $day > 28;
259             }
260             } else {
261 4 100       14 return 0 if $day > $month_duration->{$month};
262             }
263 3         11 return 1;
264             }
265            
266             sub is_leap_year {
267 4     4 1 8 _is_leap_year(shift);
268             }
269            
270             sub time_hashref (;$) {
271 0     0 1 0 _time_hashref(shift);
272             }
273            
274             sub gmtime_hashref {
275 2     2 1 5 _time_hashref(shift, 1);
276             }
277            
278             sub _time_hashref {
279 2     2   3 my $time = shift;
280 2         2 my $gmt = shift;
281 2 100       5 $time = time() unless defined $time;
282 2 50       9 my @lt = $gmt ? gmtime(int $time) : localtime(int $time);
283 2         16 (my $microseconds = sprintf '%0.'.$PRECISION.'f', ($time - int $time)) =~ s/^.+\.//;
284             return {
285 2         15 second => sprintf("%02d.$microseconds", $lt[0]),
286             minute => sprintf("%02d", $lt[1]),
287             hour => sprintf("%02d", $lt[2]),
288             day => sprintf("%02d", $lt[3]),
289             month => sprintf("%02d", ($lt[4] + 1)),
290             year => sprintf("%04d", $lt[5]),
291             wday => $lt[6],
292             yday => $lt[7],
293             isdst => $lt[8],
294             is_leap_year => is_leap_year($lt[5]),
295             };
296             }
297            
298             sub get_time_from {
299 0     0 1 0 _get_time_from('', @_);
300             }
301            
302             sub get_gmtime_from {
303 1     1 1 2757 _get_time_from(1, @_);
304             }
305            
306             sub _get_time_from {
307 1     1   4 my @call = caller;
308 1         2 my $gm = shift;
309 1 50       3 die("get_time_from expects name => value optional parameters (day, month, year, hour, minute, second) at $call[1] line $call[2]\n") if @_ % 2;
310 1 50       4 my $time = $gm ? gmtime_hashref : time_hashref;
311             my $p = {
312             day => $time->{day},
313             month => $time->{month},
314             year => $time->{year},
315 1         6 minute => 0,
316             hour => 0,
317             second => 0,
318             @_,
319             };
320 1         3 for my $i (qw(day month year minute hour second)) {
321 6 50       24 die("Parameter $i must be numeric at $call[1] line $call[2]\n") unless $p->{$i} =~ /^(-){0,1}\d+(\.\d+){0,1}$/;
322             }
323 1 50 33     7 die("Invalid parameter month, out of range 1..12 at $call[1] line $call[2]\n") if ($p->{month} < 1 or $p->{month} > 12);
324 1         3 for my $i (qw(minute hour second)) {
325 3 50 33     12 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;
326             }
327 1 0       5 my $max_day = $month_duration->{int $p->{month}} + ((int($p->{month}) == 2) ? is_leap_year($p->{year}) ? 1 : 0 : 0);
    50          
328 1 50 33     6 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;
329 1 50       5 $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});
330             }
331            
332             1;
333            
334             __END__