File Coverage

blib/lib/Array/Find.pm
Criterion Covered Total %
statement 95 98 96.9
branch 78 80 97.5
condition 60 77 77.9
subroutine 5 5 100.0
pod 1 1 100.0
total 239 261 91.5


line stmt bran cond sub pod time code
1             package Array::Find;
2              
3 1     1   43983 use 5.010;
  1         5  
  1         49  
4 1     1   6 use strict;
  1         3  
  1         45  
5 1     1   6 use warnings;
  1         2  
  1         52  
6              
7 1     1   7 use List::Util qw(shuffle);
  1         2  
  1         2668  
8              
9             require Exporter;
10             our @ISA = qw(Exporter);
11             our @EXPORT_OK = qw(find_in_array);
12              
13             our $VERSION = '0.07'; # VERSION
14              
15             our %SPEC;
16              
17             $SPEC{find_in_array} = {
18             summary => 'Find items in array, with several options',
19             description => <<'_',
20              
21             find_in_array looks for one or more items in one or more arrays and return an
22             array containing all or some results (empty arrayref if no results found). You
23             can specify several options, like maximum number of results, maximum number of
24             comparisons, searching by suffix/prefix, case sensitivity, etc. Consult the list
25             of arguments for more details.
26              
27             Currently, items are compared using the Perl's eq operator, meaning they only
28             work with scalars and compare things asciibetically.
29              
30             _
31             args => {
32             item => ['str' => {
33             summary => 'Item to find',
34             description => <<'_',
35              
36             Currently can only be scalar. See also 'items' if you want to find several items
37             at once.
38              
39             _
40             arg_pos => 0,
41             }],
42             array => ['array' => {
43             summary => 'Array to find items in',
44             description => <<'_',
45              
46             See also 'arrays' if you want to find in several arrays. Array elements can be
47             undef and will only match undef.
48              
49             _
50             arg_pos => 1,
51             }],
52             items => ['array' => {
53             of => 'str',
54             summary => "Just like 'item', except several",
55             description => <<'_',
56              
57             Use this to find several items at once. Elements can be undef if you want to
58             search for undef.
59              
60             Example: find_in_array(items => ["a", "b"], array => ["b", "a", "c", "a"]) will
61             return result ["b", "a", "a"].
62              
63             _
64             }],
65             arrays => ['array' => {
66             of => 'array', # XXX ['array*'=>{of=>'str'}]
67             summary => "Just like 'array', except several",
68             description => <<'_',
69              
70             Use this to find several items at once.
71              
72             Example: find_in_array(item => "a", arrays => [["b", "a"], ["c", "a"]]) will
73             return result ["a", "a"].
74              
75             _
76             }],
77             max_result => ['int' => {
78             summary => "Set maximum number of results",
79             description => <<'_',
80              
81             0 means unlimited (find in all elements of all arrays).
82              
83             +N means find until results have N items. Example: find_in_array(item=>'a',
84             array=>['a', 'b', 'a', 'a'], max_result=>2) will return result ['a', 'a'].
85              
86             -N is useful when looking for multiple items (see 'items' argument). It means
87             find until N items to look for have been found. Example:
88             find_in_array(items=>['a','b'], array=>['a', 'a', 'b', 'b'], max_results=>-2)
89             will return result ['a', 'a', 'b']. As soon as 2 items to look for have been
90             found it will stop.
91              
92             _
93             }],
94             max_compare => ['int' => {
95             summary => "Set maximum number of comparison",
96             description => <<'_',
97              
98             Maximum number of elements in array(s) to look for, 0 means unlimited. Finding
99             will stop as soon as this limit is reached, regardless of max_result. Example:
100             find(item=>'a', array=>['q', 'w', 'e', 'a'], max_compare=>3) will not return
101             result.
102              
103             _
104             }],
105             ci => ['bool' => {
106             default => 0,
107             summary => "Set case insensitive",
108             }],
109             mode => ['str' => {
110             in => ['exact', 'prefix', 'suffix', 'infix',
111             'prefix|infix', 'prefix|suffix',
112             'prefix|infix|suffix', 'infix|suffix',
113             'regex'],
114             default => 'exact',
115             summary => "Comparison mode",
116             description => <<'_',
117              
118             Exact match is the default, will only match 'ap' with 'ap'. Prefix matching will
119             also match 'ap' with 'ap', 'apple', and 'apricot'. Suffix matching will match
120             'le' with 'le' and 'apple'. Infix will only match 'ap' with 'claps' and not with
121             'ap', 'clap', or 'apple'. Regex will regard item as a regex and perform a regex
122             match on each element of array.
123              
124             See also 'word_sep' which affects prefix/suffix/infix matching.
125             _
126             }],
127             word_sep => ['str' => {
128             summary => "Define word separator",
129             arg_aliases => {
130             ws => {},
131             },
132             description => <<'_',
133              
134             If set, item and array element will be regarded as a separated words. This will
135             affect prefix/suffix/infix matching. Example, with '.' as the word separator
136             and 'a.b' as the item, prefix matching will 'a.b', 'a.b.', and 'a.b.c'
137             (but not 'a.bc'). Suffix matching will match 'a.b', '.a.b', 'c.a.b' (but
138             not 'ca.b'). Infix matching will match 'c.a.b.c' and won't match 'a.b',
139             'a.b.c', or 'c.a.b'.
140              
141             _
142             }],
143             unique => ['bool' => {
144             summary => "Whether to return only unique results",
145             arg_aliases => {
146             u => {},
147             },
148             description => <<'_',
149             If set to true, results will not contain duplicate items.
150             _
151             }],
152             shuffle => ['bool' => {
153             summary => "Shuffle result",
154             }],
155             },
156             result_naked => 1,
157             };
158             sub find_in_array {
159 40     40 1 164776 my %args = @_;
160              
161             # XXX schema
162 40         84 my @items;
163 40 100       232 push @items , $args{item} if exists $args{item};
164 40 100       981 push @items , @{$args{items}} if exists $args{items};
  6         66  
165              
166 40         73 my @arrays;
167 40 100       223 push @arrays, $args{array} if exists $args{array};
168 40 100       296 push @arrays, @{$args{arrays}} if exists $args{arrays};
  2         9  
169              
170 40         95 my $ci = $args{ci};
171 40   100     178 my $mode = $args{mode} // 'exact';
172 40         763 my $mode_prefix = $mode =~ /prefix/;
173 40         113 my $mode_infix = $mode =~ /infix/;
174 40         1867 my $mode_suffix = $mode =~ /suffix/;
175 40   66     198 my $ws = $args{word_sep} // $args{ws};
176 40 50 66     166 $ws = undef if defined($ws) && $ws eq '';
177 40 100 100     6350 $ws = lc($ws) if defined($ws) && $ci;
178 40 100       130 my $ws_len = defined($ws) ? length($ws) : undef;
179              
180 40         73 my $max_result = $args{max_result};
181 40         88 my $max_compare = $args{max_compare};
182              
183 40   100     174 my $unique = $args{unique} // 0;
184              
185 40         77 my $num_compare;
186             my %found_items; # for tracking which items have been found, for -max_result
187 0         0 my @matched_els; # to avoid matching the same array element with multi items
188 0         0 my @res;
189 0         0 my %res; # for unique
190              
191             FIND:
192 40         150 for my $i (0..$#items) {
193 44 100       143 my $item = $ci ? lc($items[$i]) : $items[$i];
194 44 100       136 if ($mode eq 'regex') {
195 2 100       30 $item = qr/$item/ if ref($item) ne 'Regexp';
196 2 100       10 $item = $ci ? qr/$item/i : $item; # XXX turn off i if !$ci
197             }
198 44 100       122 my $item_len = defined($item) ? length($item) : undef;
199              
200 44         127 for my $ia (0..$#arrays) {
201 50         153 my $array = $arrays[$ia];
202 50         133 for my $iel (0..@$array-1) {
203              
204 509 100 100     1955 next if $matched_els[$ia] && $matched_els[$ia][$iel];
205 499         617 $num_compare++;
206 499         813 my $el0 = $array->[$iel];
207 499 100       1615 my $el = $ci ? lc($el0) : $el0;
208 499         571 my $match;
209              
210 499 100       1831 if (!defined($el)) {
    100          
    100          
    100          
211 2         5 $match = !defined($item);
212             } elsif (!defined($item)) {
213 2         6 $match = !defined($el);
214             } elsif ($mode eq 'exact') {
215 69         116 $match = $item eq $el;
216             } elsif ($mode eq 'regex') {
217 6         114 $match = $el =~ $item;
218             } else {
219 420         574 my $el_len = length($el);
220              
221 420 100       3238 if ($mode_prefix) {
222 241         355 my $idx = index($el, $item);
223 241 100       540 if ($idx >= 0) {
224 150 100       284 if (defined($ws)) {
225 96   66     599 $match ||=
      66        
226             # left side matches ^
227             $idx == 0 &&
228             # right side matches $ or
229             ($item_len+$idx == $el_len ||
230             # ws
231             index($el, $ws, $item_len+$idx) ==
232             $item_len+$idx);
233             } else {
234 54   66     2712 $match ||= $idx == 0;
235             }
236             }
237             }
238              
239 420 100 100     10858 if ($mode_infix && !$match) {
240 157         233 my $idx = index($el, $item);
241 157 100       966 if ($idx >= 0) {
242 85 100       159 if (defined($ws)) {
243 74   100     2377 $match ||=
      66        
244             # right side matches ws
245             index($el, $ws, $item_len+$idx) ==
246             $item_len+$idx &&
247             # left-side matches ws
248             $idx >= $ws_len &&
249             index($el, $ws, $idx-$ws_len) ==
250             $idx-$ws_len;
251             } else {
252 11   100     72 $match ||= $idx > 0 && $idx < $el_len-$item_len;
      66        
253 11 100       27 if (!$match) {
254 9 100       18 if ($idx == 0) {
255             # a -> aab should match
256 7         14 my $idx2 = index($el, $item, 1);
257 7   100     50 $match ||= $idx2 > -1 &&
      66        
258             $idx2 < $el_len-$item_len;
259             } else {
260             # a -> baa should match
261 2         7 my $idx2 = index(substr($el, 1), $item);
262 2   33     24 $match ||= $idx2 > -1 &&
      33        
263             $idx2 < $el_len-$item_len-1;
264             }
265             }
266             }
267             }
268             }
269              
270 420 100 100     1507 if ($mode_suffix && !$match) {
271 184         276 my $idx = rindex($el, $item);
272 184 100       468 if ($idx >= 0) {
273 95 100       178 if (defined($ws)) {
274 79   66     623 $match ||=
      66        
275             # right side matches $
276             $idx == $el_len-$item_len &&
277             # left-side matches ^ or
278             ($idx == 0 ||
279             # ws
280             $idx >= $ws_len &&
281             index($el, $ws, $idx-$ws_len) ==
282             $idx-$ws_len);
283             } else {
284 16   66     80 $match ||= $idx == $el_len-$item_len;
285             }
286             }
287             }
288             }
289              
290 499 100       11531 if ($match) {
291 149 100 100     395 unless ($unique && $res{$el}) {
292 143         358 push @res, $el0;
293             }
294 149 100       381 $res{$el} = 1 if $unique;
295 149   100     482 $matched_els[$ia] //= [];
296 149         383 $matched_els[$ia][$iel] = 1;
297             }
298 499 100 66     1387 if (defined($max_compare) && $max_compare != 0) {
299 12 100       470 last FIND if $num_compare >= $max_compare;
300             }
301 497 100       1529 if ($match) {
302 147 100 66     727 if (defined($max_result) && $max_result != 0) {
303 10 100       25 if ($max_result > 0) {
304 2 50       19 last FIND if @res >= $max_result;
305             } else {
306 8   100     34 $found_items{$i} //= 1;
307             last FIND if
308 8 100       41 scalar(keys %found_items) >= -$max_result;
309             }
310             }
311             }
312              
313             }
314             }
315             }
316              
317 40 100       144 if ($args{shuffle}) {
318 2         117 @res = shuffle(@res);
319             }
320              
321 40         373 \@res;
322             }
323              
324             1;
325             # ABSTRACT: Find items in array, with several options
326              
327              
328              
329             __END__