File Coverage

blib/lib/Regex/PreSuf.pm
Criterion Covered Total %
statement 164 182 90.1
branch 80 100 80.0
condition 29 36 80.5
subroutine 7 8 87.5
pod 0 4 0.0
total 280 330 84.8


line stmt bran cond sub pod time code
1             package Regex::PreSuf;
2              
3 3     3   3481 use strict;
  3         7  
  3         160  
4             local $^W = 1;
5 3     3   16 use vars qw($VERSION $DEBUG);
  3         6  
  3         307  
6              
7             $VERSION = "1.17";
8              
9             $DEBUG = 0;
10              
11             =pod
12              
13             =head1 NAME
14              
15             Regex::PreSuf - create regular expressions from word lists
16              
17             =head1 SYNOPSIS
18              
19             use Regex::PreSuf;
20            
21             my $re = presuf(qw(foobar fooxar foozap));
22              
23             # $re should be now 'foo(?:zap|[bx]ar)'
24              
25             =head1 DESCRIPTION
26              
27             The B subroutine builds regular expressions out of 'word
28             lists', lists of strings. The regular expression matches the same
29             words as the word list. These regular expressions normally run faster
30             than a simple-minded '|'-concatenation of the words.
31              
32             Examples:
33              
34             =over 4
35              
36             =item *
37              
38             'foobar fooxar' => 'foo[bx]ar'
39              
40             =item *
41              
42             'foobar foozap' => 'foo(?:bar|zap)'
43              
44             =item *
45              
46             'foobar fooar' => 'foob?ar'
47              
48             =back
49              
50             The downsides:
51              
52             =over 4
53              
54             =item *
55              
56             The original order of the words is not necessarily respected,
57             for example because the character class matches are collected
58             together, separate from the '|' alternations.
59              
60             =item *
61              
62             The module blithely ignores any specialness of any regular expression
63             metacharacters such as the C<.*?+{}[]^$>, they are just plain ordinary
64             boring characters.
65              
66             =back
67              
68             For the second downside there is an exception. The module has some
69             rudimentary grasp of how to use the 'any character' metacharacter.
70             If you call B like this:
71              
72             my $re = presuf({ anychar=>1 }, qw(foobar foo.ar fooxar));
73              
74             # $re should be now 'foo.ar'
75              
76             The module finds out the common prefixes and suffixes of the words and
77             then recursively looks at the remaining differences. However, by
78             default only common prefixes are used because for many languages
79             (natural or artificial) this seems to produce the fastest matchers.
80             To allow also for suffixes use
81              
82             my $re = presuf({ suffixes=>1 }, ...);
83              
84             To use B suffixes use
85              
86             my $re = presuf({ prefixes=>0 }, ...);
87              
88             (this implicitly enables suffixes)
89              
90             =head2 Debugging
91              
92             In case you want to flood your session without debug messages
93             you can turn on debugging by saying
94              
95             Regex::PreSuf::debug(1);
96              
97             How to turn them off again is left as an exercise for the kind reader.
98              
99             =head1 COPYRIGHT
100              
101             Jarkko Hietaniemi
102              
103             This code is distributed under the same copyright terms as Perl itself.
104              
105             =cut
106              
107 3     3   17 use vars qw(@ISA @EXPORT);
  3         8  
  3         10903  
108             require Exporter;
109             @ISA = qw(Exporter);
110             @EXPORT = qw(presuf);
111              
112             sub debug {
113 0 0   0 0 0 if (@_) {
114 0         0 $DEBUG = shift;
115             } else {
116 0         0 return $DEBUG;
117             }
118             }
119              
120             sub prefix_length {
121 39429     39429 0 50419 my $n = 0;
122 39429         39119 my %diff;
123              
124 39429         53926 for(my $m = 0; ; $m++) {
125 67729         106903 foreach (@_) {
126 554093 100       569562 $diff{ @{$_} <= $m ? '' : $_->[$m] }++;
  554093         1626364  
127             }
128 67729 100       178784 last if keys %diff > 1;
129 28303 100 66     94667 if (exists $diff{ '' } and $diff{ '' } == @_) {
130 3         7 %diff = ();
131 3         5 last;
132             }
133 28300         60813 %diff = ();
134 28300         41827 $n = $m+1;
135             }
136              
137 39429         206620 return ($n, %diff);
138             }
139              
140             sub suffix_length {
141 39429     39429 0 50761 my $n = 0;
142 39429         80207 my %diff;
143              
144 39429         55073 for(my $m = 1; ; $m++) {
145 43809         77374 foreach (@_){
146 402154 100       413676 $diff{ @{$_} < $m ? '' : $_->[-$m] }++;
  402154         1181156  
147             }
148 43809 100       120982 last if keys %diff > 1;
149 4383 100 66     11606 if (exists $diff{ '' } and $diff{ '' } == @_) {
150 3         6 %diff = ();
151 3         5 last;
152             }
153 4380         12480 %diff = ();
154 4380         6606 $n = $m;
155             }
156              
157 39429         236153 return ($n, %diff);
158             }
159              
160             sub _presuf {
161 39426     39426   52431 my $level = shift;
162 39426 50       78670 my $INDENT = " " x $level if $DEBUG;
163 39426         50394 my $param = shift;
164            
165 39426 50       103578 print "_presuf:$INDENT <- ", join(" ", map { join('', @$_) } @_), "\n"
  0         0  
166             if $DEBUG;
167              
168 39426 50       82607 return '' if @_ == 0;
169              
170 39426 100       86216 if (@_ == 1) {
171 9         13 my $presuf = join('', @{ $_[0] });
  9         31  
172 9 50       28 print "_presuf:$INDENT -> $presuf\n" if $DEBUG;
173 9         45 return $presuf;
174             }
175              
176 39417         77616 my ($pre_n, %pre_d) = prefix_length @_;
177 39417         105849 my ($suf_n, %suf_d) = suffix_length @_;
178              
179 39417 50       102480 if ($DEBUG) {
180 0         0 print "_presuf:$INDENT pre_n = $pre_n (",join(" ",%pre_d),")\n";
181 0         0 print "_presuf:$INDENT suf_n = $suf_n (",join(" ",%suf_d),")\n";
182             }
183              
184             my $prefixes = not exists $param->{ prefixes } ||
185 39417   66     183705 $param->{ prefixes };
186             my $suffixes = $param->{ suffixes } ||
187             ( exists $param->{ prefixes } &&
188 39417   66     156123 not $param->{ prefixes });
189              
190 39417 100 100     165507 if ($prefixes and not $suffixes) {
191             # On qw(rattle rattlesnake) clear suffix.
192 39407         94055 foreach (keys %pre_d) {
193 122365 100       289207 if ($_ eq '') {
194 1900         2655 $suf_n = 0;
195 1900         3626 %suf_d = ();
196 1900         2971 last;
197             }
198             }
199             }
200              
201 39417 100 100     114957 if ($suffixes and not $prefixes) {
202 3         9 foreach (keys %suf_d) {
203 8 50       21 if ($_ eq '') {
204 0         0 $pre_n = 0;
205 0         0 %pre_d = ();
206 0         0 last;
207             }
208             }
209             }
210              
211 39417 100 100     142468 if ($pre_n or $suf_n) {
212 19731 100       37516 if ($pre_n == $suf_n) {
213 1191         1863 my $eq_n = 1;
214 1191         1447 my $eq_s = join('', @{ $_[0] });
  1191         4194  
215              
216 1191         3793 foreach (@_[ 1 .. $#_ ]) {
217 1191 50       1612 last if $eq_s ne join('', @{ $_ });
  1191         3629  
218 0         0 $eq_n++;
219             }
220              
221 1191 50       3400 if ($eq_n == @_) { # All equal. How boring.
222 0 0       0 print "_presuf:$INDENT -> $eq_s\n" if $DEBUG;
223 0         0 return $eq_s;
224             }
225             }
226              
227 19731         26653 my $ps_n = $pre_n + $suf_n;
228 19731         22327 my $overlap; # Guard against prefix and suffix overlapping.
229              
230 19731         31020 foreach (@_) {
231 184615 100       197768 if (@{ $_ } < $ps_n) {
  184615         471484  
232 52         91 $overlap = 1;
233 52         83 last;
234             }
235             }
236              
237             # Remove prefixes and suffixes and recurse.
238              
239 19653         51316 my $pre_s = $pre_n ?
240 19731 100       65178 join('', @{ $_[0] }[ 0 .. $pre_n - 1 ]) : '';
241 2554         11614 my $suf_s = $suf_n ?
242 19731 100       51083 join('', @{ $_[0] }[ -$suf_n .. -1 ]) : '';
243 19731         22028 my @presuf;
244              
245 19731 100       30606 if ($overlap) {
246 52 100 66     284 if ($prefixes and not $suffixes) {
    50          
247 51         102 $suf_s = '';
248 51         107 foreach (@_) {
249 104         458 push @presuf,
250 104         170 [ @{ $_ }[ $pre_n .. $#{ $_ } ] ];
  104         188  
251             }
252             } elsif ($suffixes) {
253 1         2 $pre_s = '';
254 1         3 foreach (@_) {
255 2         7 push @presuf,
256 2         2 [ @{ $_ }[ 0 .. $#{ $_ } - $suf_n ] ];
  2         3  
257             }
258             }
259             } else {
260 19679         36398 foreach (@_) {
261 184544         984553 push @presuf,
262 184544         213465 [ @{ $_ }[ $pre_n .. $#{ $_ } - $suf_n ] ];
  184544         380901  
263             }
264             }
265              
266 19731 50       50615 if ($DEBUG) {
267 0         0 print "_presuf:$INDENT pre_s = $pre_s\n";
268 0         0 print "_presuf:$INDENT suf_s = $suf_s\n";
269 0         0 print "_presuf:$INDENT presuf = ",
270 0         0 join(" ", map { join('', @$_) } @presuf), "\n";
271             }
272              
273 19731         56016 my $presuf = $pre_s . _presuf($level + 1, $param, @presuf) . $suf_s;
274              
275 19731 50       49626 print "_presuf:$INDENT -> $presuf\n" if $DEBUG;
276              
277 19731         409091 return $presuf;
278             } else {
279 19686         37219 my @len_n;
280             my @len_1;
281 19686         21882 my $len_0 = 0;
282 19686         21109 my (@alt_n, @alt_1);
283              
284 19686         31254 foreach (@_) {
285 184595         175821 my $len = @{$_};
  184595         340583  
286 184595 100       313101 if ($len > 1) { push @len_n, $_ }
  179739 100       309054  
287 3727         10810 elsif ($len == 1) { push @len_1, $_ }
288 1129         14263 else { $len_0++ } # $len == 0
289             }
290              
291             # NOTE: does not preserve the order of the words.
292              
293 19686 100       46488 if (@len_n) { # Alternation.
294 19333 100       36262 if (@len_n == 1) {
295 2183         2916 @alt_n = join('', @{ $len_n[0] });
  2183         7079  
296             } else {
297 17150         56879 my @pre_d = keys %pre_d;
298 17150         60438 my @suf_d = keys %suf_d;
299              
300 17150         26665 my (%len_m, @len_m);
301              
302 17150 100 100     72013 if ($prefixes and $suffixes) {
303 2 100       19 if (@pre_d < @suf_d) {
304 1         2 $suffixes = 0;
305             } else {
306 1 50       5 if (@pre_d == @suf_d) {
307 1 50       5 if ( $param->{ suffixes } ) {
308 1         2 $prefixes = 0;
309             } else {
310 0         0 $suffixes = 0;
311             }
312             } else {
313 0         0 $prefixes = 0;
314             }
315             }
316             }
317              
318 17150 100       29081 if ($prefixes) {
    50          
319 17147         34113 foreach (@len_n) {
320 177545         207479 push @{ $len_m{ $_->[ 0 ] } }, $_;
  177545         473809  
321             }
322             } elsif ($suffixes) {
323 3         6 foreach (@len_n) {
324 11         12 push @{ $len_m{ $_->[ -1 ] } }, $_;
  11         33  
325             }
326             }
327              
328 17150         84811 foreach (sort keys %len_m) {
329 55498 100       68075 if (@{ $len_m{ $_ } } > 1) {
  55498         131612  
330 19612         61540 push @alt_n,
331 19612         36338 _presuf($level + 1, $param, @{ $len_m{ $_ } });
332             } else {
333 35886         40280 push @alt_n, join('', @{ $len_m{ $_ }->[0] });
  35886         189342  
334             }
335             }
336             }
337             }
338              
339 19686 100       58471 if (@len_1) { # Character classes.
340 3297 100 66     15063 if ($param->{ anychar } and
      66        
      100        
341 7         24 (exists $pre_d{ '.' } or exists $suf_d{ '.' }) and
342             grep { $_->[0] eq '.' } @len_1) {
343 3         6 push @alt_1, '.';
344             } else {
345 3294 100       6359 if (@len_1 == 1) {
346 2891         7690 push @alt_1,
347 2891         3257 join('', @{$len_1[0]});
348             } else {
349 403         605 my %uniq;
350 831         4160 push @alt_1,
351             join('', '[', (sort
352 831         2663 grep { ! $uniq{$_}++ }
353 403         1366 map { join('', @$_) } @len_1), ']' );
354             }
355             }
356             }
357              
358 19686         61164 my $alt = join('|', @alt_n, @alt_1);
359              
360 19686 100       62964 $alt = '(?:' . $alt . ')' unless @alt_n == 0;
361              
362 19686 100       40676 $alt .= '?' if $len_0;
363              
364 19686 50       39988 print "_presuf:$INDENT -> $alt\n" if $DEBUG;
365              
366 19686         146608 return $alt;
367             }
368             }
369              
370             sub presuf {
371 83 100   83 0 17841597 my $param = ref $_[0] eq 'HASH' ? shift : { };
372              
373 83 50       347 return '' if @_ == 0;
374              
375 83         293 my @args = map { quotemeta() } @_;
  42934         84870  
376              
377             # Undo quotemeta for anychars.
378 83 100       1924 @args = map { s/\\\././g; $_ } @args if $param->{ anychar };
  18         32  
  18         38  
379              
380 83         12624 s/\\(\s)/$1/g for @args;
381              
382 83         240 foreach (@args) {
383 42934         534900 $_ = [ /(\\?.)/gs ];
384             }
385              
386 83         1579 return _presuf(0, $param, @args);
387             }
388              
389             1;