File Coverage

blib/lib/Array/Find.pm
Criterion Covered Total %
statement 94 97 96.9
branch 78 80 97.5
condition 58 77 75.3
subroutine 5 5 100.0
pod 1 1 100.0
total 236 260 90.7


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