File Coverage

blib/lib/Log/Log4perl/DateFormat.pm
Criterion Covered Total %
statement 123 130 94.6
branch 56 62 90.3
condition n/a
subroutine 22 23 95.6
pod 0 4 0.0
total 201 219 91.7


line stmt bran cond sub pod time code
1             ###########################################
2             ###########################################
3             use warnings;
4 70     70   493 use strict;
  70         186  
  70         2331  
5 70     70   389  
  70         163  
  70         1615  
6             use Carp qw( croak );
7 70     70   342  
  70         144  
  70         113321  
8             our $GMTIME = 0;
9              
10             my @MONTH_NAMES = qw(
11             January February March April May June July
12             August September October November December);
13              
14             my @WEEK_DAYS = qw(
15             Sunday Monday Tuesday Wednesday Thursday Friday Saturday);
16              
17             ###########################################
18             ###########################################
19             my($class, $format) = @_;
20              
21 78     78 0 7371 my $self = {
22             stack => [],
23 78         311 fmt => undef,
24             };
25              
26             bless $self, $class;
27              
28 78         322 # Predefined formats
29             if($format eq "ABSOLUTE") {
30             $format = "HH:mm:ss,SSS";
31 78 100       363 } elsif($format eq "DATE") {
    100          
    100          
    100          
32 1         3 $format = "dd MMM yyyy HH:mm:ss,SSS";
33             } elsif($format eq "ISO8601") {
34 1         3 $format = "yyyy-MM-dd HH:mm:ss,SSS";
35             } elsif($format eq "APACHE") {
36 1         2 $format = "[EEE MMM dd HH:mm:ss yyyy]";
37             }
38 1         3  
39             if($format) {
40             $self->prepare($format);
41 78 50       182 }
42 78         211  
43             return $self;
44             }
45 77         366  
46             ###########################################
47             ###########################################
48             my($self, $format) = @_;
49              
50             # the actual DateTime spec allows for literal text delimited by
51 78     78 0 172 # single quotes; a single quote can be embedded in the literal
52             # text by using two single quotes.
53             #
54             # my strategy here is to split the format into active and literal
55             # "chunks"; active chunks are prepared using $self->rep() as
56             # before, while literal chunks get transformed to accommodate
57             # single quotes and to protect percent signs.
58             #
59             # motivation: the "recommended" ISO-8601 date spec for a time in
60             # UTC is actually:
61             #
62             # YYYY-mm-dd'T'hh:mm:ss.SSS'Z'
63              
64             my $fmt = "";
65              
66             foreach my $chunk ( split /('(?:''|[^'])*')/, $format ) {
67 78         138 if ( $chunk =~ /\A'(.*)'\z/ ) {
68             # literal text
69 78         328 my $literal = $1;
70 94 100       354 $literal =~ s/''/'/g;
    100          
71             $literal =~ s/\%/\%\%/g;
72 9         22 $fmt .= $literal;
73 9         24 } elsif ( $chunk =~ /'/ ) {
74 9         14 # single quotes should always be in a literal
75 9         17 croak "bad date format \"$format\": " .
76             "unmatched single quote in chunk \"$chunk\"";
77             } else {
78 1         198 # handle active chunks just like before
79             $chunk =~ s/(([GyMdhHmsSEeDFwWakKzZ])\2*)/$self->rep($1)/ge;
80             $fmt .= $chunk;
81             }
82 84         514 }
  405         1105  
83 84         384  
84             return $self->{fmt} = $fmt;
85             }
86              
87 77         267 ###########################################
88             ###########################################
89             my ($self, $string) = @_;
90              
91             my $first = substr $string, 0, 1;
92             my $len = length $string;
93 405     405 0 1282  
94             my $time=time();
95 405         855 my @g = gmtime($time);
96 405         631 my @t = localtime($time);
97             my $z = $t[1]-$g[1]+($t[2]-$g[2])*60+($t[7]-$g[7])*1440+
98 405         591 ($t[5]-$g[5])*(525600+(abs($t[7]-$g[7])>364)*1440);
99 405         1530 my $offset = sprintf("%+.2d%.2d", $z/60, "00");
100 405         7906  
101 405         1523 #my ($s,$mi,$h,$d,$mo,$y,$wd,$yd,$dst) = localtime($time);
102              
103 405         1604 # Here's how this works:
104             # Detect what kind of parameter we're dealing with and determine
105             # what type of sprintf-placeholder to return (%d, %02d, %s or whatever).
106             # Then, we're setting up an array, specific to the current format,
107             # that can be used later on to compute the components of the placeholders
108             # one by one when we get the components of the current time later on
109             # via localtime.
110            
111             # So, we're parsing the "yyyy/MM" format once, replace it by, say
112             # "%04d:%02d" and store an array that says "for the first placeholder,
113             # get the localtime-parameter on index #5 (which is years since the
114             # epoch), add 1900 to it and pass it on to sprintf(). For the 2nd
115             # placeholder, get the localtime component at index #2 (which is hours)
116             # and pass it on unmodified to sprintf.
117            
118             # So, the array to compute the time format at logtime contains
119             # as many elements as the original SimpleDateFormat contained. Each
120             # entry is a array ref, holding an array with 2 elements: The index
121             # into the localtime to obtain the value and a reference to a subroutine
122             # to do computations eventually. The subroutine expects the original
123             # localtime() time component (like year since the epoch) and returns
124             # the desired value for sprintf (like y+1900).
125              
126             # This way, we're parsing the original format only once (during system
127             # startup) and during runtime all we do is call localtime *once* and
128             # run a number of blazingly fast computations, according to the number
129             # of placeholders in the format.
130              
131             ###########
132             #G - epoch#
133             ###########
134             if($first eq "G") {
135             # Always constant
136             return "AD";
137              
138 405 50       2346 ###################
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
139             #e - epoch seconds#
140 0         0 ###################
141             } elsif($first eq "e") {
142             # index (0) irrelevant, but we return time() which
143             # comes in as 2nd parameter
144             push @{$self->{stack}}, [0, sub { return $_[1] }];
145             return "%d";
146              
147             ##########
148 1     1   3 #y - year#
  1         7  
  1         3  
149 1         7 ##########
150             } elsif($first eq "y") {
151             if($len >= 4) {
152             # 4-digit year
153             push @{$self->{stack}}, [5, sub { return $_[0] + 1900 }];
154             return "%04d";
155 61 100       231 } else {
156             # 2-digit year
157 60     115   167 push @{$self->{stack}}, [5, sub { $_[0] % 100 }];
  60         452  
  115         405  
158 60         499 return "%02d";
159             }
160              
161 1     1   3 ###########
  1         8  
  1         6  
162 1         8 #M - month#
163             ###########
164             } elsif($first eq "M") {
165             if($len >= 3) {
166             # Use month name
167             push @{$self->{stack}}, [4, sub { return $MONTH_NAMES[$_[0]] }];
168             if($len >= 4) {
169 59 100       214 return "%s";
    100          
170             } else {
171 5     4   8 return "%.3s";
  5         27  
  4         11  
172 5 100       42 }
173 1         8 } elsif($len == 2) {
174             # Use zero-padded month number
175 4         47 push @{$self->{stack}}, [4, sub { $_[0]+1 }];
176             return "%02d";
177             } else {
178             # Use zero-padded month number
179 53     109   93 push @{$self->{stack}}, [4, sub { $_[0]+1 }];
  53         391  
  109         259  
180 53         386 return "%d";
181             }
182              
183 1     1   2 ##################
  1         7  
  1         3  
184 1         8 #d - day of month#
185             ##################
186             } elsif($first eq "d") {
187             push @{$self->{stack}}, [3, sub { return $_[0] }];
188             return "%0" . $len . "d";
189              
190             ##################
191 58     113   106 #h - am/pm hour#
  58         372  
  113         238  
192 58         469 ##################
193             } elsif($first eq "h") {
194             push @{$self->{stack}}, [2, sub { ($_[0] % 12) || 12 }];
195             return "%0" . $len . "d";
196              
197             ##################
198 6 50   4   9 #H - 24 hour#
  6         37  
  4         14  
199 6         43 ##################
200             } elsif($first eq "H") {
201             push @{$self->{stack}}, [2, sub { return $_[0] }];
202             return "%0" . $len . "d";
203              
204             ##################
205 63     115   121 #m - minute#
  63         402  
  115         226  
206 63         472 ##################
207             } elsif($first eq "m") {
208             push @{$self->{stack}}, [1, sub { return $_[0] }];
209             return "%0" . $len . "d";
210              
211             ##################
212 63     115   100 #s - second#
  63         418  
  115         238  
213 63         487 ##################
214             } elsif($first eq "s") {
215             push @{$self->{stack}}, [0, sub { return $_[0] }];
216             return "%0" . $len . "d";
217              
218             ##################
219 63     115   106 #E - day of week #
  63         381  
  115         262  
220 63         392 ##################
221             } elsif($first eq "E") {
222             push @{$self->{stack}}, [6, sub { $WEEK_DAYS[$_[0]] }];
223             if($len >= 4) {
224             return "%${len}s";
225             } else {
226 5     29   9 return "%.3s";
  5         27  
  29         60  
227 5 100       14 }
228 1         9  
229             ######################
230 4         27 #D - day of the year #
231             ######################
232             } elsif($first eq "D") {
233             push @{$self->{stack}}, [7, sub { $_[0] + 1}];
234             return "%0" . $len . "d";
235              
236             ######################
237 6     6   11 #a - am/pm marker #
  6         35  
  6         15  
238 6         41 ######################
239             } elsif($first eq "a") {
240             push @{$self->{stack}}, [2, sub { $_[0] < 12 ? "AM" : "PM" }];
241             return "%${len}s";
242              
243             ######################
244 3 50   2   5 #S - milliseconds #
  3         18  
  2         7  
245 3         21 ######################
246             } elsif($first eq "S") {
247             push @{$self->{stack}},
248             [9, sub { substr sprintf("%06d", $_[0]), 0, $len }];
249             return "%s";
250              
251 16         85 ###############################
252 16     9   28 #Z - RFC 822 time zone -0800 #
  9         37  
253 16         87 ###############################
254             } elsif($first eq "Z") {
255             push @{$self->{stack}}, [10, sub { $offset }];
256             return "%s";
257              
258             #############################
259 0     0   0 #Something that's not defined
  0         0  
  0         0  
260 0         0 #(F=day of week in month
261             # w=week in year W=week in month
262             # k=hour in day K=hour in am/pm
263             # z=timezone
264             #############################
265             } else {
266             return "-- '$first' not (yet) implemented --";
267             }
268              
269             return $string;
270 1         8 }
271              
272             ###########################################
273 0         0 ###########################################
274             my($self, $secs, $msecs) = @_;
275              
276             $msecs = 0 unless defined $msecs;
277              
278             my @time;
279 134     134 0 381  
280             if($GMTIME) {
281 134 100       309 @time = gmtime($secs);
282             } else {
283 134         219 @time = localtime($secs);
284             }
285 134 100       309  
286 26         100 # add milliseconds
287             push @time, $msecs;
288 108         2716  
289             my @values = ();
290              
291             for(@{$self->{stack}}) {
292 134         425 my($val, $code) = @$_;
293             if($code) {
294 134         234 push @values, $code->($time[$val], $secs);
295             } else {
296 134         217 push @values, $time[$val];
  134         390  
297 739         1283 }
298 739 50       1374 }
299 739         1404  
300             return sprintf($self->{fmt}, @values);
301 0         0 }
302              
303             1;
304              
305 134         1021  
306             =encoding utf8
307              
308             =head1 NAME
309              
310             Log::Log4perl::DateFormat - Log4perl advanced date formatter helper class
311              
312             =head1 SYNOPSIS
313              
314              
315             # Either in a log4j.conf file ...
316             log4perl.appender.Logfile.layout = \
317             Log::Log4perl::Layout::PatternLayout
318             log4perl.appender.Logfile.layout.ConversionPattern = %d{MM/dd HH:mm} %m
319              
320             # ... or via the PatternLayout class ...
321             use Log::Log4perl::Layout::PatternLayout;
322             my $layout = Log::Log4perl::Layout::PatternLayout->new(
323             "%d{HH:mm:ss,SSS} %m");
324              
325             # ... or even directly with this helper class:
326             use Log::Log4perl::DateFormat;
327             my $format = Log::Log4perl::DateFormat->new("HH:mm:ss,SSS");
328             my $time = time();
329             print $format->format($time), "\n";
330             # => "17:02:39,000"
331              
332             =head1 DESCRIPTION
333              
334             C<Log::Log4perl::DateFormat> is a helper class for the
335             advanced date formatting functions in C<Log::Log4perl::Layout::PatternLayout>,
336             and adheres (mostly) to the log4j SimpleDateFormat spec available on
337              
338             http://download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html
339              
340             It supports the following placeholders:
341              
342             Symbol Meaning Presentation Example
343             ------ ------- ------------ -------
344             G era designator (Text) AD
345             e epoch seconds (Number) 1315011604
346             y year (Number) 1996
347             M month in year (Text & Number) July & 07
348             d day in month (Number) 10
349             h hour in am/pm (1~12) (Number) 12
350             H hour in day (0~23) (Number) 0
351             m minute in hour (Number) 30
352             s second in minute (Number) 55
353             S millisecond (Number) 978
354             E day in week (Text) Tuesday
355             D day in year (Number) 189
356             F day of week in month (Number) 2 (2nd Wed in July)
357             w week in year (Number) 27
358             W week in month (Number) 2
359             a am/pm marker (Text) PM
360             k hour in day (1~24) (Number) 24
361             K hour in am/pm (0~11) (Number) 0
362             z time zone (Text) Pacific Standard Time
363             Z RFC 822 time zone (Text) -0800
364             ' escape for text (Delimiter)
365             '' single quote (Literal) '
366              
367             Presentation explanation:
368              
369             (Text): 4 or more pattern letters--use full form, < 4--use short or
370             abbreviated form if one exists.
371              
372             (Number): the minimum number of digits. Shorter numbers are
373             zero-padded to this amount. Year is handled
374             specially; that is, if the count of 'y' is 2, the
375             Year will be truncated to 2 digits.
376              
377             (Text & Number): 3 or over, use text, otherwise use number.
378              
379             For example, if you want to format the current Unix time in C<"MM/dd HH:mm">
380             format, all you have to do is specify it in the %d{...} section of the
381             PatternLayout in a Log4perl configuration file:
382              
383             # log4j.conf
384             # ...
385             log4perl.appender.Logfile.layout = \
386             Log::Log4perl::Layout::PatternLayout
387             log4perl.appender.Logfile.layout.ConversionPattern = %d{MM/dd HH:mm} %m
388              
389             Same goes for Perl code defining a PatternLayout for Log4perl:
390              
391             use Log::Log4perl::Layout::PatternLayout;
392             my $layout = Log::Log4perl::Layout::PatternLayout->new(
393             "%d{MM/dd HH:mm} %m");
394              
395             Or, on a lower level, you can use the class directly:
396              
397             use Log::Log4perl::DateFormat;
398             my $format = Log::Log4perl::DateFormat->new("MM/dd HH:mm");
399             my $time = time();
400             print $format->format($time), "\n";
401              
402             While the C<new()> method is expensive, because it parses the format
403             strings and sets up all kinds of structures behind the scenes,
404             followup calls to C<format()> are fast, because C<DateFormat> will
405             just call C<localtime()> and C<sprintf()> once to return the formatted
406             date/time string.
407              
408             So, typically, you would initialize the formatter once and then reuse
409             it over and over again to display all kinds of time values.
410              
411             Also, for your convenience,
412             the following predefined formats are available, just as outlined in the
413             log4j spec:
414              
415             Format Equivalent Example
416             ABSOLUTE "HH:mm:ss,SSS" "15:49:37,459"
417             DATE "dd MMM yyyy HH:mm:ss,SSS" "06 Nov 1994 15:49:37,459"
418             ISO8601 "yyyy-MM-dd HH:mm:ss,SSS" "1999-11-27 15:49:37,459"
419             APACHE "[EEE MMM dd HH:mm:ss yyyy]" "[Wed Mar 16 15:49:37 2005]"
420              
421             So, instead of passing
422              
423             Log::Log4perl::DateFormat->new("HH:mm:ss,SSS");
424              
425             you could just as well say
426              
427             Log::Log4perl::DateFormat->new("ABSOLUTE");
428              
429             and get the same result later on.
430              
431             =head2 Known Shortcomings
432            
433             The following placeholders are currently I<not> recognized, unless
434             someone (and that could be you :) implements them:
435              
436             F day of week in month
437             w week in year
438             W week in month
439             k hour in day
440             K hour in am/pm
441             z timezone (but we got 'Z' for the numeric time zone value)
442              
443             Also, C<Log::Log4perl::DateFormat> just knows about English week and
444             month names, internationalization support has to be added.
445              
446             =head1 Millisecond Times
447              
448             More granular timestamps down to the millisecond are also supported,
449             just provide the millsecond count as a second argument:
450              
451             # Advanced time, resultion in milliseconds
452             use Time::HiRes;
453             my ($secs, $msecs) = Time::HiRes::gettimeofday();
454             print $format->format($secs, $msecs), "\n";
455             # => "17:02:39,959"
456              
457             =head1 LICENSE
458              
459             Copyright 2002-2016 by Mike Schilli E<lt>m@perlmeister.comE<gt>
460             and Kevin Goess E<lt>cpan@goess.orgE<gt>.
461              
462             This library is free software; you can redistribute it and/or modify
463             it under the same terms as Perl itself.
464              
465             =head1 AUTHOR
466              
467             Please contribute patches to the project on Github:
468              
469             http://github.com/mschilli/log4perl
470              
471             Send bug reports or requests for enhancements to the authors via our
472              
473             MAILING LIST (questions, bug reports, suggestions/patches):
474             log4perl-devel@lists.sourceforge.net
475              
476             Authors (please contact them via the list above, not directly):
477             Mike Schilli <m@perlmeister.com>,
478             Kevin Goess <cpan@goess.org>
479              
480             Contributors (in alphabetical order):
481             Ateeq Altaf, Cory Bennett, Jens Berthold, Jeremy Bopp, Hutton
482             Davidson, Chris R. Donnelly, Matisse Enzer, Hugh Esco, Anthony
483             Foiani, James FitzGibbon, Carl Franks, Dennis Gregorovic, Andy
484             Grundman, Paul Harrington, Alexander Hartmaier David Hull,
485             Robert Jacobson, Jason Kohles, Jeff Macdonald, Markus Peter,
486             Brett Rann, Peter Rabbitson, Erik Selberg, Aaron Straup Cope,
487             Lars Thegler, David Viner, Mac Yang.
488