File Coverage

blib/lib/String/Errf.pm
Criterion Covered Total %
statement 111 112 99.1
branch 54 60 90.0
condition 12 15 80.0
subroutine 29 30 96.6
pod 0 5 0.0
total 206 222 92.7


line stmt bran cond sub pod time code
1 2     2   97015 use strict;
  2         5  
  2         80  
2 2     2   11 use warnings;
  2         5  
  2         137  
3             package String::Errf;
4             {
5             $String::Errf::VERSION = '0.007';
6             } # I really wanted to call it String::Fister.
7 2     2   1779 use String::Formatter 0.102081 ();
  2         42916  
  2         56  
8 2     2   2099 use parent 'String::Formatter';
  2         654  
  2         13  
9             # ABSTRACT: a simple sprintf-like dialect
10              
11 2     2   121 use Scalar::Util ();
  2         5  
  2         108  
12              
13              
14 2     2   12 use Carp ();
  2         5  
  2         32  
15 2     2   2405 use Time::Piece ();
  2         25095  
  2         60  
16 2     2   25 use Params::Util ();
  2         5  
  2         191  
17              
18             use Sub::Exporter -setup => {
19             exports => {
20             errf => sub {
21 2         302 my ($class) = @_;
22 2         1487 my $fmt = $class->new;
23 2     70   45 return sub { $fmt->format(@_) };
  70         47849  
24             },
25             }
26 2     2   13 };
  2         3  
  2         35  
27              
28             sub default_codes {
29             return {
30 2     2 0 35 i => '_format_int',
31             f => '_format_float',
32             t => '_format_timestamp',
33             s => '_format_string',
34             n => '_format_numbered',
35             N => '_format_numbered',
36             };
37             }
38              
39 2     2 0 41 sub default_input_processor { 'require_named_input' }
40 2     2 0 61 sub default_format_hunker { '__hunk_errf' }
41 2     2 0 30 sub default_string_replacer { '__replace_errf' }
42 2     2 0 22 sub default_hunk_formatter { '__format_errf' }
43              
44             my $regex = qr/
45             (% # leading '%'
46             (?:{ # {
47             ([^;]*?) # mandatory argument name
48             (?: ; ([^\}]*?) )? # optional extras after semicolon
49             }) # }
50             ($|.) # potential conversion character
51             )
52             /xi;
53              
54             sub __hunk_errf {
55 70     70   659 my ($self, $string) = @_;
56              
57 70         82 my @to_fmt;
58 70         115 my $pos = 0;
59              
60 70         1325 while ($string =~ m{\G(.*?)$regex}gs) {
61 78         603 push @to_fmt, $1, {
62             literal => $2,
63             argument => $3,
64             extra => $4,
65             conversion => $5,
66             };
67              
68 78         497 $pos = pos $string;
69             }
70              
71 70 100       174 push @to_fmt, substr $string, $pos if $pos < length $string;
72              
73 70         254 return \@to_fmt;
74             }
75              
76             sub __replace_errf {
77 70     70   1136 my ($self, $hunks, $input) = @_;
78              
79 70         100 my $heap = {};
80 70         235 my $code = $self->codes;
81              
82 70         321 for my $i (grep { ref $hunks->[$_] } 0 .. $#$hunks) {
  157         464  
83 74         104 my $hunk = $hunks->[ $i ];
84 74         140 my $conv = $code->{ $hunk->{conversion} };
85              
86 74 100       615 Carp::croak("Unknown conversion in stringf: $hunk->{conversion}")
87             unless defined $conv;
88              
89 72         173 $hunk->{replacement} = $input->{ $hunk->{argument} };
90 72 100       608 $hunk->{args} = [ $hunk->{extra} ? split /;/, $hunk->{extra} : () ];
91             }
92             }
93              
94             sub __format_errf {
95 72     72   612 my ($self, $hunk) = @_;
96              
97 72         193 my $conv = $self->codes->{ $hunk->{conversion} };
98              
99 72 50       339 Carp::croak("Unknown conversion in stringf: $hunk->{conversion}")
100             unless defined $conv;
101              
102 72         255 return $self->$conv($hunk->{replacement}, $hunk->{args}, $hunk);
103             }
104              
105             sub _proc_args {
106 105     105   139 my ($self, $input, $parse_compact) = @_;
107              
108 105 100       292 return $input if ref $input eq 'HASH';
109              
110             $parse_compact ||= sub {
111 0     0   0 Carp::croak("no compact format allowed, but compact format found");
112 69   50     121 };
113              
114 69         156 my @args = @$input;
115              
116 69 100 100     552 my $first = (defined $args[0] and length $args[0] and $args[0] !~ /=/)
117             ? shift @args
118             : undef;
119              
120 53         103 my %param = (
121             ($first ? %{ $parse_compact->($first) } : ()),
122 69 100       135 (map {; split /=/, $_, 2 } @args),
  25         131  
123             );
124              
125 69         265 return \%param;
126             }
127              
128             # Likely integer formatting options are:
129             # prefix (+ for positive numbers)
130             #
131             # Other options like (minwidth, precision, fillchar) are not out of the
132             # question, but if this system is to be used for formatting simple
133             # user-oriented error messages, they seem really unlikely to be used. Put off
134             # supplying them! -- rjbs, 2010-07-30
135             sub _format_int {
136 6     6   10 my ($self, $value, $rest) = @_;
137              
138             my $arg = $self->_proc_args($rest, sub {
139 3 50   3   21 return { prefix => $_[0] eq '+' ? '+' : '', }
140 6         29 });
141              
142 6         24 my $int_value = int $value;
143 6 50       13 $value = sprintf '%.0f', $value unless $int_value == $value;
144              
145 6 100       22 return $value if $value < 0;
146              
147 4 100       14 $arg->{prefix} = '' unless defined $arg->{prefix};
148              
149 4         28 return "$arg->{prefix}$value";
150             }
151              
152              
153             # Likely float formatting options are:
154             # prefix (+ for positive numbers)
155             # precision
156             #
157             # My remarks above for "int" go for floats, too. -- rjbs, 2010-07-30
158             sub _format_float {
159 54     54   86 my ($self, $value, $rest) = @_;
160              
161             my $arg = $self->_proc_args($rest, sub {
162 12     12   68 my ($prefix_str, $prec) = $_[0] =~ /\A(\+?)(?:\.(\d+))?\z/;
163 12         84 return { prefix => $prefix_str, precision => $prec };
164 54         225 });
165              
166 54 100 66     310 undef $arg->{precision}
167             unless defined $arg->{precision} and length $arg->{precision};
168              
169 54 100       141 $arg->{prefix} = '' unless defined $arg->{prefix};
170              
171 54 100       201 $value = defined $arg->{precision}
172             ? sprintf("%0.$arg->{precision}f", $value)
173             : $value;
174              
175 54 100       299 return $value < 0 ? $value : "$arg->{prefix}$value";
176             }
177              
178             sub _format_timestamp {
179 9     9   18 my ($self, $value, $rest) = @_;
180              
181             my $arg = $self->_proc_args($rest, sub {
182 6     6   33 return { type => $_[0] };
183 9         44 });
184              
185 9   100     52 my $type = $arg->{type} || 'datetime';
186 9   100     32 my $zone = $arg->{tz} || 'local';
187              
188 9 50       31 my $format = $type eq 'datetime' ? '%Y-%m-%d %H:%M:%S'
    100          
    100          
189             : $type eq 'date' ? '%Y-%m-%d'
190             : $type eq 'time' ? '%H:%M:%S'
191             : Carp::croak("unknown format type for %t: $type");
192              
193             # Supplying a time zone is *strictly informational*. -- rjbs, 2010-10-15
194 9 50 66     33 Carp::croak("illegal time zone for %t: $zone")
195             unless $zone eq 'local' or $zone eq 'UTC';
196              
197 9 100       21 my $method = $zone eq 'UTC' ? 'gmtime' : 'localtime';
198 9         53 my $piece = Time::Piece->$method($value);
199              
200 9         669 my $str = $piece->strftime($format);
201              
202 9 100       1305 return $zone eq 'UTC' ? "$str UTC" : $str;
203             }
204              
205             sub _format_string {
206 3     3   8 my ($self, $value, $rest) = @_;
207 3         14 return $value;
208             }
209              
210             sub _pluralize {
211 29     29   36 my ($singular) = @_;
212              
213 29 100       283 return $singular =~ /(?:[xzs]|sh|ch)\z/ ? "${singular}es"
    100          
214             : $singular =~ s/y\z/ies/ ? $singular
215             : "${singular}s";
216             }
217              
218             sub _format_numbered {
219 36     36   55 my ($self, $value, $rest, $hunk) = @_;
220              
221             my $arg = $self->_proc_args($rest, sub {
222 32     32   55 my ($word) = @_;
223              
224 32         197 my ($singular, $divider, $extra) = $word =~ m{\A(.+?)(?: ([/+]) (.+) )?\z}x;
225              
226 32 100       88 $divider = '' unless defined $divider; # just to avoid warnings
227              
228 32 100       114 my $plural = $divider eq '/' ? $extra
    100          
229             : $divider eq '+' ? "$singular$extra"
230             : _pluralize($singular);
231              
232 32         219 return { singular => $singular, plural => $plural };
233 36         190 });
234              
235 36         295 $value = $self->_format_float($value, {
236             prefix => $arg->{prefix},
237             precision => $arg->{precision},
238             });
239              
240 36 50       111 Carp::croak("no word given to number-based formatter")
241             unless defined $arg->{singular};
242              
243 36 100       73 $arg->{plural} = _pluralize($arg->{singular}) unless defined $arg->{plural};
244              
245 36 100       121 my $formed = abs($value) == 1 ? $arg->{singular} : $arg->{plural};
246              
247 36 100       172 return $formed if $hunk->{conversion} eq 'N';
248 18         118 return "$value $formed";
249             }
250              
251             1;
252              
253             __END__
254              
255             =pod
256              
257             =encoding UTF-8
258              
259             =head1 NAME
260              
261             String::Errf - a simple sprintf-like dialect
262              
263             =head1 VERSION
264              
265             version 0.007
266              
267             =head1 SYNOPSIS
268              
269             use String::Errf qw(errf);
270              
271             print errf "This process was started at %{start}t with %{args;argument}n.\n",
272             { start => $^T, args => 0 + @ARGV };
273              
274             ...might print something like:
275              
276             This process was started at 2010-10-17 14:05:29 with 0 arguments.
277              
278             =head1 DESCRIPTION
279              
280             String::Errf provides C<errf>, a simple string formatter that works something
281             like C<L<sprintf|perlfunc/sprintf>>. It is implemented using
282             L<String::Formatter> and L<Sub::Exporter>. Their documentation may be useful
283             in understanding or extending String::Errf. The C<errf> subroutine is only
284             available when imported. Calling L<String::Errf::errf> will not do what you
285             want.
286              
287             =head1 DIFFERENCES FROM SPRINTF
288              
289             The data passed to C<errf> should be organized in a single hashref, not a list.
290              
291             Formatting codes require named parameters, and the available codes are
292             different. See L</FORMATTING CODES> below.
293              
294             As with most String::Formatter formatters, C<%> is not a format code. If you
295             want a literal C<%>, do not put anything between the two percent signs, just
296             write C<%%>.
297              
298             =head2 FORMATTING CODES
299              
300             C<errf> formatting codes I<require> a set of arguments between the C<%> and the
301             formatting code letter. These arguments are placed in curly braces and
302             separated by semicolons. The first argument is the name of the data to look
303             for in the format data. For example, this is a valid use of C<errf>:
304              
305             errf "The current time in %{tz}s is %{now;local}t.", {
306             tz => $ENV{TZ},
307             now => time,
308             };
309              
310             The second argument, if present, may be a compact form for multiple named
311             arguments. The rest of the arguments will be named values in the form
312             C<name=value>. The examples below should help clarify how arguments are
313             passed. When an argument appears in both a compact and named form, the named
314             form trumps the compact form.
315              
316             The specific codes and their arguments are:
317              
318             =head3 s for string
319              
320             The C<s> format code is for any string, and takes no arguments. It just
321             includes the named item from the input data.
322              
323             errf "%{name}s", { name => 'John Smith' }; # returns "John Smith"
324              
325             Remember, C<errf> does I<not> have any of the left- or right-padding formatting
326             that C<sprintf> provides. It is not meant for building tables, only strings.
327              
328             =head3 i for integer
329              
330             The C<i> format code is used for integers. It takes one optional argument,
331             C<prefix>, which defaults to the empty string. C<prefix> may be given as the
332             compact argument, standing alone. C<prefix> is used to prefix non-negative
333             integers. It may only be a plus sign.
334              
335             errf "%{x}i", { x => 10 }; # returns "10"
336             errf "%{x;+}i", { x => 10 }; # returns "+10"
337              
338             errf "%{x;prefix=+}i", { x => 10 }; # returns "+10"
339              
340             The rounding behavior for non-integer values I<is not currently specified>.
341              
342             =head3 f for float (or fractional)
343              
344             The C<f> format code is for numbers with sub-integer precision. It works just
345             like C<i>, but adds a C<precision> argument which specifies how many decimal
346             places of precision to display. The compact argument may be just the prefix or
347             the prefix followed by a period followed by the precision.
348              
349             errf "%{x}f", { x => 10.1234 }; # returns "10";
350             errf "%{x;+}f", { x => 10.1234 }; # returns "+10";
351              
352             errf "%{x;.2}f", { x => 10.1234 }; # returns "10.12";
353             errf "%{x;+.2}f", { x => 10.1234 }; # returns "+10.12";
354              
355             errf "%{x;precision=.2}f", { x => 10.1234 }; # returns "10.12";
356             errf "%{x;prefix=+;precision=.2}f", { x => 10.1234 }; # returns "+10.12";
357              
358             =head3 t for time
359              
360             The C<t> format code is used to format timestamps provided in epoch seconds.
361             It can be given two arguments: C<type> and C<tz>.
362              
363             C<type> can be either date, time, or datetime, and indicates what part of the
364             timestamp should be displayed. The default is datetime. C<tz> requests that
365             the timestamp be displayed in either UTC or the local time zone. The default
366             is local.
367              
368             The compact form is just C<type> alone.
369              
370             # Assuming our local time zone is America/New_York...
371              
372             errf "%{x}t", { x => 1280530906 }; # "2010-07-30 19:01:46"
373             errf "%{x;type=date}t", { x => 1280530906 }; # "2010-07-30"
374             errf "%{x;type=time}t", { x => 1280530906 }; # "19:01:46"
375             errf "%{x;type=datetime}t", { x => 1280530906 }; # "2010-07-30 19:01:46"
376              
377             errf "%{x;tz=UTC}t", { x => 1280530906 }; # "2010-07-30 23:01:46 UTC"
378             errf "%{x;tz=UTC;type=date}t", { x => 1280530906 }; # "2010-07-30 UTC"
379             errf "%{x;tz=UTC;type=time}t", { x => 1280530906 }; # "23:01:46 UTC"
380             errf "%{x;tz=UTC;type=datetime}t", { x => 1280530906 }; # "2010-07-30 23:01:46 UTC"
381              
382             =head3 n and N for numbered
383              
384             The C<n> and C<N> format codes are for picking words based on number. It takes
385             two of its own arguments, C<singular> and C<plural>, as well as C<prefix> and
386             C<precision> which may be used for formatting the number itself.
387              
388             If the value being formatted is 1, the singular word is used. Otherwise, the
389             plural form is used.
390              
391             errf "%{x;singular=dog;plural=dogs}n", { x => 0 }; # 0 dogs
392             errf "%{x;singular=dog;plural=dogs}n", { x => 1 }; # 1 dog
393             errf "%{x;singular=dog;plural=dogs}n", { x => 2 }; # 2 dogs
394              
395             errf "%{x;singular=dog;plural=dogs}n", { x => 1.4 }; # 1.4 dogs
396             errf "%{x;singular=dog;plural=dogs;precision=1}n", { x => 1.4 }; # 1.4 dogs
397             errf "%{x;singular=dog;plural=dogs;precision=0}n", { x => 1.4 }; # 1 dog
398              
399             If C<N> is used instead of C<n>, the number will not be included, only the
400             chosen word.
401              
402             errf "%{x;singular=is;plural=are}N", { x => 0 }; # are
403             errf "%{x;singular=is;plural=are}N", { x => 1 }; # is
404             errf "%{x;singular=is;plural=are}N", { x => 2 }; # are
405              
406             errf "%{x;singular=is;plural=are}N", { x => 1.4 }; # 1.4 are
407             errf "%{x;singular=is;plural=are;precision=1}N", { x => 1.4 }; # 1.4 are
408             errf "%{x;singular=is;plural=are;precision=0}N", { x => 1.4 }; # 1 is
409              
410             The compact form may take any of the following forms:
411              
412             word - equivalent to singular=word
413              
414             word+suffix - equivalent to singular=word;plural=wordsuffix
415              
416             word1/word2 - equivalent to singular=word;plural=word2
417              
418             If no singular form is given, an exception is thrown. If no plural form is
419             given, one will be generated according to some basic rules of English
420             noun orthography.
421              
422             =head3
423              
424             =head1 AUTHOR
425              
426             Ricardo Signes <rjbs@cpan.org>
427              
428             =head1 COPYRIGHT AND LICENSE
429              
430             This software is copyright (c) 2013 by Ricardo Signes.
431              
432             This is free software; you can redistribute it and/or modify it under
433             the same terms as the Perl 5 programming language system itself.
434              
435             =cut