File Coverage

lib/Badger/Utils.pm
Criterion Covered Total %
statement 174 203 85.7
branch 49 72 68.0
condition 29 54 53.7
subroutine 39 42 92.8
pod 23 26 88.4
total 314 397 79.0


line stmt bran cond sub pod time code
1             #========================================================================
2             #
3             # Badger::Utils
4             #
5             # DESCRIPTION
6             # Module implementing various useful utility functions.
7             #
8             # AUTHOR
9             # Andy Wardley
10             #
11             #========================================================================
12              
13             package Badger::Utils;
14              
15 70     70   991 use strict;
  70         119  
  70         2394  
16 70     70   374 use warnings;
  70         133  
  70         2062  
17 70     70   459 use base 'Badger::Exporter';
  70         128  
  70         8422  
18 70     70   468 use File::Path;
  70         144  
  70         5586  
19 70     70   474 use Scalar::Util qw( blessed );
  70         129  
  70         3496  
20 70     70   452 use Badger::Constants 'HASH ARRAY PKG DELIMITER BLANK TRUE FALSE';
  70         120  
  70         730  
21             use Badger::Debug
22 70         410 import => ':dump',
23 70     70   464 default => 0;
  70         126  
24 70     70   429 use overload;
  70         121  
  70         414  
25             use constant {
26 70         90727 UTILS => 'Badger::Utils',
27             CLASS => 0,
28             FILE => 1,
29             LOADED => 2,
30 70     70   4723 };
  70         122  
31              
32             our $VERSION = 0.02;
33             our $ERROR = '';
34             our $WARN = sub { warn @_ }; # for testing - see t/core/utils.t
35             our $MESSAGES = { };
36             our $HELPERS = { # keep this compact in case we don't need to use it
37             'Digest::MD5' => 'md5 md5_hex md5_base64',
38             'Scalar::Util' => 'blessed dualvar isweak readonly refaddr reftype
39             tainted weaken isvstring looks_like_number
40             set_prototype',
41             'List::Util' => 'first max maxstr min minstr reduce shuffle sum',
42             'List::MoreUtils' => 'any all none notall true false firstidx
43             first_index lastidx last_index insert_after
44             insert_after_string apply after after_incl before
45             before_incl indexes firstval first_value lastval
46             last_value each_array each_arrayref pairwise
47             natatime mesh zip uniq minmax',
48             'Hash::Util' => 'lock_keys unlock_keys lock_value unlock_value
49             lock_hash unlock_hash hash_seed',
50             'Badger::Date' => 'DATE Date Today',
51             'Badger::Timestamp' => 'TIMESTAMP TS Timestamp Now',
52             'Badger::Logic' => 'LOGIC Logic',
53             'Badger::Duration' => 'DURATION Duration',
54             'Badger::URL' => 'URL',
55             'Badger::Filter' => 'FILTER Filter',
56             'Badger::Filesystem' => 'FS File Dir Bin Cwd',
57             'Badger::Filesystem::Virtual'
58             => 'VFS',
59             };
60             our $DELEGATES; # fill this from $HELPERS on demand
61             our $RANDOM_NAME_LENGTH = 32;
62             our $TEXT_WRAP_WIDTH = 78;
63              
64              
65             __PACKAGE__->export_any(qw(
66             UTILS blessed is_object numlike textlike truelike falselike
67             params self_params plural
68             odd_params xprintf dotid random_name camel_case CamelCase wrap
69             permute_fragments plurality inflect split_to_list extend merge merge_hash
70             list_each hash_each join_uri resolve_uri
71             ));
72              
73             __PACKAGE__->export_fail(\&_export_fail);
74              
75             # looks_like_number() is such a mouthful. I prefer numlike() to go with textlike()
76             *numlike = \&Scalar::Util::looks_like_number;
77              
78             # it would be too confusing not to have this alias
79             *CamelCase = \&camel_case;
80              
81              
82             sub _export_fail {
83 102     102   272 my ($class, $target, $symbol, $more_symbols) = @_;
84 102   66     426 $DELEGATES ||= _expand_helpers($HELPERS);
85 102   50     460 my $helper = $DELEGATES->{ $symbol } || return 0;
86 102 50       4408 require $helper->[FILE] unless $helper->[LOADED];
87 102         3270 $class->export_symbol($target, $symbol, \&{ $helper->[CLASS].PKG.$symbol });
  102         947  
88 102         387 return 1;
89             }
90              
91             sub _expand_helpers {
92             # invert { x => 'a b c' } into { a => 'x', b => 'x', c => 'x' }
93 70     70   121 my $helpers = shift;
94             return {
95             map {
96 70         295 my $name = $_; # e.g. Scalar::Util
  910         1087  
97 910         1249 my $file = module_file($name); # e.g. Scalar/Util.pm
98 5530         16786 map { $_ => [$name, $file, 0] } # third item is loaded flag
99 910         8670 split(DELIMITER, $helpers->{ $name })
100             }
101             keys %$helpers
102             }
103             }
104              
105             sub is_object($$) {
106 738 100   738 1 3982 blessed $_[1] && $_[1]->isa($_[0]);
107             }
108              
109             sub textlike($) {
110 95 100 100 95 1 441 ! ref $_[0] # check if $[0] is a non-reference
111             || blessed $_[0] # or an object with an overloaded
112             && overload::Method($_[0], '""'); # '""' stringification operator
113             }
114              
115             sub truelike($) {
116 11 100   11 0 21 falselike($_[0]) ? FALSE : TRUE;
117             }
118              
119             sub falselike($) {
120 22 100 100 22 0 217 (! $_[0] || $_[0] =~ /^(0|off|no|none|false)$/i) ? TRUE : FALSE;
121             }
122              
123             sub params {
124             # enable $DEBUG to track down calls to params() that pass an odd number
125             # of arguments, typically when the rhs argument returns an empty list,
126             # e.g. $obj->foo( x => this_returns_empty_list() )
127 17     17 1 61 my @args = @_;
128             local $SIG{__WARN__} = sub {
129 3     1   48 odd_params(@args);
130 17         40 } if DEBUG;
131              
132 15 100 100     80 @_ && ref $_[0] eq HASH ? shift : { @_ };
133             }
134              
135             sub self_params {
136 3     3 1 23 my @args = @_;
137             local $SIG{__WARN__} = sub {
138 2     0   62 odd_params(@args);
139 3         16 } if DEBUG;
140              
141 1 100 66     10 (shift, @_ && ref $_[0] eq HASH ? shift : { @_ });
142             }
143              
144             sub odd_params {
145 1     1 1 8 my $method = (caller(2))[3];
146             $WARN->(
147             "$method() called with an odd number of arguments: ",
148 1 50       5 join(', ', map { defined $_ ? $_ : '' } @_),
  3         40  
149             "\n"
150             );
151 1         7 my $i = 3;
152 1         2 while (1) {
153 4         27 my @info = caller($i);
154 4 100       18 last unless @info;
155 3         8 my ($pkg, $file, $line, $sub) = @info;
156 3         15 $WARN->(
157             sprintf(
158             "%4s: Called from %s in %s at line %s\n",
159             '#' . ($i++ - 2), $sub, $file, $line
160             )
161             );
162             }
163             }
164              
165              
166             sub module_file {
167 910     910 1 999 my $file = shift;
168 910         2170 $file =~ s[::][/]g;
169 910         1584 $file .= '.pm';
170             }
171              
172             sub xprintf {
173 1907     1907 1 2475 my $format = shift;
174 1907         3603 my @args = @_;
175 1907         3282 $format =~
176             s{ < (\d+)
177             (?: :( [#\-\+ ]? [\w\.]+ ) )?
178             (?: \| (.*?) )?
179             >
180 48 100 100     319 }
181             { defined $3
182             ? _xprintf_ifdef(\@args, $1, $2, $3)
183             : '%' . $1 . '$' . ($2 || 's')
184 70     70   46304 }egx;
  70         932  
  70         486  
185 1907         11598 no if $] > 5.021, warnings => "redundant";
186             sprintf($format, @_);
187             }
188              
189 3     3   13 sub _xprintf_ifdef {
190 3 100       13 my ($args, $n, $format, $text) = @_;
191 2 50       7 if (defined $args->[$n-1]) {
192 2         6 $format = 's' unless defined $format;
193 2         8 $format = '%' . $n . '$' . $format;
194 2         6 $text =~ s/\?/$format/g;
195             return $text;
196             }
197 1         4 else {
198             return '';
199             }
200             }
201              
202 83     83 1 165 sub dotid {
203 83         241 my $text = shift; # munge $text to canonical lower case and dotted form
204 83         216 $text =~ s/\W+/./g; # e.g. Foo::Bar ==> Foo.Bar
205             return lc $text; # e.g. Foo.Bar ==> foo.bar
206             }
207              
208             sub camel_case {
209             join(
210             BLANK,
211 132     132 1 193 map {
  132         257  
  141         588  
212             map { ucfirst $_ }
213             split '_'
214             }
215             @_
216             );
217             }
218              
219 5   66 5 1 13 sub random_name {
220 5         6 my $length = shift || $RANDOM_NAME_LENGTH;
221 5         20 my $name = '';
222             require Digest::MD5;
223 5         15  
224 7         123 while (length $name < $length) {
225             $name .= Digest::MD5::md5_hex(
226             time(), rand(), $$, { }, @_
227             );
228 5         32 }
229             return substr($name, 0, $length);
230             }
231              
232 19     19 1 55 sub alternates {
233             my $text = shift;
234 19 100       206 return [
235             $text =~ /\|/
236             ? split(qr<\|>, $text, -1) # alternates: (foo|bar) as ['foo', 'bar']
237             : ('', $text) # optional (foo) as (|foo) as ['', 'foo']
238             ];
239             }
240              
241 0     0 1 0 sub wrap {
242 0   0     0 my $text = shift;
243 0   0     0 my $width = shift || $TEXT_WRAP_WIDTH;
244 0         0 my $indent = shift || 0;
245 0         0 my @words = split(/\s+/, $text);
246 0         0 my (@lines, @line, $length);
247             my $total = 0;
248 0         0  
249 0   0     0 while (@words) {
250 0 0 0     0 $length = length $words[0] || (shift(@words), next);
251 0 0       0 if ($total + $length > 74 || $words[0] eq '\n') {
252 0         0 shift @words if $words[0] eq '\n';
253 0         0 push(@lines, join(" ", @line));
254 0         0 @line = ();
255             $total = 0;
256             }
257 0         0 else {
258 0         0 $total += $length + 1; # account for spaces joining words
259             push(@line, shift @words);
260             }
261 0 0       0 }
262 0         0 push(@lines, join(" ", @line)) if @line;
263             return join(
264             "\n" . (' ' x $indent),
265             @lines
266             );
267             }
268              
269              
270 54     54 1 121 sub permute_fragments {
271 54         119 my $input = shift;
272             my (@frags, @outputs);
273              
274             # Lookup all the (a) optional fragments and (a|b|c) alternate fragments
275             # replace them with %s. This gives us an sprintf format that we can later
276             # user to re-fill the fragment slots. Meanwhile create a list of @frags
277             # with each item corresponding to a (...) fragment which is represented
278             # by a list reference containing the alternates. e.g. the input
279             # string 'Fo(o|p) Ba(r|z)' generates @frags as ( ['o','p'], ['r','z'] ),
280             # leaving $input set to 'Fo%s Ba%s'. We treat (foo) as sugar for (|foo),
281             # so that 'Template(X)' is permuted as ('Template', 'TemplateX'), for
282             # example.
283 54         219  
284             $input =~
285             s/
286             \( ( .*? ) \)
287 19         94 /
288 19         81 push(@frags, alternates($1));
289             '%s';
290             /gex;
291              
292             # If any of the fragments have multiple values then $format will still contain
293             # one or more '%s' tokens and @frags will have the same number of list refs
294             # in it, one for each fragment. To iterate across all permutations of the
295             # fragment values, we calculate the product P of the sizes of all the lists in
296             # @frags and loop from 0 to P-1. Then we use a div and a mod to get the right
297             # value for each fragment, for each iteration. We divide $n by the product of
298             # all fragment lists to the right of the current fragment and mod it by the size
299             # of the current fragment list. It's effectively counting with a different base
300             # for each column. e.g. consider 3 fragments with 7, 3, and 5 values respectively
301             # [7] [3] [5] P = 7 * 3 * 5 = 105
302             # [n / 15 % 7] [n / 5 % 3] [n % 5] for 0 < n < P
303 54 100       164  
304 18         40 if (@frags) {
  18         78  
305 18         98 my $product = 1; $product *= @$_ for @frags;
306 38         71 for (my $n = 0; $n < $product; $n++) {
307             my $divisor = 1;
308 38         68 my @args = reverse map {
  42         158  
309 42         84 my $item = $_->[ $n / $divisor % @$_ ];
310 42         103 $divisor *= @$_;
311             $item;
312 38         217 } reverse @frags; # working backwards from right to left
313             push(@outputs, sprintf($input, @args));
314             }
315             }
316 36         67 else {
317             push(@outputs, $input);
318             }
319             return wantarray
320 54 50       242 ? @outputs
321             : \@outputs;
322             }
323              
324             #-----------------------------------------------------------------------------
325             # pluralisation and inflection
326             #-----------------------------------------------------------------------------
327              
328 73     73 1 167 sub plural {
329             my $name = shift;
330 73 50       769  
    100          
    50          
331 0         0 if ($name =~ /(ss|sh|ch|x)$/) {
332             $name .= 'es';
333             }
334 3         13 elsif ($name =~ s/([^aeiou])y$//) {
335             $name .= $1.'ies';
336             }
337 70         169 elsif ($name =~ /([^s\d\W])$/) {
338             $name .= 's';
339 73         201 }
340             return $name;
341             }
342              
343 19   50 19 1 35 sub plurality {
344 19         30 my $n = shift || 0;
345             my @items = map { permute_fragments($_) }
346 19 50 33     79 (@_ == 1 && ref $_[0] eq ARRAY)
  0         0  
347             ? @{ $_[0] }
348             : @_;
349              
350             # if the user specifies a single word then we pluralise it for them,
351 19 50       36 # assuming that 0 items are plural, 1 is singular, and > 1 is plural
352 19         35 if (@items == 1) {
353 19         35 my $plural = plural($items[0]);
354 19         34 unshift(@items, $plural); # 0 whatevers
355             push(@items, $plural); # n whatevers (where n > 1)
356             }
357 19 50       44  
358 19 100       72 die "$n is not a number\n" unless numlike($n);
359 19 50       38 my $i = $n > $#items ? $#items : $n;
360             $i = 0 if $i < 0;
361 19         48  
362             return $items[$i];
363             }
364              
365 19   50 19 1 34 sub inflect {
366 19         25 my $n = shift || 0;
367 19   50     47 my $i = shift;
368 19 50       32 my $f = shift || '%s %s';
369 19   33     40 my $z = @_ ? shift : 'no';
370             return xprintf(
371             $f, ($n or $z), plurality($n, $i)
372             );
373             }
374              
375              
376 0     0   0 sub _debug {
377             print STDERR @_;
378             }
379              
380             #-----------------------------------------------------------------------------
381             # List utilities
382             #-----------------------------------------------------------------------------
383              
384 1     1 1 3 sub list_each {
385 1         2 my ($list, $fn) = @_;
386             my $n = 0;
387 1         2  
388 3         16 for (@$list) {
389             $fn->($list, $n++, $_);
390             }
391 1         5  
392             return $list;
393             }
394              
395 3     3 1 5 sub split_to_list {
396 3 50 33     14 my $list = shift;
397             return [ ] unless defined $list and length $list;
398 3 100       9  
    50          
399 2         23 if (! ref $list) {
400             return [ split(DELIMITER, $list) ];
401             }
402 1         7 elsif (ref $list eq ARRAY) {
403             return $list;
404             }
405 0         0 else {
406             return [$list];
407             }
408             }
409              
410             #-----------------------------------------------------------------------------
411             # Hash utilities
412             #-----------------------------------------------------------------------------
413              
414 1     1 1 2 sub hash_each {
415             my ($hash, $fn) = @_;
416 1         21  
417 2         19 while (my ($key, $value) = each %$hash) {
418             $fn->($hash, $key, $value);
419             }
420 1         7  
421             return $hash;
422             }
423              
424              
425 2     2 1 15 sub extend {
426 2         3 my $hash = shift;
427             my $more;
428 2         7  
429 4 50       12 while (@_) {
    50          
430             if (! $_[0]) {
431 0         0 # ignore undefined/false values
432 0         0 shift;
433             next;
434             }
435 4         6 elsif (ref $_[0] eq HASH) {
436             $more = shift;
437             }
438 0         0 else {
439 0         0 $more = params(@_);
440             @_ = ();
441 4         14 }
442             @$hash{ keys %$more } = values %$more;
443             }
444 2         5  
445             return $hash;
446             }
447              
448 2     2 1 50 sub merge {
449 2         2 my $hash = shift;
450             my $more;
451 2         7  
452 3 50       10 while (@_) {
    50          
453             if (! $_[0]) {
454 0         0 # ignore undefined/false values
455 0         0 shift;
456             next;
457             }
458 3         5 elsif (ref $_[0] eq HASH) {
459 3         10 $more = shift;
460             $more = { %$more };
461             }
462 0         0 else {
463 0         0 $more = params(@_);
464             @_ = ();
465 3         7 }
466             merge_hash($hash, $more);
467             }
468 2         9  
469             return $hash;
470             }
471              
472 6     6 0 11 sub merge_hash {
473 6         6 my ($hash, $more) = @_;
474             my $into;
475 6         17  
476 10         11 while (my ($key, $value) = each %$more) {
477             $into = $hash->{ $key };
478 10 100 100     48  
      66        
479 3         16 if ($into && ref $into eq HASH && ref $value eq HASH) {
480             merge_hash($into, $value);
481             }
482 7         35 else {
483             $hash->{ $key } = $value;
484             };
485 6         13 }
486             return $hash;
487             }
488              
489              
490             #-----------------------------------------------------------------------------
491             # Simple URI manipulation
492             #-----------------------------------------------------------------------------
493              
494 10     10 1 34 sub join_uri {
495 10         46 my $uri = join('/', @_);
496 10         42 $uri =~ s{(?
497             return $uri;
498             }
499              
500 3     3 1 10 sub resolve_uri {
501 3         10 my $base = shift;
502 3 100       28 my $rel = join_uri(@_);
503             return ($rel =~ m{^/})
504             ? $rel
505             : join_uri($base, $rel);
506             }
507              
508             1;
509              
510             __END__