File Coverage

blib/lib/String/Print.pm
Criterion Covered Total %
statement 222 242 91.7
branch 128 178 71.9
condition 59 95 62.1
subroutine 32 36 88.8
pod 7 8 87.5
total 448 559 80.1


line stmt bran cond sub pod time code
1             # Copyrights 2016-2017 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5 14     14   286484 use warnings;
  14         45  
  14         560  
6 14     14   97 use strict;
  14         33  
  14         709  
7              
8             package String::Print;
9 14     14   92 use vars '$VERSION';
  14         35  
  14         929  
10             $VERSION = '0.92';
11              
12              
13             #use Log::Report::Optional 'log-report';
14              
15 14     14   7023 use Encode qw/is_utf8 decode/;
  14         125150  
  14         1061  
16 14     14   5380 use Unicode::GCString ();
  14         206180  
  14         410  
17 14     14   7185 use HTML::Entities qw/encode_entities/;
  14         72580  
  14         1648  
18 14     14   129 use Scalar::Util qw/blessed reftype/;
  14         30  
  14         1139  
19 14     14   6969 use POSIX qw/strftime/;
  14         74142  
  14         120  
20 14     14   25258 use Date::Parse qw/str2time/;
  14         83527  
  14         9734  
21              
22             my @default_modifiers =
23             ( qr/\%\S+/ => \&_modif_format
24             , qr/BYTES\b/ => \&_modif_bytes
25             , qr/YEAR\b/ => \&_modif_year
26             , qr/DT\([^)]*\)/ => \&_modif_dt
27             , qr/DT\b/ => \&_modif_dt
28             , qr/DATE\b/ => \&_modif_date
29             , qr/TIME\b/ => \&_modif_time
30             , qr!//(?:\"[^"]*\"|\'[^']*\'|\w+)! => \&_modif_undef
31             );
32              
33             my %default_serializers =
34             ( UNDEF => sub { 'undef' }
35             , '' => sub { $_[1] }
36             , SCALAR => sub { ${$_[1]} // shift->{SP_seri}{UNDEF}->(@_) }
37             , ARRAY =>
38             sub { my $v = $_[1]; my $join = $_[2]{_join} // ', ';
39             join $join, map +($_ // 'undef'), @$v;
40             }
41             , HASH =>
42             sub { my $v = $_[1];
43             join ', ', map "$_ => ".($v->{$_} // 'undef'), sort keys %$v;
44             }
45             # CODE value has different purpose
46             );
47              
48             my %predefined_encodings =
49             ( HTML =>
50             { exclude => [ qr/html$/i ]
51             , encode => sub { encode_entities $_[0] }
52             }
53             );
54              
55              
56 24     24 1 173 sub new(@) { my $class = shift; (bless {}, $class)->init( {@_} ) }
  24         145  
57              
58             sub init($)
59 24     24 0 77 { my ($self, $args) = @_;
60              
61 24         259 my $modif = $self->{SP_modif} = [ @default_modifiers ];
62 24 100       117 if(my $m = $args->{modifiers})
63 3         15 { unshift @$modif, @$m;
64             }
65              
66 24   100     163 my $s = $args->{serializers} || {};
67             my $seri = $self->{SP_seri}
68 24 100       229 = { %default_serializers, (ref $s eq 'ARRAY' ? @$s : %$s) };
69              
70 24         152 $self->encodeFor($args->{encode_for});
71 24   50     180 $self->{SP_missing} = $args->{missing_key} || \&_reportMissingKey;
72 24         100 $self;
73             }
74              
75             sub import(@)
76 14     14   205 { my $class = shift;
77 14         46 my ($oo, %func);
78 14         85 while(@_)
79 5 100       40 { last if $_[0] !~ m/^s?print[ip]$/;
80 3         15 $func{shift()} = 1;
81             }
82              
83 14 100 100     104 if(@_ && $_[0] eq 'oo') # only object oriented interface
84 1         3 { shift @_;
85 1 50       4 @_ and die "no options allowed at import with oo interface";
86 1         27 return;
87             }
88              
89 13         63 my $all = !keys %func;
90 13         64 my $f = $class->new(@_); # OO encapsulated
91 13         64 my ($pkg) = caller;
92 14     14   206 no strict 'refs';
  14         34  
  14         7697  
93 13 100 66 0   103 *{"$pkg\::printi"} = sub { $f->printi(@_) } if $all || $func{printi};
  11         83  
  0         0  
94 13 100 100 3   108 *{"$pkg\::sprinti"} = sub { $f->sprinti(@_) } if $all || $func{sprinti};
  12         67  
  3         25  
95 13 100 66 0   93 *{"$pkg\::printp"} = sub { $f->printp(@_) } if $all || $func{printp};
  11         51  
  0         0  
96 13 50 66 4   91 *{"$pkg\::sprintp"} = sub { $f->sprintp(@_) } if $all || $func{sprintp};
  13         63  
  4         1992  
97 13         17040 $class;
98             }
99              
100             #-------------
101              
102 2     2 1 5 sub addModifiers(@) {my $self = shift; unshift @{$self->{SP_modif}}, @_}
  2         6  
  2         11  
103              
104              
105              
106             sub encodeFor($)
107 26     26 1 113 { my ($self, $type) = (shift, shift);
108             defined $type
109 26 100       131 or return $self->{SP_enc} = undef;
110              
111 2         5 my %def;
112 2 50       7 if(ref $type eq 'HASH') {
113 0         0 %def = %$type;
114             }
115             else
116 2 50       9 { my $def = $predefined_encodings{$type}
117             or die "ERROR: unknown output encoding type $type\n";
118 2         10 %def = (%$def, @_);
119             }
120              
121 2   50     8 my $excls = $def{exclude} || [];
122 2 50       93 my $regexes = join '|'
    50          
123             , map +(ref $_ eq 'Regexp' ? $_ : qr/(?:^|\.)\Q$_\E$/)
124             , ref $excls eq 'ARRAY' ? @$excls : $excls;
125 2         21 $def{SP_exclude} = qr/$regexes/o;
126              
127 2         7 $self->{SP_enc} = \%def;
128             }
129              
130             # You cannot have functions and methods with the same name in OODoc and POD
131              
132             #-------------------
133              
134             sub sprinti($@)
135 102     102 1 15900 { my ($self, $format) = (shift, shift);
136 102 100       482 my $args = @_==1 ? shift : {@_};
137             # $args may be a blessed HASH, for instance a Log::Report::Message
138              
139 102   100     585 $args->{_join} //= ', ';
140 102         240 local $args->{_format} = $format;
141              
142 102 100       612 my @frags = split /\{([^}]*)\}/, # enforce unicode
143             is_utf8($format) ? $format : decode(latin1 => $format);
144              
145 102         11269 my @parts;
146              
147             # Code parially duplicated for performance!
148 102 100       293 if(my $enc = $self->{SP_enc})
149 5         13 { my $encode = $enc->{encode};
150 5         9 my $exclude = $enc->{SP_exclude};
151 5 50       18 push @parts, $encode->($args->{_prepend}) if defined $args->{_prepend};
152 5         16 push @parts, $encode->(shift @frags);
153 5         145 while(@frags) {
154 4 50       37 my ($name, $tricks) = (shift @frags)
155 14     14   4434 =~ m!^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$!o or die $format;
  14         117  
  14         272  
156              
157 4 100       38 push @parts, $name =~ $exclude
158             ? $self->_expand($name, $tricks, $args)
159             : $encode->($self->_expand($name, $tricks, $args));
160              
161 4 50       59 push @parts, $encode->(shift @frags) if @frags;
162             }
163 5 50       122 push @parts, $encode->($args->{_append}) if defined $args->{_append};
164             }
165             else
166 97 50       278 { push @parts, $args->{_prepend} if defined $args->{_prepend};
167 97         204 push @parts, shift @frags;
168 97         255 while(@frags) {
169 101 50       602 (shift @frags) =~ /^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$/o
170             or die $format;
171 101         333 push @parts, $self->_expand($1, $2, $args);
172 101 100       392 push @parts, shift @frags if @frags;
173             }
174 97 50       263 push @parts, $args->{_append} if defined $args->{_append};
175             }
176              
177 102         742 join '', @parts;
178             }
179              
180             sub _expand($$$)
181 105     105   406 { my ($self, $key, $modifier, $args) = @_;
182              
183 105         185 my $value;
184 105 100       358 if(index($key, '.')== -1)
185             { # simple value
186 99 100       309 $value = exists $args->{$key} ? $args->{$key}
187             : $self->_missingKey($key, $args);
188 99         295 $value = $value->($self, $key, $args)
189             while ref $value eq 'CODE';
190             }
191             else
192 6         19 { my @parts = split /\./, $key;
193 6         11 my $key = shift @parts;
194 6 50       20 $value = exists $args->{$key} ? $args->{$key}
195             : $self->_missingKey($key, $args);
196              
197 6         17 $value = $value->($self, $key, $args)
198             while ref $value eq 'CODE';
199              
200 6   66     27 while(defined $value && @parts)
201 8 100 66     61 { if(blessed $value)
    100 33        
    50          
202 1         3 { my $method = shift @parts;
203 1 50       10 $value->can($method) or die "object $value cannot $method\n";
204 1         4 $value = $value->$method; # parameters not supported here
205             }
206             elsif(ref $value && reftype $value eq 'HASH')
207 6         13 { $value = $value->{shift @parts};
208             }
209             elsif(index($value, ':') != -1 || $::{$value.'::'})
210 1         3 { my $method = shift @parts;
211 1 50       7 $value->can($method) or die "class $value cannot $method\n";
212 1         4 $value = $value->$method; # parameters not supported here
213             }
214             else
215 0         0 { die "not a HASH, object, or class at $parts[0] in $key\n";
216             }
217              
218 8         45 $value = $value->($self, $key, $args)
219             while ref $value eq 'CODE';
220             }
221             }
222              
223 105         186 my $mod;
224             STACKED:
225 105         333 while(length $modifier)
226 78         125 { my @modif = @{$self->{SP_modif}};
  78         258  
227 78         189 while(@modif)
228 268         540 { my ($regex, $callback) = (shift @modif, shift @modif);
229 268 100       3265 $modifier =~ s/^($regex)\s*// or next;
230              
231 78         251 $value = $callback->($self, $1, $value, $args);
232 78         669 next STACKED;
233             }
234 0         0 return "{unknown modifier '$modifier'}";
235             }
236              
237 105 100       322 my $seri = $self->{SP_seri}{defined $value ? ref $value : 'UNDEF'};
238 105 50       346 $seri ? $seri->($self, $value, $args) : "$value";
239             }
240              
241             sub _missingKey($$)
242 1     1   4 { my ($self, $key, $args) = @_;
243 1         7 $self->{SP_missing}->($self, $key, $args);
244             }
245              
246             sub _reportMissingKey($$)
247 1     1   4 { my ($self, $key, $args) = @_;
248              
249 1         2 my $depth = 0;
250 1         2 my ($filename, $linenr);
251 1         14 while((my $pkg, $filename, $linenr) = caller $depth++)
252             { last unless
253 4 100 66     55 $pkg->isa(__PACKAGE__)
254             || $pkg->isa('Log::Report::Minimal::Domain');
255             }
256              
257             warn $self->sprinti
258             ( "Missing key '{key}' in format '{format}', file {fn} line {line}\n"
259             , key => $key, format => $args->{_format}
260 1         9 , fn => $filename, line => $linenr
261             );
262              
263 1         8 undef;
264             }
265              
266             # See dedicated section in explanation in DETAILS
267             sub _modif_format($$$$)
268 15     15   45 { my ($self, $format, $value, $args) = @_;
269 15 50 33     93 defined $value && length $value or return undef;
270              
271 14     14   310055 use locale;
  14         7841  
  14         93  
272 15 50       55 if(ref $value eq 'ARRAY')
    50          
273 0 0       0 { @$value or return '(none)';
274 0         0 return [ map $self->_format_print($format, $_, $args), @$value ] ;
275             }
276             elsif(ref $value eq 'HASH')
277 0 0       0 { keys %$value or return '(none)';
278 0         0 return { map +($_ => $self->_format_print($format, $value->{$_}, $args))
279             , keys %$value } ;
280             }
281              
282 15 100       88 $format =~ m/^\%([-+ ]?)([0-9]*)(?:\.([0-9]*))?([sS])$/
283             or return sprintf $format, $value; # simple: not a string
284              
285 13         54 my ($padding, $width, $max, $u) = ($1, $2, $3, $4);
286              
287             # String formats like %10s or %-3.5s count characters, not width.
288             # String formats like %10S or %-3.5S are subject to column width.
289             # The latter means: minimal 3 chars, max 5, padding right with blanks.
290             # All inserted strings are upgraded into utf8.
291              
292 13 100       79 my $s = Unicode::GCString->new
293             ( is_utf8($value) ? $value : decode(latin1 => $value));
294              
295 13         876 my $pad;
296 13 50       37 if($u eq 'S')
297             { # too large to fit
298 0 0 0     0 return $value if !$max && $width && $width <= $s->columns;
      0        
299              
300             # wider than max. Waiting for $s->trim($max) if $max, see
301             # https://rt.cpan.org/Public/Bug/Display.html?id=84549
302 0   0     0 $s->substr(-1, 1, '')
303             while $max && $s->columns > $max;
304              
305 0 0       0 $pad = $width ? $width - $s->columns : 0;
306             }
307             else # $u eq 's'
308 13 100 100     263 { return $value if !$max && $width && $width <= length $s;
      100        
309 12 100 66     73 $s->substr($max, length($s)-$max, '') if $max && length $s > $max;
310 12 100       47 $pad = $width ? $width - length $s : 0;
311             }
312              
313 12 100       111 $pad==0 ? $s->as_string
    100          
314             : $padding eq '-' ? $s->as_string . (' ' x $pad)
315             : (' ' x $pad) . $s->as_string;
316             }
317              
318             # See dedicated section in explanation in DETAILS
319             sub _modif_bytes($$$)
320 19     19   49 { my ($self, $format, $value, $args) = @_;
321 19 50 33     122 defined $value && length $value or return undef;
322              
323 19 100       57 return sprintf("%3d B", $value) if $value < 1000;
324              
325 14         34 my @scale = qw/kB MB GB TB PB EB ZB/;
326 14         26 $value /= 1024;
327              
328 14   100     61 while(@scale > 1 && $value > 999)
329 18         26 { shift @scale;
330 18         61 $value /= 1024;
331             }
332              
333 14 100       50 return sprintf "%3d $scale[0]", $value + 0.5
334             if $value > 9.949;
335              
336 8         57 sprintf "%3.1f $scale[0]", $value;
337             }
338              
339             # Be warned: %F and %T (from C99) are not supported on Windows
340             my %dt_format =
341             ( ASC => '%a %b %e %H:%M:%S %Y'
342             , ISO => '%Y-%m-%dT%H:%M:%S%z'
343             , RFC2822 => '%a, %d %b %Y %H:%M:%S %z'
344             , RFC822 => '%a, %d %b %y %H:%M:%S %z'
345             , FT => '%Y-%m-%d %H:%M:%S'
346             );
347              
348             sub _modif_year($$$)
349 5     5   16 { my ($self, $format, $value, $args) = @_;
350 5 50 33     29 defined $value && length $value or return undef;
351              
352 5 100 100     35 return $value
353             if $value !~ /\D/ && $value < 2200;
354              
355 3 100       16 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
356 3 50       810 defined $stamp or return "year not found in '$value'";
357              
358 3         190 strftime "%Y", localtime($stamp);
359             }
360              
361             sub _modif_date($$$)
362 6     6   19 { my ($self, $format, $value, $args) = @_;
363 6 50 33     31 defined $value && length $value or return undef;
364              
365 6 100 100     74 return sprintf("%4d-%02d-%02d", $1, $2, $3)
366             if $value =~ m!^\s*([0-9]{4})[:/.-]([0-9]?[0-9])[:/.-]([0-9]?[0-9])\s*$!
367             || $value =~ m!^\s*([0-9]{4})([0-9][0-9])([0-9][0-9])\s*$!;
368              
369 1 50       7 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
370 1 50       192 defined $stamp or return "date not found in '$value'";
371              
372 1         43 strftime "%Y-%m-%d", localtime($stamp);
373             }
374              
375             sub _modif_time($$$)
376 4     4   13 { my ($self, $format, $value, $args) = @_;
377 4 50 33     21 defined $value && length $value or return undef;
378              
379 4 100 100     50 return sprintf "%02d:%02d:%02d", $1, $2, $3||0
      66        
380             if $value =~ m!^\s*(0?[0-9]|1[0-9]|2[0-3])\:([0-5]?[0-9])(?:\:([0-5]?[0-9]))?\s*$!
381             || $value =~ m!^\s*(0[0-9]|1[0-9]|2[0-3])([0-5][0-9])(?:([0-5][0-9]))?\s*$!;
382              
383 2 50       13 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
384 2 50       401 defined $stamp or return "time not found in '$value'";
385              
386 2         86 strftime "%H:%M:%S", localtime($stamp);
387             }
388              
389             sub _modif_dt($$$)
390 4     4   14 { my ($self, $format, $value, $args) = @_;
391 4 100 66     24 defined $value && length $value or return undef;
392              
393 2   50     16 my $kind = ($format =~ m/DT\(([^)]*)\)/ ? $1 : undef) || 'FT';
394 2 50       11 my $pattern = $dt_format{$kind}
395             or return "dt format $kind not known";
396              
397 2 50       22 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
398 2 50       563 defined $stamp or return "dt not found in '$value'";
399              
400 2         110 strftime $pattern, localtime($stamp);
401             }
402              
403              
404             sub _modif_undef($$$)
405 12     12   34 { my ($self, $format, $value, $args) = @_;
406 12 100 66     51 return $value if defined $value && length $value;
407 7 50       50 $format =~ m!//"([^"]*)"|//'([^']*)'|//(\w*)! ? $+ : undef;
408             }
409              
410              
411             sub printi($$@)
412 0     0 1 0 { my $self = shift;
413 0 0       0 my $fh = ref $_[0] eq 'GLOB' ? shift : select;
414 0         0 $fh->print($self->sprinti(@_));
415             }
416              
417              
418              
419             sub printp($$@)
420 0     0 1 0 { my $self = shift;
421 0 0       0 my $fh = ref $_[0] eq 'GLOB' ? shift : select;
422 0         0 $fh->print($self->sprintp(@_));
423             }
424              
425              
426             sub _printp_rewrite($)
427 20     20   36742 { my @params = @{$_[0]};
  20         69  
428 20         42 my $printp = $params[0];
429 20         40 my ($printi, @iparam);
430 20         45 my ($pos, $maxpos) = (1, 1);
431 20   33     191 while(length $printp && $printp =~ s/^([^%]+)//s)
432 45         137 { $printi .= $1;
433 45 100       127 length $printp or last;
434 25 50       74 if($printp =~ s/^\%\%//)
435 0         0 { $printi .= '%';
436 0         0 next;
437             }
438 25 50       167 $printp =~ s/\%(?:([0-9]+)\$)? # 1=positional
439             ([-+0 \#]*) # 2=flags
440             ([0-9]*|\*)? # 3=width
441             (?:\.([0-9]*|\*))? # 4=precission
442             (?:\{ ([^}]*) \})? # 5=modifiers
443             (\w) # 6=conversion
444             //x
445             or die "format error at '$printp' in '$params[0]'";
446              
447 25 100       90 $pos = $1 if $1;
448 25 100       115 my $width = !defined $3 ? '' : $3 eq '*' ? $params[$pos++] : $3;
    50          
449 25 100       88 my $prec = !defined $4 ? '' : $4 eq '*' ? $params[$pos++] : $4;
    100          
450 25 100       69 my $modif = !defined $5 ? '' : $5;
451 25         47 my $valpos= $pos++;
452 25 100       77 $maxpos = $pos if $pos > $maxpos;
453 25         80 push @iparam, "_$valpos" => $params[$valpos];
454 25 100 100     145 my $format= '%'.$2.($width || '').($prec ? ".$prec" : '').$6;
455 25 100       69 $format = '' if $format eq '%s';
456 25 100       88 my $sep = $modif.$format =~ m/^\w/ ? ' ' : '';
457 25         193 $printi .= "{_$valpos$sep$modif$format}";
458             }
459 20         66 splice @params, 0, $maxpos, @iparam;
460 20         92 ($printi, \@params);
461             }
462              
463             sub sprintp(@)
464 4     4 1 11 { my $self = shift;
465 4         15 my ($i, $iparam) = _printp_rewrite \@_;
466 4         30 $self->sprinti($i, {@$iparam});
467             }
468              
469             #-------------------
470              
471             1;