File Coverage

lib/Time/Precise.pm
Criterion Covered Total %
statement 115 140 82.1
branch 51 98 52.0
condition 16 57 28.0
subroutine 25 31 80.6
pod 10 12 83.3
total 217 338 64.2


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