File Coverage

blib/lib/List/Maker.pm
Criterion Covered Total %
statement 88 89 98.8
branch 40 44 90.9
condition 39 48 81.2
subroutine 15 15 100.0
pod 1 1 100.0
total 183 197 92.8


line stmt bran cond sub pod time code
1             package List::Maker;
2              
3             our $VERSION = '0.005';
4              
5 20     20   762052 use warnings;
  20         62  
  20         993  
6 20     20   110 use strict;
  20         40  
  20         728  
7 20     20   118 use Carp;
  20         41  
  20         2438  
8 20     20   125 use List::Util qw;
  20         39  
  20         66306  
9              
10             # Handle contextual returns
11             sub _context {
12             # Return original list in list context...
13 35 100   35   515 return @_ if (caller 1)[5];
14              
15             # Otherwise, Anglicize list...
16 7 100       33 return "" if @_ == 0;
17 6 100       20 return "$_[0]" if @_ == 1;
18 5 100       33 return "$_[0] and $_[1]" if @_ == 2;
19              
20 1 50       578 my $sep = grep(/,/, @_) ? q{; } : q{, };
21 1         14 return join($sep, @_[0..@_-2]) . $sep . "and $_[-1]";
22             }
23              
24             # General filters that can be applied...
25             my %selector_sub = (
26             pick => sub {
27             return (shuffle @_)[0..shift()-1]
28             },
29              
30             roll => sub {
31             return map { $_[rand @_] } 1..shift;
32             },
33              
34             all => sub {
35             shift; return @_;
36             },
37             );
38             # Regexes to parse the acceptable list syntaxes...
39             my $NUM = qr{\s* [+-]? \d+ (?:\.\d*)? \s* }xms;
40             my $TO = qr{\s* \.\. \s*}xms;
41             my $FILTER = qr{ (?: : (?! \s* (?:pick|roll)) (.*?) )? }xms;
42             my $SELECTOR = qr{ (?: : \s* (pick|roll) \s* (\d*) )? \s* }xms;
43              
44             # Mappings from specifications to handlers...
45             my @handlers = (
46             # <1, 2 .. 10>
47             { pat => qr{\A ($NUM) , ($NUM) ,? $TO (\^?) ($NUM) $FILTER $SELECTOR \Z}xms,
48             gen => sub{ _gen_range({
49             from=>$1, to=>$4, by=>$2-$1, exto=>$3,
50             filter=>$5, selector => $6, count => $7,
51             })
52             },
53             },
54              
55             # <1 .. 10 by 2>
56             { pat => qr{\A ($NUM) (\^?) $TO (\^?) ($NUM) (?:(?:x|by) ($NUM))? $FILTER $SELECTOR \Z}xms,
57             gen => sub{ _gen_range({
58             from=>$1, to=>$4, by=>$5, exfrom=>$2, exto=>$3,
59             filter=>$6, selector => $7, count => $8,
60             });
61             },
62             },
63              
64             # <^7 by 2>
65             { pat => qr{\A \s* \^ ($NUM) \s* (?:(?:x|by) \s* ($NUM))? $FILTER $SELECTOR \Z}xms,
66             gen => sub{ _gen_range({
67             from=>0, to=>$1, by=>$2, exto=>1,
68             filter=>$3, selector => $4, count => $5,
69             });
70             },
71             },
72              
73             # <^@foo>
74             { pat => qr{\A \s* \^ \s* ( (?:\S+\s+)* \S+) \s* \Z}xms,
75             gen => sub{
76             my $specification = $1;
77             $specification =~ s{$SELECTOR \Z}{}x;
78             my ($selector, $count) = ($selector_sub{$1||'all'}, $2||1);
79              
80             my @array = split /\s+/, $specification;
81             $selector->($count, _gen_range( {from=>0, to=>@array-1}));
82             },
83             },
84              
85             # MINrMAX random range notation
86             { pat => qr/^\s* ([+-]?\d+(?:[.]\d*)?|[.]\d+) \s* r \s* ([+-]?\d+(?:[.]\d*)?|[.]\d+) \s* $/xms,
87             gen => sub {
88             my ($min, $max) = ($1 < $2) ? ($1,$2) : ($2,$1);
89             return $min + rand($max - $min);
90             }
91             },
92              
93             # NdS dice notation
94             { pat => qr/^\s* (\d+(?:[.]\d*)?|[.]\d+) \s* d \s* (\d+(?:[.]\d*)?|[.]\d+) \s* $/xms,
95             gen => sub {
96             my ($count, $sides) = ($1, $2);
97              
98             # Non-integer counts require an extra random (partial) value...
99             if ($count =~ /[.]/) {
100             $count++;
101             }
102              
103             # Generate random values...
104             my @rolls = $sides =~ /[.]/ ? map { rand $sides} 1..$count
105             : map {1 + int rand $sides} 1..$count
106             ;
107              
108             # Handle a non-integer count by scaling final random (partial) value...
109             if ($count =~ /([.].*)/) {
110             my $fraction = $1;
111             $rolls[-1] *= $fraction;
112             }
113              
114             return @rolls if wantarray;
115             return sum @rolls;
116             }
117             },
118              
119             # Perl 6 xx operator on 'strings'...
120             { pat => qr/^ \s* ' ( [^']* ) ' \s* xx \s* (\d+) \s* $/xms,
121             gen => sub {
122             my ($string, $repetitions) = ($1, $2);
123             return ($string) x $repetitions;
124             }
125             },
126              
127             # Perl 6 xx operator on "strings"...
128             { pat => qr/^ \s* " ( [^"]* ) " \s* xx \s* (\d+) \s* $/xms,
129             gen => sub {
130             my ($string, $repetitions) = ($1, $2);
131             return ($string) x $repetitions;
132             }
133             },
134              
135             # Perl 6 xx operator on numbers...
136             { pat => qr/^ \s* ( [+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)? ) \s* xx \s* (\d+) \s* $/xms,
137             gen => sub {
138             my ($number, $repetitions) = ($1, $2);
139             return (0+$number) x $repetitions;
140             }
141             },
142             );
143              
144             my %caller_expecting_special_behaviour;
145             my @user_handlers;
146              
147             # This does the magic...
148             my $list_maker_sub = sub {
149 20     20   1361 my ($listspec) = @_;
150              
151             # If it doesn't match a special form, it's a < word list >...
152 20         45 for my $handler (@user_handlers, @handlers) {
153 40 100       3835 next if $listspec !~ m{$handler->{pat} }xms;
154 20         97 return $handler->{gen}();
155             }
156              
157 0           return _context _qww($listspec);
158             };
159              
160             sub import {
161 20     20   397 shift; # Don't need package name
162 20         58 my $caller = caller;
163              
164             # Note lexical scope of import...
165 20         233 $^H{'List::Maker::is_active'} = 1;
166              
167             # Explicit export(s) requested...
168 20 100       112 if (@_) {
169 1         4 for my $name (@_) {
170 20     20   249 no strict 'refs';
  20         44  
  20         2371  
171 1         2 *{$caller.'::'.$name} = $list_maker_sub;
  1         2452  
172             }
173             }
174              
175             # Otherwise use 'glob' (to provide magic behaviour as well)...
176             else {
177 19         65 my ($package, $file) = caller;
178              
179             # Get as close to lexical behavior as per-5.10 will allow...
180 19         108 $caller_expecting_special_behaviour{ $package, $file } = 1;
181              
182 20     20   301 no strict 'refs';
  20         72  
  20         4966  
183 19         47 *{$caller.'::glob'} = \&_glob_replacement;
  19         51662  
184             }
185             }
186              
187             # Users can add their own handlers...
188             sub add_handler {
189 2     2 1 29 while (my ($regex, $sub) = splice @_, 0, 2) {
190 2 50 33     14 croak "Usage: List::Make::add_handler(qr/.../, sub{...})\nError"
191             if ref($regex) ne 'Regexp' || ref($sub) ne 'CODE';
192 2         11 push @user_handlers, { pat=>$regex, gen=>$sub };
193             }
194 2         5 return;
195             }
196              
197              
198              
199             # This sub is used instead of globs the special behaviours...
200 20     20   125 no warnings 'redefine';
  20         39  
  20         2999  
201             sub _glob_replacement {
202             # Don't be magical in those files that haven't loaded the module...
203 947     947   1111781 my ($package, $file, $scope_ref) = (caller 0)[0,1,10];
204              
205             # Check for lexical scoping (only works in 5.10 and later)...
206 947   33     15504 my $in_scope = $] < 5.010 || $scope_ref && $scope_ref->{'List::Maker::is_active'};
207              
208             # If not being magical...
209 947 100 66     9187 if (!$caller_expecting_special_behaviour{$package, $file} || !$in_scope ) {
210             # Use any overloaded version of glob...
211 3 100       15 goto &CORE::GLOBAL::glob if exists &CORE::GLOBAL::glob;
212              
213             # Otherwise, use the core glob behaviour...
214 20     20   122 use File::Glob 'csh_glob';
  20         66  
  20         36885  
215 2         796 return &csh_glob;
216             }
217              
218             # Otherwise, be magical...
219             else {
220 944         1277 goto &{$list_maker_sub};
  944         5091  
221             }
222             };
223              
224             # Generate a range of values, selected or filtered as appropriate...
225             sub _gen_range {
226 214         1052 my ($from, $to, $incr, $filter, $exfrom, $exto, $selector, $count)
227 214     214   402 = @{shift()}{ qw };
228              
229             # Trim leading and trailing whitespace from endpoints...
230 214         2131 s/^ \s+ | \s+ $//gxms for $from, $to;
231              
232             # Default increment is +/- 1, depending on end-points...
233 214 100       612 if (!defined $incr) {
234 86         268 $incr = -($from <=> $to);
235             }
236              
237             # Default count is 1...
238 214   100     824 $count ||= 1;
239              
240             # Check for nonsensical increments (zero or the wrong sign)...
241 214         564 my $delta = $to - $from;
242 214 100 100     2650 croak sprintf "Sequence <%s, %s, %s...> will never reach %s",
      100        
243             $from, $from+$incr, $from+2*$incr, $to
244             if $incr == 0 && $from != $to || $delta * $incr < 0;
245              
246             # Generate unfiltered list of values...
247 206 100       473 $from += $incr if $exfrom;
248 206         256 my @vals;
249             #
250 206 100       701 if ($incr==0) {
    100          
    50          
251 11 100 100     59 @vals = $exto || $exfrom ? () : $from;
252             }
253              
254             #
255             elsif ($incr>0) {
256 163         219 while (1) {
257 3223 100 100     29989 last if $exto && ($from >= $to || $from eq $to)
      66        
      100        
      66        
258             || !$exto && $from > $to;
259 3060         4520 push @vals, $from;
260 3060         3466 $from += $incr;
261             }
262             }
263              
264             #
265             elsif ($incr<0) {
266 32         38 while (1) {
267 248 100 100     1791 last if $exto && ($from <= $to || $from eq $to)
      66        
      100        
      66        
268             || !$exto && $from < $to;
269 216         323 push @vals, $from;
270 216         267 $from += $incr;
271             }
272             }
273              
274             # Apply any filter before returning the values...
275 206 100       561 if (defined $filter) {
276 40         204 (my $trans_filter = $filter) =~ s/\b[A-Z]\b/\$_/g;
277 40         4465 @vals = eval "grep {package ".caller(2)."; $trans_filter } \@vals";
278 40 50       429 croak "Bad filter ($filter): $@" if $@;
279             }
280              
281             # Apply any selector before returning values...
282 206 100       641 if (defined $selector) {
283 50         479 @vals = $selector_sub{$selector}->($count, @vals);
284             }
285              
286 206         3715 return @vals;
287             };
288              
289             # Simulate a Perl 6 <<...>> construct...
290             sub _qww {
291 35     35   72 my ($content) = @_;
292              
293             # Strip any filter...
294 35         6956 $content =~ s{$SELECTOR \Z}{}x;
295 35   100     359 my ($selector, $count) = ($selector_sub{$1||'all'}, $2||1);
      100        
296              
297             # Break into words (or "w o r d s" or 'w o r d s') and strip quoters...
298 321         573 return $selector->( $count,
299 35         519 grep { defined($_) }
300             $content =~ m{ " ( [^\\"]* (?:\\. [^\\"]*)* ) "
301             | ' ( [^\\']* (?:\\. [^\\']*)* ) '
302             | ( \S+ )
303             }gxms
304             );
305             }
306              
307              
308             1; # Magic true value required at end of module
309             __END__