File Coverage

blib/lib/Encode/Mapper.pm
Criterion Covered Total %
statement 216 327 66.0
branch 116 186 62.3
condition 18 31 58.0
subroutine 15 18 83.3
pod 9 11 81.8
total 374 573 65.2


line stmt bran cond sub pod time code
1             # ############################################################################ Otakar Smrz, 2003/01/23
2             #
3             # Mapper Engine Class ##################################################################### 2003/06/19
4              
5             package Encode::Mapper;
6              
7             our $VERSION = '14.1';
8              
9 8     8   41717 use 5.008;
  8         16  
10              
11 8     8   31 use strict;
  8         6  
  8         138  
12 8     8   27 use warnings;
  8         6  
  8         160  
13              
14 8     8   23 use Carp;
  8         8  
  8         1009  
15              
16 8     8   1478 use bytes; # ensures splitting into one-byte tokens .. lexically scoped
  8         29  
  8         28  
17              
18              
19             our %options; # records of options per package .. global register
20             our %option; # options of the caller package .. used with local
21              
22              
23             sub import { # enforces setting of options
24 13     13   41 my $cls = shift @_;
25 13 100       102 $cls->options(@_) if @_;
26             }
27              
28              
29             sub whisper ($) { # module's internal function
30              
31 1 50   1 0 3 carp shift unless $option{'silent'};
32             }
33              
34              
35             sub verify_rule ($$) { # module's internal function
36              
37 158302 50 33 158302 0 383974 unless (defined $_[0] and $_[0] ne '') {
38 0         0 whisper "Rule's LHS is empty, rule ignored";
39 0         0 return;
40             }
41 158302 50       166512 unless (defined $_[1]) {
42 0         0 whisper "Rule's RHS is undefined, rule ignored";
43 0         0 return;
44             }
45              
46 158302 100 66     254507 if (UNIVERSAL::isa($_[1], 'ARRAY')) {
    50          
47 130977 50       163423 unless (defined $_[1]->[0]) {
48 0         0 whisper "Rule's RHS is undefined, rule ignored";
49 0         0 return;
50             }
51 130977 50 66     196969 unless (ref \$_[1]->[0] eq 'SCALAR' or UNIVERSAL::isa($_[1]->[0], 'CODE')) {
52 0         0 whisper "Rule's RHS is neither literal nor subroutine reference, rule ignored";
53 0         0 return;
54             }
55 130977 50 33     337499 unless (defined $_[1]->[1] and length $_[1]->[1] < length $_[0]) {
56 0         0 whisper "Rule type '\$A => [\$X, \$Y], length \$A > length \$Y' misused, considering it '\$A => \$X'";
57 0         0 $_[1] = $_[1]->[0];
58             }
59             }
60             elsif (ref \$_[1] ne 'SCALAR' and not UNIVERSAL::isa($_[1], 'CODE')) {
61 0         0 whisper "Rule's RHS is neither literal nor subroutine reference, rule ignored";
62 0         0 return;
63             }
64              
65 158302         195013 return 1;
66             }
67              
68              
69             sub options ($%) { # options for general compilation of Mappers
70 27     27 1 57 my $cls = shift @_;
71 27         27 my ($i, $opt, %opt);
72              
73 27 100       39 my $caller = caller 0; $caller = caller 1 if $caller eq __PACKAGE__;
  27         85  
74 27 100       92 my @returns = exists $options{$caller} ? %{$options{$caller}} : ();
  17         65  
75              
76 27         952 while (@_) {
77 52         87 $opt = lc shift @_;
78              
79 52 100       135 if ($opt =~ /^\:/) {
80 28 100 100 2191   126 $opt eq ':others' and $opt{'others'} = sub { shift } and next;
  2191         3198  
81 18 100 100     64 $opt eq ':silent' and $opt{'silent'} = 1 and next;
82 8 50       28 $opt eq ':join' and $opt{'join'} = '';
83             }
84             else {
85 24         92 $opt =~ /^\-*(.*)$/;
86 24         89 $opt{$1} = shift @_;
87             }
88             }
89              
90             {
91 27 100       31 local $option{'silent'} = exists $opt{'silent'} ? $opt{'silent'} : $options{$caller}{'silent'};
  27         75  
92              
93 27 100 66     88 if (defined $opt{'complement'} and UNIVERSAL::isa($opt{'complement'}, 'ARRAY')) {
94 1         2 for ($i = 0; $i < @{$opt{'complement'}}; $i += 2) {
  3         8  
95 2         6 verify_rule $opt{'complement'}->[$i], $opt{'complement'}->[$i + 1];
96             }
97             }
98              
99 27 100 66     90 if (defined $opt{'override'} and UNIVERSAL::isa($opt{'override'}, 'ARRAY')) {
100 3         6 for ($i = 0; $i < @{$opt{'override'}}; $i += 2) {
  1554         1932  
101 1551         1524 verify_rule $opt{'override'}->[$i], $opt{'override'}->[$i + 1];
102             }
103             }
104              
105 27 50 66     125 if (defined $opt{'others'} and not $option{'silent'}) { # see whisper
106 0 0       0 if (UNIVERSAL::isa($opt{'others'}, 'CODE')) {
107 0         0 carp "The subroutine will be called with the 'other' LHS parameter to get the rule's RHS";
108             }
109             else {
110 0         0 carp "The scalar value will become the RHS of each 'other' LHS";
111             }
112             }
113             }
114              
115 27 100       131 return %opt unless defined $cls;
116              
117 16         72 $options{$caller}{$_} = $opt{$_} foreach keys %opt;
118              
119 16         7335 return @returns;
120             }
121              
122              
123             *new = *compile{'CODE'}; # provides the 'new' constructor .. the 'compile' method
124             # *new = \&compile; # might be known at compile-time
125              
126              
127             sub compile ($@) { # returns Mapper .. modified Aho-Corasick and Boyer-Moore search engine
128 15     15 1 12760 my $cls = shift @_;
129 15         27 my (@tree, @bell, @skip, @queue, %redef);
130 0         0 my ($q, $r, $s, $t, $i, $token, $trick);
131              
132 15         34 my ($null_list, $null_hash) = ([], {}); # references to empties need not consume unique memory
133 15         33 my ($no_code, $no_list) = (1, 1); # optimization indicators
134              
135 15 100       88 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  14         134  
136             # options be local due to verify_rule and whisper
137              
138 15 100       89 if (UNIVERSAL::isa($_[0], 'ARRAY')) {
    50          
139 11         35 %option = (%option, options undef, @{shift @_});
  11         36  
140             }
141             elsif (UNIVERSAL::isa($_[0], 'HASH')) {
142 0         0 %option = (%option, options undef, %{shift @_});
  0         0  
143             }
144              
145 15         37 $skip[0] = undef; # never ever used .. fix the number of list elements equal
146 15         21 $bell[0] = $null_list; # important .. depth-wise inheritation of the lists
147              
148 15 100       47 if (defined $option{'complement'}) {
149 1         1 for ($i = 0; $i < @{$option{'complement'}}; $i += 2) {
  3         7  
150              
151 2         2 $q = 0;
152              
153 2         5 foreach $token (split //, $option{'complement'}->[$i]) {
154 2 100       5 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
155 2         3 $q = $tree[$q]->{$token};
156             }
157              
158 2 100       4 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
159              
160 2 100       10 whisper "Redefining the mapping for '" . $option{'complement'}->[$i] . "'" if defined $bell[$q];
161              
162 2         5 $bell[$q] = [ $option{'complement'}->[$i + 1] ];
163             }
164             }
165              
166 15         53 for ($i = 0; $i < @_; $i += 2) { # generate $tree[$q] transition function and initial $bell[$q]
167              
168 156749 50       167147 next unless verify_rule $_[$i], $_[$i + 1];
169              
170 156749         98299 $q = 0;
171              
172 156749         261366 foreach $token (split //, $_[$i]) {
173 945101 100       1101164 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
174 945101         675012 $q = $tree[$q]->{$token};
175             }
176              
177 156749 100       261449 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
178              
179 156749 50       315372 whisper "Redefining the mapping for '$_[$i]'" if $redef{$q}++;
180              
181 156749         297051 $bell[$q] = [ $_[$i + 1] ];
182             }
183              
184 15 100       49 if (defined $option{'override'}) {
185 10         18 for ($i = 0; $i < @{$option{'override'}}; $i += 2) {
  5180         6514  
186              
187 5170         2940 $q = 0;
188              
189 5170         6309 foreach $token (split //, $option{'override'}->[$i]) {
190 10350 100       13994 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
191 10350         7567 $q = $tree[$q]->{$token};
192             }
193              
194 5170 100       7475 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
195              
196 5170 50       8345 whisper "Redefining the mapping for '" . $option{'override'}->[$i] . "'" if $redef{$q}++;
197              
198 5170         7369 $bell[$q] = [ $option{'override'}->[$i + 1] ];
199             }
200             }
201              
202 15         49 foreach $token (map { chr } 0x00..0xFF) {
  3840         3183  
203 3840 100       4722 unless (defined $tree[0]->{$token}) {
204 3283 100       3068 unless (defined $option{'others'}) {
205 898         872 $tree[0]->{$token} = 0;
206             }
207             else {
208 2385         2218 $tree[0]->{$token} = ++$r; # increment $r ^^
209 2385         2133 $tree[$r] = {}; # define trees correctly
210             }
211             }
212              
213 3840         2576 $q = $tree[0]->{$token}; # including existing prefixes
214              
215 3840 100       4169 unless ($q == 0) {
216 2942 100       3197 unless (defined $bell[$q]) {
217 2458 100       3743 if (not defined $option{'others'}) {
    50          
218 12         13 $bell[$q] = $bell[0];
219             }
220             elsif (UNIVERSAL::isa($option{'others'}, 'CODE')) {
221 2446         2187 $bell[$q] = [ $option{'others'}->($token) ];
222             }
223             else {
224 0         0 $bell[$q] = [ $option{'others'} ];
225             }
226             }
227              
228 2942         3568 $skip[$q] = 0;
229              
230 2942         2595 push @queue, $q;
231             }
232             }
233              
234 15         178 while (@queue) { # generate $skip[$q] backward function and complete $bell[$q]
235 188058         130378 $q = shift @queue;
236              
237 188058         105048 foreach $token (keys %{$tree[$q]}) {
  188058         270589  
238 185116         135299 $t = $tree[$q]->{$token};
239 185116         114691 push @queue, $t;
240              
241 185116 100       175294 if (defined $bell[$t]) {
242 161435         99428 $skip[$t] = 0;
243              
244 161435 100       286428 if (UNIVERSAL::isa($bell[$t]->[0], 'ARRAY')) { # shortening property of the rules
245 134568         82126 $s = $skip[$t];
246              
247 134568         192152 foreach $trick (split //, $bell[$t]->[0]->[1]) {
248 327467         400882 until (defined $tree[$s]->{$trick}) { # loops only if not in the root ^^
249 114135         67165 push @{$bell[$t]}, @{$bell[$s]};
  114135         86215  
  114135         116458  
250 114135         154986 $s = $skip[$s];
251             }
252 327467         251509 $s = $tree[$s]->{$trick};
253             }
254              
255 134568         105963 $skip[$t] = $s;
256 134568         144456 $bell[$t]->[0] = $bell[$t]->[0]->[0];
257             }
258             }
259             else {
260 23681         15179 $s = $skip[$q];
261 23681         13297 $bell[$t] = [ @{$bell[$q]} ]; # unique reference quite important ^^
  23681         32659  
262              
263 23681         31962 until (defined $tree[$s]->{$token}) { # extremely tricky ...
264 6147         3730 push @{$bell[$t]}, @{$bell[$s]};
  6147         4826  
  6147         6192  
265 6147         8438 $s = $skip[$s];
266             }
267              
268 23681         21201 $skip[$t] = $tree[$s]->{$token};
269             }
270             }
271              
272 188058 100       116913 $tree[$q] = $null_hash unless keys %{$tree[$q]}; # economize with memory
  188058         351916  
273             }
274              
275 15         61 for ($q = 1; $q < @bell; $q++) { # optimize the bell function for $q > 0
276              
277 188058 100       106371 if (grep { UNIVERSAL::isa($_, 'CODE') } @{$bell[$q]}) {
  316312 50       651864  
  188058         166325  
278 8         9 $no_code = 0;
279             }
280             elsif (defined $option{'join'}) {
281 0         0 $bell[$q] = join $option{'join'}, @{$bell[$q]};
  0         0  
282 0         0 next;
283             }
284              
285 188058 100       108613 if (@{$bell[$q]} == 1) {
  188058         186424  
286 68344         113350 $bell[$q] = $bell[$q]->[0];
287             }
288             else {
289 119714 100       67578 $bell[$q] = $null_list if @{$bell[$q]} == 0;
  119714         134580  
290 119714         158108 $no_list = 0;
291             }
292             }
293              
294             return bless {
295             'current' => 0,
296             'tree' => \@tree,
297             'bell' => \@bell,
298             'skip' => \@skip,
299             'null' => { 'list' => $null_list, 'hash' => $null_hash },
300 15         46661 'join' => $option{'join'},
301             'no_code' => $no_code,
302             'no_list' => $no_list,
303             }, $cls;
304             }
305              
306              
307             sub process ($@) { # returns the list of search results performed by Mapper
308 18     18 1 41198 my $obj = shift @_;
309 18         19 my (@returns, $phrase, $token, $q);
310              
311 18         27 $q = $obj->{'current'};
312              
313 18 100       38 if ($obj->{'no_list'}) {
314 7         13 foreach $phrase (@_) {
315 118         216 foreach $token (split //, $phrase) {
316 724         918 until (defined $obj->{'tree'}[$q]->{$token}) {
317 496         490 push @returns, $obj->{'bell'}[$q];
318 496         691 $q = $obj->{'skip'}[$q];
319             }
320 724         631 $q = $obj->{'tree'}[$q]->{$token};
321             }
322             }
323             }
324             else {
325 11         19 foreach $phrase (@_) {
326 21         81 foreach $token (split //, $phrase) {
327 572         808 until (defined $obj->{'tree'}[$q]->{$token}) {
328             push @returns, ref $obj->{'bell'}[$q] eq 'ARRAY' ?
329 386 100       549 @{$obj->{'bell'}[$q]} : $obj->{'bell'}[$q];
  27         52  
330 386         593 $q = $obj->{'skip'}[$q];
331             }
332 572         477 $q = $obj->{'tree'}[$q]->{$token};
333             }
334             }
335             }
336              
337 18         25 $obj->{'current'} = $q;
338              
339 18         242 return @returns;
340             }
341              
342              
343             sub recover ($;$$) { # returns the 'in-progress' search result and resets Mapper
344 18     18 1 27 my ($obj, $r, $q) = @_;
345 18         18 my (@returns);
346              
347 18 50       60 $q = $obj->{'current'} unless defined $q;
348              
349 18 100       38 if ($obj->{'no_list'}) {
350 7         20 until ($q == 0) {
351 7         13 push @returns, $obj->{'bell'}[$q];
352 7         17 $q = $obj->{'skip'}[$q];
353             }
354             }
355             else {
356 11         27 until ($q == 0) {
357             push @returns, ref $obj->{'bell'}[$q] eq 'ARRAY' ?
358 13 100       34 @{$obj->{'bell'}[$q]} : $obj->{'bell'}[$q];
  2         3  
359 13         30 $q = $obj->{'skip'}[$q];
360             }
361             }
362              
363 18 50       38 $obj->{'current'} = defined $r ? $r : 0;
364              
365 18         101 return @returns;
366             }
367              
368              
369             sub compute ($@) {
370 0     0 1 0 my $obj = shift @_;
371 0         0 my (@returns, $phrase, $token, $q);
372              
373 0         0 $obj->recover();
374              
375 0         0 foreach $phrase (@_) {
376 0         0 foreach $token (split //, $phrase) {
377 0         0 push @returns, [$token, $obj->{'current'}];
378 0         0 push @{$returns[-1]}, [$obj->process($token)];
  0         0  
379 0         0 $q = $obj->{'current'};
380 0         0 push @{$returns[-1]}, $q, $obj->{'bell'}[$q], $obj->{'skip'}[$q];
  0         0  
381             }
382             }
383              
384 0         0 push @returns, ['recover', $obj->{'current'}];
385 0         0 push @{$returns[-1]}, [$obj->recover()];
  0         0  
386 0         0 $q = $obj->{'current'};
387 0 0       0 push @{$returns[-1]}, $q, $obj->{'bell'}[$q], ($q == 0 ? 'undef' : $obj->{'skip'}[$q]);
  0         0  
388              
389 0         0 return @returns;
390             }
391              
392              
393             sub dumper ($;$) {
394 0     0 1 0 my ($obj, $ref) = @_;
395              
396 0 0       0 $ref = ['L', 'H', 'mapper'] unless defined $ref;
397              
398 0         0 require Data::Dumper;
399              
400 0         0 return Data::Dumper->new([$obj->{'null'}{'list'}, $obj->{'null'}{'hash'}, $obj], $ref);
401             }
402              
403              
404             sub describe ($;$) {
405 0     0 1 0 my ($obj, $ref) = @_;
406 0         0 my ($q, $nodes, $edges, $skips, $bells, $paths, $lists);
407              
408 0         0 $nodes = @{$obj->{'tree'}};
  0         0  
409 0         0 $edges = [];
410 0         0 $lists = [];
411              
412 0 0       0 if ($obj->{'no_list'}) {
413 0         0 for ($q = 0; $q < @{$obj->{'tree'}}; $q++) {
  0         0  
414 0         0 $lists->[$q * 3] = scalar %{$obj->{'tree'}[$q]};
  0         0  
415 0         0 $lists->[$q * 3] =~ m{^([0-9]+)(?:/([0-9]+))?$};
416              
417 0         0 $edges->[0] += scalar keys %{$obj->{'tree'}[$q]};
  0         0  
418 0         0 $lists->[$q * 3] .= " " . keys %{$obj->{'tree'}[$q]};
  0         0  
419              
420 0 0       0 if (defined $2) {
421 0         0 $edges->[1] += $1;
422 0         0 $edges->[2] += $2;
423             }
424             else {
425 0 0       0 $paths++ unless $1;
426             }
427              
428 0 0       0 $lists->[$q * 3 + 1] = $obj->{'bell'}[$q] eq "" ? 0 : 1;
429 0         0 $bells += $lists->[$q * 3 + 1];
430              
431 0 0       0 next if $q == 0;
432              
433 0         0 $lists->[$obj->{'skip'}[$q] * 3 + 2]++;
434 0 0       0 $skips++ unless $obj->{'skip'}[$q] == 0;
435             }
436             }
437             else {
438 0         0 for ($q = 0; $q < @{$obj->{'tree'}}; $q++) {
  0         0  
439 0         0 $lists->[$q * 3] = scalar %{$obj->{'tree'}[$q]};
  0         0  
440 0         0 $lists->[$q * 3] =~ m{^([0-9]+)(?:/([0-9]+))?$};
441              
442 0         0 $edges->[0] += scalar keys %{$obj->{'tree'}[$q]};
  0         0  
443 0         0 $lists->[$q * 3] .= " " . keys %{$obj->{'tree'}[$q]};
  0         0  
444              
445 0 0       0 if (defined $2) {
446 0         0 $edges->[1] += $1;
447 0         0 $edges->[2] += $2;
448             }
449             else {
450 0 0       0 $paths++ unless $1;
451             }
452              
453 0 0       0 $lists->[$q * 3 + 1] = ref $obj->{'bell'}[$q] eq 'ARRAY' ? scalar @{$obj->{'bell'}[$q]} : 1;
  0         0  
454 0         0 $bells += $lists->[$q * 3 + 1];
455              
456 0 0       0 next if $q == 0;
457              
458 0         0 $lists->[$obj->{'skip'}[$q] * 3 + 2]++;
459 0 0       0 $skips++ unless $obj->{'skip'}[$q] == 0;
460             }
461             }
462              
463 0         0 my $return = {'nodes' => $nodes, 'edges' => $edges->[0], 'slots' => $edges->[1] . "/" . $edges->[2],
464             'skips' => $skips, 'bells' => $bells, 'paths' => $paths, 'lists' => $lists};
465              
466 0 0       0 if (defined $ref) {
467 0 0       0 $ref = *STDERR if ref $ref ne 'GLOB';
468 0 0       0 print $ref ( join ", ", map { ( defined $return->{$_} ? $return->{$_} : 'undef' ) . " " . $_ }
469 0         0 grep { $_ ne 'lists' } keys %{$return} ) . "\n";
  0         0  
  0         0  
470             }
471              
472 0         0 return $return;
473             }
474              
475              
476             sub encode ($$$;$) {
477 6     6 1 9 my ($cls, $text, $encoder, $enc) = @_;
478 6         7 my ($mapper, $join);
479              
480 6 50       19 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  6         31  
481             # options be local due to whisper
482              
483 6         35 require Encode;
484              
485 6 50       816 unless (Encode::is_utf8($text)) {
486 0         0 whisper "The input text is not in Perl's internal utf8 .. note only, might be fine";
487             }
488              
489 6 50       12 if ($enc) {
490 0 0       0 unless (Encode::resolve_alias($enc)) {
491 0         0 carp "Cannot resolve the proposed '$enc' encoding";
492 0         0 return undef;
493             }
494              
495 0         0 $text = Encode::encode($enc, $text);
496             }
497              
498 6 100 33     36 if (not UNIVERSAL::isa($encoder, 'ARRAY') or grep { defined $_ and not $_->isa($cls) } @{$encoder}) {
  26 50       494  
  6         11  
499 0         0 carp "Expecting a reference to an array of '$cls' objects";
500 0         0 return undef;
501             }
502              
503 6         7 foreach $mapper (@{$encoder}) {
  6         10  
504 10 100       25 last unless defined $mapper;
505              
506             $join = defined $mapper->{'join'} ? $mapper->{'join'} :
507 6 100       336 defined $option{'join'} ? $option{'join'} : "";
    50          
508              
509 6 50       12 if ($mapper->{'no_code'}) {
510 6         16 $text = join $join, $mapper->process($text), $mapper->recover();
511             }
512             else {
513             $text = join $join, map {
514 0 0       0 UNIVERSAL::isa($_, 'CODE') ? $_->() : $_
  0         0  
515             } $mapper->process($text), $mapper->recover();
516             }
517             }
518              
519 6         24 return $text;
520             }
521              
522              
523             sub decode ($$$;$) {
524 3     3 1 6 my ($cls, $text, $decoder, $enc) = @_;
525 3         3 my ($mapper, $join);
526              
527 3 50       11 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  3         14  
528             # options be local due to tradition ^^
529              
530 3         12 require Encode;
531              
532 3 50       9 $enc = 'utf8' unless $enc;
533              
534 3 50       9 unless (Encode::resolve_alias($enc)) {
535 0         0 carp "Cannot resolve the proposed '$enc' encoding";
536 0         0 return undef;
537             }
538              
539 3 100 33     71 if (not UNIVERSAL::isa($decoder, 'ARRAY') or grep { defined $_ and not $_->isa($cls) } @{$decoder}) {
  15 50       58  
  3         6  
540 0         0 carp "Expecting a reference to an array of $cls objects";
541 0         0 return undef;
542             }
543              
544 3         4 foreach $mapper (@{$decoder}) {
  3         79  
545 9 100       22 last unless defined $mapper;
546              
547             $join = defined $mapper->{'join'} ? $mapper->{'join'} :
548 7 100       25 defined $option{'join'} ? $option{'join'} : "";
    50          
549              
550 7 50       12 if ($mapper->{'no_code'}) {
551 7         14 $text = join $join, $mapper->process($text), $mapper->recover();
552             }
553             else {
554             $text = join $join, map {
555 0 0       0 UNIVERSAL::isa($_, 'CODE') ? $_->() : $_
  0         0  
556             } $mapper->process($text), $mapper->recover();
557             }
558             }
559              
560 3 50       25 return Encode::is_utf8($text) ? $text : Encode::decode($enc, $text);
561             }
562              
563              
564             1;
565              
566             __END__