File Coverage

blib/lib/Time/Stamp.pm
Criterion Covered Total %
statement 72 72 100.0
branch 51 52 98.0
condition 9 11 81.8
subroutine 14 14 100.0
pod n/a
total 146 149 97.9


line stmt bran cond sub pod time code
1             # vim: set sw=2 sts=2 ts=2 expandtab smarttab:
2             #
3             # This file is part of Time-Stamp
4             #
5             # This software is copyright (c) 2011 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 7     7   214199 use strict;
  7         18  
  7         272  
11 7     7   39 use warnings;
  7         14  
  7         432  
12              
13             package Time::Stamp;
14             {
15             $Time::Stamp::VERSION = '1.300';
16             }
17             # git description: v1.200-5-gd4b4217
18              
19             BEGIN {
20 7     7   463 $Time::Stamp::AUTHORITY = 'cpan:RWSTAUNER';
21             }
22             # ABSTRACT: Easy, readable, efficient timestamp functions
23              
24             # TODO: use collector?
25              
26 7         112 use Sub::Exporter 0.982 -setup => {
27             -as => 'do_import',
28             exports => [
29             localstamp => \'_build_localstamp',
30             gmstamp => \'_build_gmstamp',
31             parsegm => \'_build_parsestamp',
32             parselocal => \'_build_parsestamp',
33             ],
34             groups => [
35             stamps => [qw(localstamp gmstamp)],
36             parsers => [qw(parselocal parsegm)],
37             ]
38 7     7   6398 };
  7         97038  
39              
40             sub import {
41 8 100       44 @_ = map {
42 22     22   4277 /(local|gm)(?:stamp)?((?:-\w+)+)/
43             ? ($1.'stamp' => {
44             map {
45 14         21 /^([um]s)$/ ? ($1 => 1)
46             : (format => $_)
47             }
48 144 100       427 grep { $_ }
49             split(/-/, $2)
50             })
51             : $_
52             } @_;
53 22         100 goto &do_import;
54             }
55              
56             # set up named formats with default values
57             my $formats = do {
58             # should we offer { prefix => '', suffix => '' } ? is that really useful?
59             # the stamps are easy enough to parse as is (the whole point of this module)
60             my %default = (
61             date_sep => '-',
62             dt_sep => 'T', # ISO 8601
63             time_sep => ':',
64             tz_sep => '',
65             tz => '',
66             );
67             my %blank = map { $_ => '' } keys %default;
68             my $n = {
69             default => {%default},
70             easy => {%default, dt_sep => ' ', tz_sep => ' '}, # easier to read
71             numeric => {%blank},
72             compact => {
73             %blank,
74             dt_sep => '_', # visual separation
75             },
76             };
77             # aliases
78             $n->{$_} = $n->{default} for qw(iso8601 rfc3339 w3cdtf);
79             $n;
80             };
81              
82             # we could offer a separate format_time_array() but currently
83             # I think the gain would be less than the cost of the extra function call:
84             # sub _build { return sub { format_time_array($arg, @_ or localtime) }; }
85             # sub format_time_array { sprintf(_format(shift), _ymdhms(@_)) }
86              
87             sub _build_localstamp {
88             ##my ( $class, $name, $arg, $col ) = @_;
89 30     30   2507 my ( undef, undef, $arg, undef ) = @_;
90              
91 30         65 return _generate_code(local => $arg);
92             }
93              
94             sub _build_gmstamp {
95             ##my ( $class, $name, $arg, $col ) = @_;
96 29     29   1362 my ( undef, undef, $arg, undef ) = @_;
97              
98             # add the Z for UTC (Zulu) time zone unless the numeric format is requested
99 29 100 100     184 $arg = {tz => 'Z', %$arg}
100             unless $arg->{format} && $arg->{format} eq 'numeric';
101              
102 29         63 return _generate_code(gm => $arg);
103             }
104              
105             # TODO: could these subs be faster with a no_args option? would only save 2 if's
106             sub _generate_code {
107 59     59   77 my ($which, $arg) = @_;
108 59         217 $arg = { %$arg };
109             # note: mu is 03BC
110 59 100 100     327 $arg->{frac} ||= $arg->{us} ? 6 : $arg->{ms} ? 3 : 0;
    100          
111              
112 59         104 my $format = _format($arg);
113              
114 59         71 my $code;
115 59         129 my $vars = {
116             which => $which,
117             };
118 59 100       124 if( $arg->{frac} ){
119 17         27 $vars->{frac} = $arg->{frac};
120             # always display a fraction if requested
121 17 100       35 $vars->{gettime} = _have_hires()
122             # gettimeofday() returns microseconds, so we need six digits to stringify
123             # which means we need a sprintf somewhere
124             ? 'do { my @t = Time::HiRes::gettimeofday(); $t[1] = sprintf "%06d", $t[1]; @t }'
125             # if HiRes fails to load use whole number precision
126             : '(time(), 0)';
127              
128 17         35 $code = <<'CODE';
129             sub {
130             # localtime() will not preserve the fraction, so separate it
131             my ($t, $f) = @_ ? (split(/\./, $_[0]), 0) : {{gettime}};
132              
133             my @lt = _ymdhms(@_ > 1 ? @_ : {{which}}time($t));
134              
135             # use %.6f for precision, but strip leading zero
136             return sprintf($format, @lt, substr(sprintf('%.{{frac}}f', '.'.$f), 1));
137             };
138             CODE
139             }
140             # if not using fraction return a more efficient sub
141             else {
142 42         72 $code = <<'CODE';
143             sub {
144             return sprintf($format,
145             _ymdhms(@_ > 1 ? @_ : {{which}}time(@_ ? $_[0] : time))
146             );
147             };
148             CODE
149             }
150             # poor man's template (easier than sprintf or escaping sigils)
151 59         616 $code =~ s/\{\{(\w+)\}\}/$vars->{$1}/g;
152              
153 59 50   5   96 return do { eval $code or die $@ }; ## no critic (StringyEval)
  59 100       7847  
  5 100       496  
  9 100       5327  
  14 100       3700  
  12 100       150  
  8 100       2291  
154             }
155              
156             sub _build_parsestamp {
157             ##my ($class, $name, $arg, $col) = @_;
158 31     25   1312 my ( undef, $name, $arg, undef ) = @_;
159              
160             # pre-compile the regexp
161 33 100       410 my $regexp = exists $arg->{regexp}
162             ? qr/$arg->{regexp}/
163             : qr/^ (\d{4}) \D* (\d{2}) \D* (\d{2}) \D*
164             (\d{2}) \D* (\d{2}) \D* (\d{2}) (?:\.(\d+))? .* $/x;
165              
166 33         4301 require Time::Local; # core
167 31 100       8260 my $time = $name eq 'parsegm'
168             ? \&Time::Local::timegm
169             : \&Time::Local::timelocal;
170              
171             return sub {
172 90     84   8503 my ($stamp) = @_;
173 86         1267 my ($frac, @time) = reverse ($stamp =~ $regexp);
174              
175             # if the regexp didn't match (empty list) give up now
176             return
177 86 100       487 if !@time;
178              
179             # regexp didn't have 7th capture group (for fraction)
180 84 100       212 if( @time < 6 ){
181 12         137 unshift @time, $frac;
182             # if there was a fraction in group 6 separate it
183             # or timelocal may produce something unexpected.
184             # if there was no fraction $frac will be undef
185 12         111 ($time[0], $frac) = split(/\./, $time[0]);
186             }
187              
188             # coerce strings into numbers (map { int } would not work for fractions)
189 86         231 @time = map { $_ + 0 } @time;
  496         1433  
190              
191 86         249 $time[5] -= 1900; # year
192 86         2400 $time[4] -= 1; # month
193              
194             # make sure it starts with a dot (whether it has one or not)
195 86 100       3345 $frac =~ s/^0?\.?/./
196             if defined $frac;
197              
198 83 100       351 if( wantarray ){
199 21 100       57 $time[0] .= $frac
200             if defined $frac;
201 25         3452 return @time;
202             }
203             else {
204 62         184 my $ts = &$time(@time);
205 63 100       2721 $ts .= $frac
206             if defined $frac;
207 63         358 return $ts;
208             }
209 31         344 };
210             }
211              
212             sub _format {
213 96     92   178 my ($arg) = @_;
214              
215 96   100     406 my $name = $arg->{format} || ''; # avoid undef
216             # we could return $arg->{format} unless exists $formats->{$name}; warn if no % found?
217             # or just return $arg->{sprintf} if exists $arg->{sprintf};
218 96 100       268 $name = 'default'
219             unless exists $formats->{$name};
220              
221             # start with named format, overwrite with any explicitly specified options
222 94         184 my %opt = (%{ $formats->{$name} }, %$arg);
  94         724  
223              
224             # TODO: $opt{tz} = tz_offset() if $opt{guess_tz};
225              
226             # sadly "%02.6f" does not zero-pad the integer portion, so we have to be trickier
227              
228             return
229 94 100       933 join($opt{date_sep}, qw(%04d %02d %02d)) .
    100          
230             $opt{dt_sep} .
231             join($opt{time_sep}, qw(%02d %02d %02d)) .
232             ($opt{frac} ? '%s' : '') .
233             ($opt{tz} ? $opt{tz_sep} . $opt{tz} : '')
234             ;
235             }
236              
237             # convert *time() arrays to something ready to send to sprintf
238             sub _ymdhms {
239 88     84   1821 return ($_[5] + 1900, $_[4] + 1, @_[3, 2, 1, 0]);
240             }
241              
242             my $_have_hires;
243             sub _have_hires {
244 18 100   14   102 if( !defined($_have_hires) ){
245 6         2222 local $@;
246 3   33     52 $_have_hires = eval { require Time::HiRes; 1 } || do {
247             warn "Time::HiRes requested but failed to load: $@";
248             0;
249             };
250             }
251 15         58 return $_have_hires;
252             }
253              
254             # define default localstamp and gmstamp in this package
255             # so that exporting is not strictly required
256             __PACKAGE__->import(qw(
257             localstamp
258             gmstamp
259             parsegm
260             parselocal
261             ));
262              
263             1;
264              
265             __END__