File Coverage

blib/lib/Log/Stamper.pm
Criterion Covered Total %
statement 118 130 90.7
branch 50 58 86.2
condition 1 3 33.3
subroutine 22 24 91.6
pod 3 3 100.0
total 194 218 88.9


line stmt bran cond sub pod time code
1             package Log::Stamper;
2 3     3   86922 use strict;
  3         8  
  3         126  
3 3     3   16 use warnings;
  3         6  
  3         95  
4 3     3   16 use Carp qw/croak/;
  3         10  
  3         6274  
5              
6             our $VERSION = '0.031';
7              
8             our $GMTIME = 0;
9              
10             our @MONTH_NAMES = qw/
11             January February March April May June July
12             August September October November December
13             /;
14              
15             our @WEEK_DAYS = qw/
16             Sunday Monday Tuesday Wednesday Thursday Friday Saturday
17             /;
18              
19             sub new {
20 26     26 1 4703 my ($class, $format, $callback) = @_;
21              
22 26 100       133 my $self = +{
23             stack => [],
24             fmt => undef,
25             callback => ref($callback) eq 'CODE' ? $callback : undef,
26             };
27              
28 26         59 bless $self, $class;
29              
30 26 50       86 $self->_prepare($format) if $format;
31              
32 25         71 return $self;
33             }
34              
35             sub _prepare {
36 26     26   38 my($self, $format) = @_;
37              
38             # the actual DateTime spec allows for literal text delimited by
39             # single quotes; a single quote can be embedded in the literal
40             # text by using two single quotes.
41             #
42             # my strategy here is to split the format into active and literal
43             # "chunks"; active chunks are prepared using $self->rep() as
44             # before, while literal chunks get transformed to accomodate
45             # single quotes and to protect percent signs.
46             #
47             # motivation: the "recommended" ISO-8601 date spec for a time in
48             # UTC is actually:
49             #
50             # YYYY-mm-dd'T'hh:mm:ss.SSS'Z'
51              
52 26         32 my $fmt = '';
53              
54 26         109 for my $chunk ( split /('(?:''|[^'])*')/, $format ) {
55 42 100       135 if ( $chunk =~ /\A'(.*)'\z/ ) {
    100          
56             # literal text
57 9         15 my $literal = $1;
58 9         18 $literal =~ s/''/'/g;
59 9         10 $literal =~ s/\%/\%\%/g;
60 9         17 $fmt .= $literal;
61             } elsif ( $chunk =~ /'/ ) {
62             # single quotes should always be in a literal
63 1         199 croak "bad date format \"$format\": " .
64             "unmatched single quote in chunk \"$chunk\"";
65             } else {
66             # handle active chunks just like before
67 32         188 $chunk =~ s/(([GyMdhHmsSEeDFwWakKzZ])\2*)/$self->_rep($1)/ge;
  107         267  
68 32         115 $fmt .= $chunk;
69             }
70             }
71              
72 25         75 $self->{fmt} = $fmt;
73             }
74              
75             sub _rep {
76 107     107   199 my ($self, $string) = @_;
77              
78 107         140 my $first = substr $string, 0, 1;
79 107         144 my $len = length $string;
80              
81 107         134 my $time=time();
82 107         366 my @g = gmtime($time);
83 107         2334 my @t = localtime($time);
84 107         365 my $z = $t[1]-$g[1]+($t[2]-$g[2])*60+($t[7]-$g[7])*1440+
85             ($t[5]-$g[5])*(525600+(abs($t[7]-$g[7])>364)*1440);
86 107         305 my $offset = sprintf("%+.2d%.2d", $z/60, "00");
87              
88             #my ($s,$mi,$h,$d,$mo,$y,$wd,$yd,$dst) = localtime($time);
89              
90             # Here's how this works:
91             # Detect what kind of parameter we're dealing with and determine
92             # what type of sprintf-placeholder to return (%d, %02d, %s or whatever).
93             # Then, we're setting up an array, specific to the current format,
94             # that can be used later on to compute the components of the placeholders
95             # one by one when we get the components of the current time later on
96             # via localtime.
97              
98             # So, we're parsing the "yyyy/MM" format once, replace it by, say
99             # "%04d:%02d" and store an array that says "for the first placeholder,
100             # get the localtime-parameter on index #5 (which is years since the
101             # epoch), add 1900 to it and pass it on to sprintf(). For the 2nd
102             # placeholder, get the localtime component at index #2 (which is hours)
103             # and pass it on unmodified to sprintf.
104              
105             # So, the array to compute the time format at logtime contains
106             # as many elements as the original SimpleDateFormat contained. Each
107             # entry is a arrary ref, holding an array with 2 elements: The index
108             # into the localtime to obtain the value and a reference to a subroutine
109             # to do computations eventually. The subroutine expects the orginal
110             # localtime() time component (like year since the epoch) and returns
111             # the desired value for sprintf (like y+1900).
112              
113             # This way, we're parsing the original format only once (during system
114             # startup) and during runtime all we do is call localtime *once* and
115             # run a number of blazingly fast computations, according to the number
116             # of placeholders in the format.
117              
118 107 50       613 if($first eq 'G') { # G - epoch
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
119             # Always constant
120 0         0 return 'AD';
121             }
122             elsif($first eq 'e') { # e - epoch seconds
123             # index (0) irrelevant, but we return time() which
124             # comes in as 2nd parameter
125 0     0   0 push @{$self->{stack}}, [0, sub { return $_[1] }];
  0         0  
  0         0  
126 0         0 return "%d";
127             }
128             elsif($first eq 'y') { # y - year
129 16 100       33 if($len >= 4) {
130             # 4-digit year
131 14     7   16 push @{$self->{stack}}, [5, sub { return $_[0] + 1900 }];
  14         88  
  7         25  
132 14         78 return "%04d";
133             }
134             else {
135             # 2-digit year
136 2     2   4 push @{$self->{stack}}, [5, sub { $_[0] % 100 }];
  2         11  
  2         46  
137 2         25 return "%02d";
138             }
139             }
140             elsif($first eq 'M') { # M - month
141 11 100       29 if($len >= 3) {
    100          
142             # Use month name
143 3     2   6 push @{$self->{stack}}, [4, sub { return $MONTH_NAMES[$_[0]] }];
  3         15  
  2         7  
144 3 100       8 if($len >= 4) {
145 1         7 return "%s";
146             }
147             else {
148 2         14 return "%.3s";
149             }
150             }
151             elsif($len == 2) {
152             # Use zero-padded month number
153 7     1   7 push @{$self->{stack}}, [4, sub { $_[0]+1 }];
  7         41  
  1         3  
154 7         38 return "%02d";
155             }
156             else {
157             # Use zero-padded month number
158 1     1   2 push @{$self->{stack}}, [4, sub { $_[0]+1 }];
  1         6  
  1         3  
159 1         7 return "%d";
160             }
161             }
162             elsif($first eq 'd') { # d - day of month
163 11     4   11 push @{$self->{stack}}, [3, sub { return $_[0] }];
  11         45  
  4         9  
164 11         61 return "%0" . $len . 'd';
165             }
166             elsif($first eq 'h') { #h - am/pm hour
167 6 50   4   10 push @{$self->{stack}}, [2, sub { ($_[0] % 12) || 12 }];
  6         28  
  4         15  
168 6         35 return "%0" . $len . 'd';
169             }
170             elsif($first eq 'H') { # H - 24 hour
171 12     4   14 push @{$self->{stack}}, [2, sub { return $_[0] }];
  12         52  
  4         11  
172 12         70 return "%0" . $len . 'd';
173             }
174             elsif($first eq 'm') { # m - minute
175 12     4   12 push @{$self->{stack}}, [1, sub { return $_[0] }];
  12         56  
  4         9  
176 12         69 return "%0" . $len . 'd';
177             }
178             elsif($first eq 's') { # s - second
179 12     4   12 push @{$self->{stack}}, [0, sub { return $_[0] }];
  12         56  
  4         9  
180 12         66 return "%0" . $len . 'd';
181             }
182             elsif($first eq 'E') { # E - day of week
183 4     28   6 push @{$self->{stack}}, [6, sub { $WEEK_DAYS[$_[0]] }];
  4         20  
  28         70  
184 4 100       10 if($len >= 4) {
185 1         7 return "%${len}s";
186             }
187             else {
188 3         19 return "%.3s";
189             }
190             }
191             elsif($first eq 'D') { # D - day of the year
192 6     6   8 push @{$self->{stack}}, [7, sub { $_[0] + 1}];
  6         29  
  6         17  
193 6         32 return "%0" . $len . 'd';
194             }
195             elsif($first eq 'a') { # a - am/pm marker
196 3 50   2   3 push @{$self->{stack}}, [2, sub { $_[0] < 12 ? 'AM' : 'PM' }];
  3         16  
  2         8  
197 3         17 return "%${len}s";
198             }
199             elsif($first eq 'S') { # S - milliseconds
200 13         60 push @{$self->{stack}},
201 13     6   15 [9, sub { substr sprintf("%06d", $_[0]), 0, $len }];
  6         26  
202 13         93 return "%s";
203             }
204             elsif($first eq 'Z') { # Z - RFC 822 time zone -0800
205 0     0   0 push @{$self->{stack}}, [10, sub { $offset }];
  0         0  
  0         0  
206 0         0 return "$offset";
207             }
208             # Something that's not defined
209             # (F=day of week in month
210             # w=week in year W=week in month
211             # k=hour in day K=hour in am/pm
212             # z=timezone
213             else {
214 1         8 return "-- '$first' not (yet) implemented --";
215             }
216              
217 0         0 return $string;
218             }
219              
220             sub format {
221 22     22 1 166 my($self, $secs, $msecs) = @_;
222              
223 22   33     51 $secs ||= time();
224 22 100       53 $msecs = 0 unless defined $msecs;
225              
226 22         27 my @time;
227              
228 22 50       48 if($GMTIME) {
229 22         84 @time = gmtime $secs;
230             }
231             else {
232 0         0 @time = localtime $secs;
233             }
234              
235             # add milliseconds
236 22         37 push @time, $msecs;
237              
238 22         28 my @values = ();
239              
240 22         24 for my $stack ( @{$self->{stack}} ) {
  22         47  
241 75         71 my($val, $code) = @{$stack};
  75         111  
242 75 50       123 if($code) {
243 75         144 push @values, $code->($time[$val], $secs);
244             }
245             else {
246 0         0 push @values, $time[$val];
247             }
248             }
249              
250 22         88 my $ret = sprintf($self->{fmt}, @values);
251              
252 22 100       50 if ($self->callback) {
253 1         3 return ($self->callback)->($ret);
254             }
255             else {
256 21         713 return $ret;
257             }
258             }
259              
260 23     23 1 61 sub callback { $_[0]->{callback}; }
261              
262             1;
263              
264             __END__