File Coverage

blib/lib/POSIX/strftime/GNU/PP.pm
Criterion Covered Total %
statement 79 113 69.9
branch 1 22 4.5
condition 1 39 2.5
subroutine 21 23 91.3
pod 1 1 100.0
total 103 198 52.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -c
2              
3             package POSIX::strftime::GNU::PP;
4              
5             =head1 NAME
6              
7             POSIX::strftime::GNU::PP - Pure-Perl extension for POSIX::strftime::GNU
8              
9             =head1 SYNOPSIS
10              
11             $ export PERL_POSIX_STRFTIME_GNU_PP=1
12              
13             =head1 DESCRIPTION
14              
15             This is PP extension for POSIX::strftime which implements more character
16             sequences compatible with GNU systems.
17              
18             =cut
19              
20              
21 4     4   92 use 5.006;
  4         13  
  4         153  
22 4     4   20 use strict;
  4         6  
  4         128  
23 4     4   18 use warnings;
  4         6  
  4         150  
24              
25             our $VERSION = '0.0304';
26              
27 4     4   16 use Carp ();
  4         5  
  4         66  
28 4     4   17 use POSIX ();
  4         12  
  4         57  
29 4     4   2538 use Time::Local ();
  4         5330  
  4         101  
30              
31 4     4   26 use constant SEC => 0;
  4         7  
  4         236  
32 4     4   22 use constant MIN => 1;
  4         7  
  4         213  
33 4     4   21 use constant HOUR => 2;
  4         6  
  4         16083  
34 4     4   29 use constant MDAY => 3;
  4         7  
  4         1036  
35 4     4   20 use constant MON => 4;
  4         9  
  4         169  
36 4     4   18 use constant YEAR => 5;
  4         8  
  4         147  
37 4     4   18 use constant WDAY => 6;
  4         7  
  4         164  
38 4     4   18 use constant YDAY => 7;
  4         9  
  4         152  
39 4     4   20 use constant ISDST => 8;
  4         7  
  4         3121  
40              
41             # $str = tzoffset (@time)
42             #
43             # Returns the C<+hhmm> or C<-hhmm> numeric timezone (the hour and minute offset
44             # from UTC).
45              
46             my $tzoffset = sub {
47             my ($colons, @t) = @_;
48              
49             # Normalize @t array, we need seconds without frac
50             $t[SEC] = int $t[SEC];
51              
52             my $diff = (exists $ENV{TZ} and $ENV{TZ} eq 'GMT')
53             ? 0
54             : Time::Local::timegm(@t) - Time::Local::timelocal(@t);
55              
56             my $h = $diff / 60 / 60;
57             my $m = $diff / 60 % 60;
58             my $s = $diff % 60;
59              
60             my $fmt = do {
61             if ($colons == 0) {
62             '%+03d%02u';
63             }
64             elsif ($colons == 1) {
65             '%+03d:%02u';
66             }
67             elsif ($colons == 2) {
68             '%+03d:%02u:%02u';
69             }
70             elsif ($colons == 3) {
71             $s ? '%+03d:%02u:%02u' : $m ? '%+03d:%02u' : '%+03d';
72             }
73             else {
74             '%%' . ':' x $colons . 'z';
75             };
76             };
77              
78             return sprintf $fmt, $h, $m, $s;
79             };
80              
81             my @offset2zone = qw(
82             -11 0 SST -11 0 SST
83             -10 0 HAST -09 1 HADT
84             -10 0 HST -10 0 HST
85             -09:30 0 MART -09:30 0 MART
86             -09 0 AKST -08 1 AKDT
87             -09 0 GAMT -09 0 GAMT
88             -08 0 PST -07 1 PDT
89             -08 0 PST -08 0 PST
90             -07 0 MST -06 1 MDT
91             -07 0 MST -07 0 MST
92             -06 0 CST -05 1 CDT
93             -06 0 GALT -06 0 GALT
94             -05 0 ECT -05 0 ECT
95             -05 0 EST -04 1 EDT
96             -05 1 EASST -06 0 EAST
97             -04:30 0 VET -04:30 0 VET
98             -04 0 AMT -04 0 AMT
99             -04 0 AST -03 1 ADT
100             -03:30 0 NST -02:30 1 NDT
101             -03 0 ART -03 0 ART
102             -03 0 PMST -02 1 PMDT
103             -03 1 AMST -04 0 AMT
104             -03 1 WARST -03 1 WARST
105             -02 0 FNT -02 0 FNT
106             -02 1 UYST -03 0 UYT
107             -01 0 AZOT +00 1 AZOST
108             -01 0 CVT -01 0 CVT
109             +00 0 GMT +00 0 GMT
110             +00 0 WET +01 1 WEST
111             +01 0 CET +02 1 CEST
112             +01 0 WAT +01 0 WAT
113             +02 0 EET +02 0 EET
114             +02 0 IST +03 1 IDT
115             +02 1 WAST +01 0 WAT
116             +03 0 FET +03 0 FET
117             +03:07:04 0 zzz +03:07:04 0 zzz
118             +03:30 0 IRST +04:30 1 IRDT
119             +04 0 AZT +05 1 AZST
120             +04 0 GST +04 0 GST
121             +04:30 0 AFT +04:30 0 AFT
122             +05 0 DAVT +07 0 DAVT
123             +05 0 MVT +05 0 MVT
124             +05:30 0 IST +05:30 0 IST
125             +05:45 0 NPT +05:45 0 NPT
126             +06 0 BDT +06 0 BDT
127             +06:30 0 CCT +06:30 0 CCT
128             +07 0 ICT +07 0 ICT
129             +08 0 HKT +08 0 HKT
130             +08:45 0 CWST +08:45 0 CWST
131             +09 0 JST +09 0 JST
132             +09:30 0 CST +09:30 0 CST
133             +10 0 PGT +10 0 PGT
134             +10:30 1 CST +09:30 0 CST
135             +11 0 CAST +08 0 WST
136             +11 0 NCT +11 0 NCT
137             +11 1 EST +10 0 EST
138             +11 1 LHST +10:30 0 LHST
139             +11:30 0 NFT +11:30 0 NFT
140             +12 0 FJT +12 0 FJT
141             +13 0 TKT +13 0 TKT
142             +13 1 NZDT +12 0 NZST
143             +13:45 1 CHADT +12:45 0 CHAST
144             +14 0 LINT +14 0 LINT
145             +14 1 WSDT +13 0 WST
146             );
147              
148             # $str = tzname (@time)
149             #
150             # Returns the abbreviation of the time zone (e.g. "UTC" or "CEST").
151              
152             my $tzname = sub {
153             my @t = @_;
154              
155             return 'GMT' if exists $ENV{TZ} and $ENV{TZ} eq 'GMT';
156              
157             my $diff = $tzoffset->(3, @t);
158              
159             my @t1 = my @t2 = @t;
160             @t1[MDAY,MON] = (1, 1); # winter
161             @t2[MDAY,MON] = (1, 7); # summer
162              
163             my $diff1 = $tzoffset->(3, @t1);
164             my $diff2 = $tzoffset->(3, @t2);
165              
166             for (my $i=0; $i < @offset2zone; $i += 6) {
167             next unless $offset2zone[$i] eq $diff1 and $offset2zone[$i+3] eq $diff2;
168             return $diff2 eq $diff ? $offset2zone[$i+5] : $offset2zone[$i+2];
169             }
170              
171             if ($diff =~ /^([+-])(\d\d)$/) {
172             return sprintf 'GMT%s%d', $1 eq '-' ? '+' : '-', $2;
173             };
174              
175             return 'Etc';
176             };
177              
178 4     4   28 use constant ISO_WEEK_START_WDAY => 1; # Monday
  4         8  
  4         251  
179 4     4   87 use constant ISO_WEEK1_WDAY => 4; # Thursday
  4         9  
  4         155  
180 4     4   27 use constant YDAY_MINIMUM => -366;
  4         7  
  4         180  
181 4     4   19 use constant TM_YEAR_BASE => 1900;
  4         8  
  4         4292  
182              
183             # ($days, $year_adjust) = isodaysnum (@time)
184             #
185             # Returns the number of the year's day based on ISO-8601 standard and year
186             # adjust value.
187              
188             my $isodaysnum = sub {
189             my @t = @_;
190              
191             my $isleap = sub {
192             my ($year) = @_;
193             return (($year) % 4 == 0 && (($year) % 100 != 0 || ($year) % 400 == 0));
194             };
195              
196             my $iso_week_days = sub {
197             my ($yday, $wday) = @_;
198              
199             # Add enough to the first operand of % to make it nonnegative.
200             my $big_enough_multiple_of_7 = (int(- YDAY_MINIMUM / 7) + 2) * 7;
201             return ($yday
202             - ($yday - $wday + ISO_WEEK1_WDAY + $big_enough_multiple_of_7) % 7
203             + ISO_WEEK1_WDAY - ISO_WEEK_START_WDAY);
204             };
205              
206             # Normalize @t array, we need WDAY
207             $t[SEC] = int $t[SEC];
208             @t = gmtime Time::Local::timegm(@t);
209              
210             # YEAR is a leap year if and only if (tp->tm_year + TM_YEAR_BASE)
211             # is a leap year, except that YEAR and YEAR - 1 both work
212             # correctly even when (tp->tm_year + TM_YEAR_BASE) would
213             # overflow.
214             my $year = ($t[YEAR] + ($t[YEAR] < 0 ? TM_YEAR_BASE % 400 : TM_YEAR_BASE % 400 - 400));
215             my $year_adjust = 0;
216             my $days = $iso_week_days->($t[YDAY], $t[WDAY]);
217              
218             if ($days < 0) {
219             # This ISO week belongs to the previous year.
220             $year_adjust = -1;
221             $days = $iso_week_days->($t[YDAY] + (365 + $isleap->($year - 1)), $t[WDAY]);
222             }
223             else {
224             my $d = $iso_week_days->($t[YDAY] - (365 + $isleap->($year)), $t[WDAY]);
225             if ($d >= 0) {
226             # This ISO week belongs to the next year. */
227             $year_adjust = 1;
228             $days = $d;
229             };
230             };
231              
232             return ($days, $year_adjust);
233             };
234              
235             # $num = isoyearnum (@time)
236             #
237             # Returns the number of the year based on ISO-8601 standard. See
238             # L for details.
239              
240             my $isoyearnum = sub {
241             my @t = @_;
242             my ($days, $year_adjust) = $isodaysnum->(@t);
243             return sprintf '%04d', $t[YEAR] + TM_YEAR_BASE + $year_adjust;
244             };
245              
246             # $num = isoweeknum (@time)
247             #
248             # Returns the number of the week based on ISO-8601 standard. See
249             # L for details.
250              
251             my $isoweeknum = sub {
252             my @t = @_;
253             my ($days, $year_adjust) = $isodaysnum->(@t);
254             return sprintf '%02d', int($days / 7) + 1;
255             };
256              
257              
258             =head1 FUNCTIONS
259              
260             =head2 strftime_orig
261              
262             $str = strftime_orig (@time)
263              
264             This is original L function.
265              
266             =cut
267              
268             *strftime_orig = *POSIX::strftime;
269              
270             my %format = (
271             C => sub { 19 + int $_[YEAR] / 100 },
272             D => sub { '%m/%d/%y' },
273             e => sub { sprintf '%2d', $_[MDAY] },
274             F => sub { '%Y-%m-%d' },
275             G => $isoyearnum,
276             g => sub { sprintf '%02d', $isoyearnum->(@_) % 100 },
277             h => sub { '%b' },
278             k => sub { sprintf '%2d', $_[HOUR] },
279             l => sub { sprintf '%2d', $_[HOUR] % 12 + ($_[HOUR] % 12 == 0 ? 12 : 0) },
280             n => sub { "\n" },
281             N => sub { substr sprintf('%.9f', $_[SEC] - int $_[SEC]), 2 },
282             P => sub { lc strftime_orig('%p', @_) },
283             r => sub { '%I:%M:%S %p' },
284             R => sub { '%H:%M' },
285             s => sub { int Time::Local::timegm(@_) },
286             t => sub { "\t" },
287             T => sub { '%H:%M:%S' },
288             u => sub { my $dw = strftime_orig('%w', @_); $dw += ($dw == 0 ? 7 : 0); $dw },
289             V => $isoweeknum,
290             z => $tzoffset,
291             Z => $tzname,
292             '%' => sub { '%%' },
293             );
294              
295             my $formats = join '', sort keys %format;
296              
297              
298             =head2 strftime
299              
300             $str = strftime($format, @time)
301              
302             This is replacement for L function.
303              
304             The non-POSIX feature is that seconds can be float number.
305              
306             =cut
307              
308             sub strftime {
309 8     8 1 22331 my ($fmt, @t) = @_;
310              
311 8 50 33     60 Carp::croak 'Usage: POSIX::strftime::GNU::PP::strftime(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1)'
312             unless @t >= 6 and @t <= 9;
313              
314             my $strftime_modifier = sub {
315 0     0   0 my ($prefix, $modifier, $format, @t) = @_;
316 0         0 my $suffix = '';
317              
318 4     4   25 no warnings 'uninitialized';
  4         7  
  4         5334  
319 0         0 my $str = strftime("%$format", @t);
320              
321 0         0 for (;;) {
322 0 0 0     0 if ($modifier eq '_' and $suffix !~ /0/ or $modifier eq '-' and $suffix !~ /0/ and $format =~ /[aAbBDFhnpPrRtTxXZ%]$/) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
      0        
      0        
      0        
323 0         0 $str =~ s/^([+-])(0+)(\d:.*?|\d$)/' ' x length($2) . $1 . $3/ge;
  0         0  
324 0         0 $str =~ s/^(0+)(.+?)$/' ' x length($1) . $2/ge;
  0         0  
325             }
326             elsif ($modifier eq '-' and $suffix !~ /0/ and $format =~ /[CdgGHIjmMNsSuUVwWyYz]$/) {
327 0         0 $str =~ s/^([+-])(0+)(\d:.*?|\d$)/$1$3/g;
328 0         0 $str =~ s/^(0+)(.+?)$/$2/g;
329             }
330             elsif ($modifier eq '-') {
331 0         0 $str =~ s/^ +//ge;
332             }
333             elsif ($modifier eq '0' and $suffix !~ /_/) {
334 0         0 $str =~ s/^( +)/'0' x length($1)/ge;
  0         0  
335             }
336             elsif ($modifier eq '^' and "$prefix$suffix" =~ /#/ and $format =~ /Z$/) {
337 0         0 $str = lc($str);
338             }
339             elsif ($modifier eq '^' and $format !~ /[pP]$/) {
340 0         0 $str = uc($str);
341             }
342             elsif ($modifier eq '#' and $format =~ /[aAbBh]$/) {
343 0         0 $str = uc($str);
344             }
345             elsif ($modifier eq '#' and $format =~ /[pZ]$/) {
346 0         0 $str = lc($str);
347             };
348              
349 0 0       0 last unless $prefix =~ s/(.)$//;
350 0         0 $suffix = "$modifier$suffix";
351 0         0 $modifier = $1;
352             };
353              
354 0         0 return $str;
355 8         52 };
356              
357             my $strftime_0z = sub {
358 0     0   0 my ($digits, $format, @t) = @_;
359 0         0 $digits --;
360 0         0 my $str = strftime($format, @t);
361 0 0       0 $str =~ /^([+-])(.*)$/ or return $format;
362 0         0 return $1 . sprintf "%0${digits}s", $2;
363 8         38 };
364              
365             # recursively handle modifiers
366 8         37 $fmt =~ s/%([_0\^#-]*)([_0\^#-])((?:[1-9][0-9]*)?:*[EO]?[a-zA-Z])/$strftime_modifier->($1, $2, $3, @t)/ge;
  0         0  
367 8         28 $fmt =~ s/%([_0\^#-]*)([_0\^#-])((?:[1-9][0-9]*)?[%])/$strftime_modifier->($1, $2, $3, @t) . '%'/ge;
  0         0  
368              
369             # numbers before character
370 8         19 $fmt =~ s/%([1-9][0-9]*)([EO]?[aAbBDeFhklnpPrRtTxXZ])/sprintf("%$1s", strftime("%$2", @t))/ge;
  0         0  
371 8         20 $fmt =~ s/%([1-9][0-9]*)([%])/sprintf("%$1s%%", '%')/ge;
  0         0  
372 8         18 $fmt =~ s/%([1-9][0-9]*)([EO]?[CdGgHIjmMsSuUVwWyY])/sprintf("%0$1s", strftime("%$2", @t))/ge;
  0         0  
373 8         14 $fmt =~ s/%([1-9][0-9]*)([N])/sprintf("%0$1.$1s", strftime("%$2", @t))/ge;
  0         0  
374 8         17 $fmt =~ s/%([1-9][0-9]*)(:*[z])/$strftime_0z->($1, "%$2", @t)/ge;
  0         0  
375              
376             # "E", "O", ":" modifiers
377 8         13 $fmt =~ s/%E([CcXxYy])/%$1/;
378 8         13 $fmt =~ s/%O([deHIMmSUuVWwy])/%$1/;
379 8         21 $fmt =~ s/%(:{0,3})?(z)/$format{$2}->(length $1, @t)/ge;
  1         7  
380              
381             # supported by Pure Perl
382 8         90 $fmt =~ s/%([$formats])/$format{$1}->(@t)/ge;
  15         54  
383              
384             # as-is if there is some modifiers left
385 8         31 $fmt =~ s/%([_0\^#-]+(?:[1-9][0-9]*)?|[_0\^#-]?(?:[1-9][0-9]*))([a-zA-Z%])/%%$1$2/;
386              
387 8         491 return strftime_orig($fmt, @t);
388             };
389              
390             1;
391              
392              
393             =head1 PERFORMANCE
394              
395             The PP module is about 10 times slower than XS module.
396              
397             =head1 SEE ALSO
398              
399             L.
400              
401             =head1 AUTHOR
402              
403             Piotr Roszatycki
404              
405             =head1 LICENSE
406              
407             Copyright (c) 2012-2014 Piotr Roszatycki .
408              
409             This is free software; you can redistribute it and/or modify it under
410             the same terms as perl itself.
411              
412             ISO 8601 functions:
413              
414             Copyright (c) 1991-2001, 2003-2007, 2009-2012 Free Software Foundation, Inc.
415              
416             This program is free software: you can redistribute it and/or modify
417             it under the terms of the GNU General Public License as published by
418             the Free Software Foundation; either version 3 of the License, or
419             (at your option) any later version.
420              
421             See L