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