File Coverage

blib/lib/POSIX/strftime/GNU/PP.pm
Criterion Covered Total %
statement 84 118 71.1
branch 1 22 4.5
condition 1 39 2.5
subroutine 22 24 91.6
pod 1 1 100.0
total 109 204 53.4


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