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-2020 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   933372 use vars '$VERSION';
  14         133  
  14         1020  
11             $VERSION = '0.94';
12              
13              
14 14     14   94 use warnings;
  14         31  
  14         498  
15 14     14   83 use strict;
  14         55  
  14         454  
16              
17             #use Log::Report::Optional 'log-report';
18              
19 14     14   7503 use Encode qw/is_utf8 decode/;
  14         124436  
  14         1026  
20 14     14   5616 use Unicode::GCString ();
  14         223591  
  14         414  
21 14     14   6875 use HTML::Entities qw/encode_entities/;
  14         75946  
  14         1052  
22 14     14   105 use Scalar::Util qw/blessed reftype/;
  14         28  
  14         757  
23 14     14   7201 use POSIX qw/strftime/;
  14         90333  
  14         76  
24 14     14   27741 use Date::Parse qw/str2time/;
  14         106236  
  14         11645  
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 962 sub new(@) { my $class = shift; (bless {}, $class)->init( {@_} ) }
  24         120  
61              
62             sub init($)
63 24     24 0 66 { my ($self, $args) = @_;
64              
65 24         208 my $modif = $self->{SP_modif} = [ @default_modifiers ];
66 24 100       97 if(my $m = $args->{modifiers})
67 3         14 { unshift @$modif, @$m;
68             }
69              
70 24   100     145 my $s = $args->{serializers} || {};
71             my $seri = $self->{SP_seri}
72 24 100       218 = { %default_serializers, (ref $s eq 'ARRAY' ? @$s : %$s) };
73              
74 24         130 $self->encodeFor($args->{encode_for});
75 24   50     150 $self->{SP_missing} = $args->{missing_key} || \&_reportMissingKey;
76 24         93 $self;
77             }
78              
79             sub import(@)
80 14     14   166 { my $class = shift;
81 14         36 my ($oo, %func);
82 14         74 while(@_)
83 5 100       34 { last if $_[0] !~ m/^s?print[ip]$/;
84 3         12 $func{shift()} = 1;
85             }
86              
87 14 100 100     87 if(@_ && $_[0] eq 'oo') # only object oriented interface
88 1         3 { shift @_;
89 1 50       3 @_ and die "no options allowed at import with oo interface";
90 1         33 return;
91             }
92              
93 13         55 my $all = !keys %func;
94 13         51 my $f = $class->new(@_); # OO encapsulated
95 13         52 my ($pkg) = caller;
96 14     14   158 no strict 'refs';
  14         36  
  14         9715  
97 13 100 66 0   93 *{"$pkg\::printi"} = sub { $f->printi(@_) } if $all || $func{printi};
  11         72  
  0         0  
98 13 100 100 3   98 *{"$pkg\::sprinti"} = sub { $f->sprinti(@_) } if $all || $func{sprinti};
  12         132  
  3         101  
99 13 100 66 0   134 *{"$pkg\::printp"} = sub { $f->printp(@_) } if $all || $func{printp};
  11         47  
  0         0  
100 13 50 66 4   70 *{"$pkg\::sprintp"} = sub { $f->sprintp(@_) } if $all || $func{sprintp};
  13         54  
  4         2308  
101 13         18609 $class;
102             }
103              
104             #-------------
105              
106 2     2 1 5 sub addModifiers(@) {my $self = shift; unshift @{$self->{SP_modif}}, @_}
  2         5  
  2         9  
107              
108              
109              
110             sub encodeFor($)
111 26     26 1 113 { my ($self, $type) = (shift, shift);
112             defined $type
113 26 100       143 or return $self->{SP_enc} = undef;
114              
115 2         3 my %def;
116 2 50       6 if(ref $type eq 'HASH') {
117 0         0 %def = %$type;
118             }
119             else
120 2 50       8 { 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     8 my $excls = $def{exclude} || [];
126 2 50       14 my $regexes = join '|'
    50          
127             , map +(ref $_ eq 'Regexp' ? $_ : qr/(?:^|\.)\Q$_\E$/)
128             , ref $excls eq 'ARRAY' ? @$excls : $excls;
129 2         19 $def{SP_exclude} = qr/$regexes/o;
130              
131 2         8 $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 17897 { my ($self, $format) = (shift, shift);
140 102 100       388 my $args = @_==1 ? shift : {@_};
141             # $args may be a blessed HASH, for instance a Log::Report::Message
142              
143 102   100     515 $args->{_join} //= ', ';
144 102         225 local $args->{_format} = $format;
145              
146 102 100       473 my @frags = split /\{([^}]*)\}/, # enforce unicode
147             is_utf8($format) ? $format : decode(latin1 => $format);
148              
149 102         10220 my @parts;
150              
151             # Code parially duplicated for performance!
152 102 100       274 if(my $enc = $self->{SP_enc})
153 5         11 { my $encode = $enc->{encode};
154 5         6 my $exclude = $enc->{SP_exclude};
155 5 50       14 push @parts, $encode->($args->{_prepend}) if defined $args->{_prepend};
156 5         13 push @parts, $encode->(shift @frags);
157 5         123 while(@frags) {
158 4 50       32 my ($name, $tricks) = (shift @frags)
159 14     14   4428 =~ m!^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$!o or die $format;
  14         119  
  14         237  
160              
161 4 100       32 push @parts, $name =~ $exclude
162             ? $self->_expand($name, $tricks, $args)
163             : $encode->($self->_expand($name, $tricks, $args));
164              
165 4 50       54 push @parts, $encode->(shift @frags) if @frags;
166             }
167 5 50       86 push @parts, $encode->($args->{_append}) if defined $args->{_append};
168             }
169             else
170 97 50       249 { push @parts, $args->{_prepend} if defined $args->{_prepend};
171 97         186 push @parts, shift @frags;
172 97         210 while(@frags) {
173 101 50       586 (shift @frags) =~ /^\s*([\pL\p{Pc}\pM][\w.]*)\s*(.*?)\s*$/o
174             or die $format;
175 101         304 push @parts, $self->_expand($1, $2, $args);
176 101 100       349 push @parts, shift @frags if @frags;
177             }
178 97 50       235 push @parts, $args->{_append} if defined $args->{_append};
179             }
180              
181 102         754 join '', @parts;
182             }
183              
184             sub _expand($$$)
185 105     105   408 { my ($self, $key, $modifier, $args) = @_;
186              
187 105         167 my $value;
188 105 100       322 if(index($key, '.')== -1)
189             { # simple value
190 99 100       294 $value = exists $args->{$key} ? $args->{$key}
191             : $self->_missingKey($key, $args);
192 99         287 $value = $value->($self, $key, $args)
193             while ref $value eq 'CODE';
194             }
195             else
196 6         19 { my @parts = split /\./, $key;
197 6         13 my $key = shift @parts;
198 6 50       18 $value = exists $args->{$key} ? $args->{$key}
199             : $self->_missingKey($key, $args);
200              
201 6         20 $value = $value->($self, $key, $args)
202             while ref $value eq 'CODE';
203              
204 6   66     25 while(defined $value && @parts)
205 8 100 66     56 { if(blessed $value)
    100 33        
    50          
206 1         4 { my $method = shift @parts;
207 1 50       10 $value->can($method) or die "object $value cannot $method\n";
208 1         6 $value = $value->$method; # parameters not supported here
209             }
210             elsif(ref $value && reftype $value eq 'HASH')
211 6         15 { $value = $value->{shift @parts};
212             }
213             elsif(index($value, ':') != -1 || $::{$value.'::'})
214 1         3 { my $method = shift @parts;
215 1 50       12 $value->can($method) or die "class $value cannot $method\n";
216 1         5 $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         41 $value = $value->($self, $key, $args)
223             while ref $value eq 'CODE';
224             }
225             }
226              
227 105         165 my $mod;
228             STACKED:
229 105         290 while(length $modifier)
230 78         100 { my @modif = @{$self->{SP_modif}};
  78         235  
231 78         163 while(@modif)
232 268         480 { my ($regex, $callback) = (shift @modif, shift @modif);
233 268 100       3271 $modifier =~ s/^($regex)\s*// or next;
234              
235 78         229 $value = $callback->($self, $1, $value, $args);
236 78         630 next STACKED;
237             }
238 0         0 return "{unknown modifier '$modifier'}";
239             }
240              
241 105 100       355 my $seri = $self->{SP_seri}{defined $value ? ref $value : 'UNDEF'};
242 105 50       301 $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         2 my ($filename, $linenr);
255 1         11 while((my $pkg, $filename, $linenr) = caller $depth++)
256             { last unless
257 4 100 66     36 $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         8 undef;
268             }
269              
270             # See dedicated section in explanation in DETAILS
271             sub _modif_format($$$$)
272 15     15   49 { my ($self, $format, $value, $args) = @_;
273 15 50 33     80 defined $value && length $value or return undef;
274              
275 14     14   337263 use locale;
  14         8669  
  14         88  
276 15 50       55 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       98 $format =~ m/^\%([-+ ]?)([0-9]*)(?:\.([0-9]*))?([sS])$/
287             or return sprintf $format, $value; # simple: not a string
288              
289 13         50 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       75 my $s = Unicode::GCString->new
297             ( is_utf8($value) ? $value : decode(latin1 => $value));
298              
299 13         947 my $pad;
300 13 50       56 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     237 { return $value if !$max && $width && $width <= length $s;
      100        
313 12 100 66     67 $s->substr($max, length($s)-$max, '') if $max && length $s > $max;
314 12 100       47 $pad = $width ? $width - length $s : 0;
315             }
316              
317 12 100       108 $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   52 { my ($self, $format, $value, $args) = @_;
325 19 50 33     129 defined $value && length $value or return undef;
326              
327 19 100       61 return sprintf("%3d B", $value) if $value < 1000;
328              
329 14         34 my @scale = qw/kB MB GB TB PB EB ZB/;
330 14         25 $value /= 1024;
331              
332 14   100     57 while(@scale > 1 && $value > 999)
333 18         27 { shift @scale;
334 18         50 $value /= 1024;
335             }
336              
337 14 100       56 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   16 { my ($self, $format, $value, $args) = @_;
354 5 50 33     24 defined $value && length $value or return undef;
355              
356 5 100 100     41 return $1
357             if $value =~ /^\s*([0-9]+)\s*$/ && $1 < 2200;
358              
359 2 100       13 my $stamp = $value =~ /^\s*([0-9]+)\s*$/ ? $1 : str2time($value);
360 2 50       342 defined $stamp or return "year not found in '$value'";
361              
362 2         93 strftime "%Y", localtime($stamp);
363             }
364              
365             sub _modif_date($$$)
366 6     6   18 { my ($self, $format, $value, $args) = @_;
367 6 50 33     24 defined $value && length $value or return undef;
368              
369 6 100 100     67 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       7 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
374 1 50       183 defined $stamp or return "date not found in '$value'";
375              
376 1         37 strftime "%Y-%m-%d", localtime($stamp);
377             }
378              
379             sub _modif_time($$$)
380 4     4   14 { my ($self, $format, $value, $args) = @_;
381 4 50 33     15 defined $value && length $value or return undef;
382              
383 4 100 100     51 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       14 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
388 2 50       410 defined $stamp or return "time not found in '$value'";
389              
390 2         78 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     22 defined $value && length $value or return undef;
396              
397 2   50     13 my $kind = ($format =~ m/DT\(([^)]*)\)/ ? $1 : undef) || 'FT';
398 2 50       9 my $pattern = $dt_format{$kind}
399             or return "dt format $kind not known";
400              
401 2 50       16 my $stamp = $value =~ /\D/ ? str2time($value) : $value;
402 2 50       505 defined $stamp or return "dt not found in '$value'";
403              
404 2         88 strftime $pattern, localtime($stamp);
405             }
406              
407              
408             sub _modif_undef($$$)
409 12     12   31 { my ($self, $format, $value, $args) = @_;
410 12 100 66     39 return $value if defined $value && length $value;
411 7 50       47 $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   40943 { my @params = @{$_[0]};
  20         59  
432 20         36 my $printp = $params[0];
433 20         33 my ($printi, @iparam);
434 20         40 my ($pos, $maxpos) = (1, 1);
435 20   33     199 while(length $printp && $printp =~ s/^([^%]+)//s)
436 45         110 { $printi .= $1;
437 45 100       95 length $printp or last;
438 25 50       64 if($printp =~ s/^\%\%//)
439 0         0 { $printi .= '%';
440 0         0 next;
441             }
442 25 50       157 $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       72 $pos = $1 if $1;
452 25 100       97 my $width = !defined $3 ? '' : $3 eq '*' ? $params[$pos++] : $3;
    50          
453 25 100       70 my $prec = !defined $4 ? '' : $4 eq '*' ? $params[$pos++] : $4;
    100          
454 25 100       55 my $modif = !defined $5 ? '' : $5;
455 25         45 my $valpos= $pos++;
456 25 100       57 $maxpos = $pos if $pos > $maxpos;
457 25         62 push @iparam, "_$valpos" => $params[$valpos];
458 25 100 100     119 my $format= '%'.$2.($width || '').($prec ? ".$prec" : '').$6;
459 25 100       56 $format = '' if $format eq '%s';
460 25 100       73 my $sep = $modif.$format =~ m/^\w/ ? ' ' : '';
461 25         149 $printi .= "{_$valpos$sep$modif$format}";
462             }
463 20         68 splice @params, 0, $maxpos, @iparam;
464 20         84 ($printi, \@params);
465             }
466              
467             sub sprintp(@)
468 4     4 1 9 { my $self = shift;
469 4         13 my ($i, $iparam) = _printp_rewrite \@_;
470 4         20 $self->sprinti($i, {@$iparam});
471             }
472              
473             #-------------------
474              
475             1;