File Coverage

blib/lib/String/Formatter.pm
Criterion Covered Total %
statement 136 145 93.7
branch 22 36 61.1
condition 15 24 62.5
subroutine 26 27 96.3
pod 11 13 84.6
total 210 245 85.7


line stmt bran cond sub pod time code
1 5     5   140776 use strict;
  5         14  
  5         195  
2 5     5   28 use warnings;
  5         10  
  5         358  
3             package String::Formatter;
4             {
5             $String::Formatter::VERSION = '0.102084';
6             }
7             # ABSTRACT: build sprintf-like functions of your own
8              
9              
10             require 5.006;
11              
12 5     5   4929 use Params::Util ();
  5         48595  
  5         1437  
13             use Sub::Exporter -setup => {
14             exports => {
15             stringf => sub {
16 2         231 my ($class, $name, $arg, $col) = @_;
17 2         6 my $formatter = $class->new($arg);
18 2     2   10 return sub { $formatter->format(@_) };
  2         614  
19             },
20             method_stringf => sub {
21 0         0 my ($class, $name, $arg, $col) = @_;
22 0         0 my $formatter = $class->new({
23             input_processor => 'require_single_input',
24             string_replacer => 'method_replace',
25             %$arg,
26             });
27 0         0 return sub { $formatter->format(@_) };
  0         0  
28             },
29             named_stringf => sub {
30 1         23 my ($class, $name, $arg, $col) = @_;
31 1         6 my $formatter = $class->new({
32             input_processor => 'require_named_input',
33             string_replacer => 'named_replace',
34             %$arg,
35             });
36 1     1   6 return sub { $formatter->format(@_) };
  1         341  
37             },
38             indexed_stringf => sub {
39 1         22 my ($class, $name, $arg, $col) = @_;
40 1         6 my $formatter = $class->new({
41             input_processor => 'require_arrayref_input',
42             string_replacer => 'indexed_replace',
43             %$arg,
44             });
45 1     1   6 return sub { $formatter->format(@_) };
  1         308  
46             },
47             },
48 5     5   6314 };
  5         48295  
  5         121  
49              
50             my %METHODS;
51             BEGIN {
52 5     5   33 %METHODS = (
53             format_hunker => 'hunk_simply',
54             input_processor => 'return_input',
55             string_replacer => 'positional_replace',
56             hunk_formatter => 'format_simply',
57             );
58              
59 5     5   2474 no strict 'refs';
  5         12  
  5         526  
60 5         20 for my $method (keys %METHODS) {
61 20     83   144 *$method = sub { $_[0]->{ $method } };
  83         177  
62              
63 20         41 my $default = "default_$method";
64 20     30   6588 *$default = sub { $METHODS{ $method } };
  30         89  
65             }
66             }
67              
68              
69             sub default_codes {
70 11     11 0 35 return {};
71             }
72              
73             sub new {
74 11     11 1 2337 my ($class, $arg) = @_;
75              
76 11         36 my $_codes = {
77 11 50       73 %{ $class->default_codes },
78 11         16 %{ $arg->{codes} || {} },
79             };
80              
81 11         46 my $self = bless { codes => $_codes } => $class;
82              
83 11         42 for (keys %METHODS) {
84 44   66     146 $self->{ $_ } = $arg->{ $_ } || do {
85             my $default_method = "default_$_";
86             $class->$default_method;
87             };
88              
89 44 50       243 $self->{$_} = $self->can($self->{$_}) unless ref $self->{$_};
90             }
91              
92 11         35 my $codes = $self->codes;
93              
94 11         31 return $self;
95             }
96              
97 32     32 0 66 sub codes { $_[0]->{codes} }
98              
99              
100             sub format {
101 21     21 1 3582 my $self = shift;
102 21         31 my $format = shift;
103              
104 21 50       111 Carp::croak("not enough arguments for stringf-based format")
105             unless defined $format;
106              
107 21         45 my $hunker = $self->format_hunker;
108 21         50 my $hunks = $self->$hunker($format);
109              
110 21         56 my $processor = $self->input_processor;
111 21         67 my $input = $self->$processor([ @_ ]);
112              
113 21         53 my $replacer = $self->string_replacer;
114 21         53 $self->$replacer($hunks, $input);
115              
116 20         114 my $formatter = $self->hunk_formatter;
117 20   100     105 ref($_) and $_ = $self->$formatter($_) for @$hunks;
118              
119 20         51 my $string = join q{}, @$hunks;
120              
121 20         80 return $string;
122             }
123              
124              
125             my $regex = qr/
126             (% # leading '%'
127             (-)? # left-align, rather than right
128             ([0-9]+)? # (optional) minimum field width
129             (?:\.([0-9]*))? # (optional) maximum field width
130             (?:{(.*?)})? # (optional) stuff inside
131             (\S) # actual format character
132             )
133             /x;
134              
135             sub hunk_simply {
136 21     21 1 33 my ($self, $string) = @_;
137              
138 21         28 my @to_fmt;
139 21         28 my $pos = 0;
140              
141 21         644 while ($string =~ m{\G(.*?)$regex}gs) {
142 36         306 push @to_fmt, $1, {
143             alignment => $3,
144             min_width => $4,
145             max_width => $5,
146              
147             literal => $2,
148             argument => $6,
149             conversion => $7,
150             };
151              
152 36 100       133 $to_fmt[-1] = '%' if $to_fmt[-1]{literal} eq '%%';
153              
154 36         318 $pos = pos $string;
155             }
156              
157 21 100       79 push @to_fmt, substr $string, $pos if $pos < length $string;
158              
159 21         61 return \@to_fmt;
160             }
161              
162              
163             sub return_input {
164 14     14 1 21 return $_[1];
165             }
166              
167              
168             sub require_named_input {
169 3     3 1 7 my ($self, $args) = @_;
170              
171 3 50 33     31 Carp::croak("routine must be called with exactly one hashref arg")
172             if @$args != 1 or ! Params::Util::_HASHLIKE($args->[0]);
173              
174 3         7 return $args->[0];
175             }
176              
177              
178             sub require_arrayref_input {
179 1     1 1 3 my ($self, $args) = @_;
180              
181 1 50 33     10 Carp::croak("routine must be called with exactly one arrayref arg")
182             if @$args != 1 or ! Params::Util::_ARRAYLIKE($args->[0]);
183              
184 1         2 return $args->[0];
185             }
186              
187              
188             sub require_single_input {
189 3     3 1 6 my ($self, $args) = @_;
190              
191 3 50       10 Carp::croak("routine must be called with exactly one argument after string")
192             if @$args != 1;
193              
194 3         7 return $args->[0];
195             }
196              
197              
198             sub forbid_input {
199 0     0 1 0 my ($self, $args) = @_;
200              
201 0 0       0 Carp::croak("routine must be called with no arguments after format string")
202             if @$args;
203              
204 0         0 return $args;
205             }
206              
207              
208             sub __closure_replace {
209 15     15   22 my ($closure) = @_;
210              
211             return sub {
212 18     18   26 my ($self, $hunks, $input) = @_;
213              
214 18         27 my $heap = {};
215 18         41 my $code = $self->codes;
216              
217 18         47 for my $i (grep { ref $hunks->[$_] } 0 .. $#$hunks) {
  75         144  
218 28         70 my $hunk = $hunks->[ $i ];
219 28         47 my $conv = $code->{ $hunk->{conversion} };
220              
221 28 100       256 Carp::croak("Unknown conversion in stringf: $hunk->{conversion}")
222             unless defined $conv;
223              
224 27 100       56 if (ref $conv) {
225 13         63 $hunks->[ $i ]->{replacement} = $self->$closure({
226             conv => $conv,
227             hunk => $hunk,
228             heap => $heap,
229             input => $input,
230             });
231             } else {
232 14         42 $hunks->[ $i ]->{replacement} = $conv;
233             }
234             }
235 15         8515 };
236             }
237              
238             # $self->$string_replacer($hunks, $input);
239             BEGIN {
240             *positional_replace = __closure_replace(sub {
241 5         10 my ($self, $arg) = @_;
242 5         17 local $_ = $arg->{input}->[ $arg->{heap}{nth}++ ];
243 5         23 return $arg->{conv}->($self, $_, $arg->{hunk}{argument});
244 5     5   32 });
245              
246             *named_replace = __closure_replace(sub {
247 6         10 my ($self, $arg) = @_;
248 6         17 local $_ = $arg->{input}->{ $arg->{hunk}{argument} };
249 6         23 return $arg->{conv}->($self, $_, $arg->{hunk}{argument});
250 5         22 });
251              
252             *indexed_replace = __closure_replace(sub {
253 2         3 my ($self, $arg) = @_;
254 2         6 local $_ = $arg->{input}->[ $arg->{hunk}{argument} ];
255 2         7 return $arg->{conv}->($self, $_, $arg->{hunk}{argument});
256 5         29 });
257             }
258              
259              
260             # should totally be rewritten with commonality with keyed_replace factored out
261             sub method_replace {
262 2     2 1 5 my ($self, $hunks, $input) = @_;
263              
264 2         3 my $heap = {};
265 2         5 my $code = $self->codes;
266              
267 2         6 for my $i (grep { ref $hunks->[$_] } 0 .. $#$hunks) {
  10         25  
268 5         25 my $hunk = $hunks->[ $i ];
269 5         10 my $conv = $code->{ $hunk->{conversion} };
270              
271 5 50       12 Carp::croak("Unknown conversion in stringf: $hunk->{conversion}")
272             unless defined $conv;
273              
274 5 100       10 if (ref $conv) {
275 3         5 local $_ = $input;
276 3         11 $hunks->[ $i ]->{replacement} = $input->$conv($hunk->{argument});
277             } else {
278 2         2 local $_ = $input;
279 2 100       18 $hunks->[ $i ]->{replacement} = $input->$conv(
280             defined $hunk->{argument} ? $hunk->{argument} : ()
281             );
282             }
283             }
284             }
285              
286              
287             # should totally be rewritten with commonality with method_replace factored out
288             sub keyed_replace {
289 1     1 1 2 my ($self, $hunks, $input) = @_;
290              
291 1         2 my $heap = {};
292 1         3 my $code = $self->codes;
293              
294 1         3 for my $i (grep { ref $hunks->[$_] } 0 .. $#$hunks) {
  4         7  
295 2         3 my $hunk = $hunks->[ $i ];
296 2         6 my $conv = $code->{ $hunk->{conversion} };
297              
298 2 50       4 Carp::croak("Unknown conversion in stringf: $hunk->{conversion}")
299             unless defined $conv;
300              
301 2 50       4 if (ref $conv) {
302 0         0 local $_ = $input;
303 0         0 $hunks->[ $i ]->{replacement} = $input->$conv($hunk->{argument});
304             } else {
305 2         2 local $_ = $input;
306 2         7 $hunks->[ $i ]->{replacement} = $input->{$conv};
307             }
308             }
309             }
310              
311              
312             sub format_simply {
313 34     34 1 50 my ($self, $hunk) = @_;
314              
315 34         54 my $replacement = $hunk->{replacement};
316 34         46 my $replength = length $replacement;
317              
318 34   100     127 my $alignment = $hunk->{alignment} || '';
319 34   100     119 my $min_width = $hunk->{min_width} || 0;
320 34   66     115 my $max_width = $hunk->{max_width} || $replength;
321              
322 34 50 66     151 $min_width ||= $replength > $min_width ? $min_width : $replength;
323 34 0 33     59 $max_width ||= $max_width > $replength ? $max_width : $replength;
324              
325 34         317 return sprintf "%$alignment${min_width}.${max_width}s", $replacement;
326             }
327              
328             1;
329              
330             __END__