File Coverage

blib/lib/Encode/Mapper.pm
Criterion Covered Total %
statement 217 328 66.1
branch 116 186 62.3
condition 18 31 58.0
subroutine 15 18 83.3
pod 9 11 81.8
total 375 574 65.3


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   56546 use 5.008;
  8         25  
  8         307  
10              
11 8     8   40 use strict;
  8         12  
  8         351  
12 8     8   44 use warnings;
  8         9  
  8         260  
13              
14 8     8   43 use Carp;
  8         11  
  8         606  
15              
16 8     8   1920 use bytes; # ensures splitting into one-byte tokens .. lexically scoped
  8         32  
  8         40  
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   61 my $cls = shift @_;
25 13 100       164 $cls->options(@_) if @_;
26             }
27              
28              
29             sub whisper ($) { # module's internal function
30              
31 1 50   1 0 5 carp shift unless $option{'silent'};
32             }
33              
34              
35             sub verify_rule ($$) { # module's internal function
36              
37 158302 50 33 158302 0 529669 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       229271 unless (defined $_[1]) {
42 0         0 whisper "Rule's RHS is undefined, rule ignored";
43 0         0 return;
44             }
45              
46 158302 100 66     388850 if (UNIVERSAL::isa($_[1], 'ARRAY')) {
    50          
47 130977 50       231943 unless (defined $_[1]->[0]) {
48 0         0 whisper "Rule's RHS is undefined, rule ignored";
49 0         0 return;
50             }
51 130977 50 66     286424 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     455315 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         272294 return 1;
66             }
67              
68              
69             sub options ($%) { # options for general compilation of Mappers
70 27     27 1 80 my $cls = shift @_;
71 27         40 my ($i, $opt, %opt);
72              
73 27 100       65 my $caller = caller 0; $caller = caller 1 if $caller eq __PACKAGE__;
  27         114  
74 27 100       96 my @returns = exists $options{$caller} ? %{$options{$caller}} : ();
  17         79  
75              
76 27         135 while (@_) {
77 52         117 $opt = lc shift @_;
78              
79 52 100       178 if ($opt =~ /^\:/) {
80 28 100 100 2191   177 $opt eq ':others' and $opt{'others'} = sub { shift } and next;
  2191         4491  
81 18 100 100     95 $opt eq ':silent' and $opt{'silent'} = 1 and next;
82 8 50       44 $opt eq ':join' and $opt{'join'} = '';
83             }
84             else {
85 24         102 $opt =~ /^\-*(.*)$/;
86 24         113 $opt{$1} = shift @_;
87             }
88             }
89              
90             {
91 27 100       39 local $option{'silent'} = exists $opt{'silent'} ? $opt{'silent'} : $options{$caller}{'silent'};
  27         117  
92              
93 27 100 66     111 if (defined $opt{'complement'} and UNIVERSAL::isa($opt{'complement'}, 'ARRAY')) {
94 1         3 for ($i = 0; $i < @{$opt{'complement'}}; $i += 2) {
  3         7  
95 2         6 verify_rule $opt{'complement'}->[$i], $opt{'complement'}->[$i + 1];
96             }
97             }
98              
99 27 100 66     136 if (defined $opt{'override'} and UNIVERSAL::isa($opt{'override'}, 'ARRAY')) {
100 3         8 for ($i = 0; $i < @{$opt{'override'}}; $i += 2) {
  1554         3129  
101 1551         2879 verify_rule $opt{'override'}->[$i], $opt{'override'}->[$i + 1];
102             }
103             }
104              
105 27 50 66     167 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       133 return %opt unless defined $cls;
116              
117 16         130 $options{$caller}{$_} = $opt{$_} foreach keys %opt;
118              
119 16         12383 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 17284 my $cls = shift @_;
129 15         28 my (@tree, @bell, @skip, @queue, %redef);
130 0         0 my ($q, $r, $s, $t, $i, $token, $trick);
131              
132 15         45 my ($null_list, $null_hash) = ([], {}); # references to empties need not consume unique memory
133 15         32 my ($no_code, $no_list) = (1, 1); # optimization indicators
134              
135 15 100       76 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  14         146  
136             # options be local due to verify_rule and whisper
137              
138 15 100       100 if (UNIVERSAL::isa($_[0], 'ARRAY')) {
    50          
139 11         35 %option = (%option, options undef, @{shift @_});
  11         49  
140             }
141             elsif (UNIVERSAL::isa($_[0], 'HASH')) {
142 0         0 %option = (%option, options undef, %{shift @_});
  0         0  
143             }
144              
145 15         42 $skip[0] = undef; # never ever used .. fix the number of list elements equal
146 15         25 $bell[0] = $null_list; # important .. depth-wise inheritation of the lists
147              
148 15 100       60 if (defined $option{'complement'}) {
149 1         2 for ($i = 0; $i < @{$option{'complement'}}; $i += 2) {
  3         9  
150              
151 2         3 $q = 0;
152              
153 2         7 foreach $token (split //, $option{'complement'}->[$i]) {
154 2 100       9 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
155 2         6 $q = $tree[$q]->{$token};
156             }
157              
158 2 100       13 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
159              
160 2 100       13 whisper "Redefining the mapping for '" . $option{'complement'}->[$i] . "'" if defined $bell[$q];
161              
162 2         8 $bell[$q] = [ $option{'complement'}->[$i + 1] ];
163             }
164             }
165              
166 15         56 for ($i = 0; $i < @_; $i += 2) { # generate $tree[$q] transition function and initial $bell[$q]
167              
168 156749 50       260297 next unless verify_rule $_[$i], $_[$i + 1];
169              
170 156749         141151 $q = 0;
171              
172 156749         375312 foreach $token (split //, $_[$i]) {
173 945101 100       1592035 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
174 945101         1022596 $q = $tree[$q]->{$token};
175             }
176              
177 156749 100       378091 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
178              
179 156749 50       458585 whisper "Redefining the mapping for '$_[$i]'" if $redef{$q}++;
180              
181 156749         433520 $bell[$q] = [ $_[$i + 1] ];
182             }
183              
184 15 100       63 if (defined $option{'override'}) {
185 10         21 for ($i = 0; $i < @{$option{'override'}}; $i += 2) {
  5180         9010  
186              
187 5170         3971 $q = 0;
188              
189 5170         9208 foreach $token (split //, $option{'override'}->[$i]) {
190 10350 100       20502 $tree[$q]->{$token} = ++$r unless defined $tree[$q]->{$token}; # increment $r ^^
191 10350         12955 $q = $tree[$q]->{$token};
192             }
193              
194 5170 100       10719 $tree[$q] = {} unless defined $tree[$q]; # define trees correctly, economize below
195              
196 5170 50       12369 whisper "Redefining the mapping for '" . $option{'override'}->[$i] . "'" if $redef{$q}++;
197              
198 5170         11282 $bell[$q] = [ $option{'override'}->[$i + 1] ];
199             }
200             }
201              
202 15         67 foreach $token (map { chr } 0x00..0xFF) {
  3840         4192  
203 3840 100       6483 unless (defined $tree[0]->{$token}) {
204 3283 100       4325 unless (defined $option{'others'}) {
205 898         1590 $tree[0]->{$token} = 0;
206             }
207             else {
208 2385         3232 $tree[0]->{$token} = ++$r; # increment $r ^^
209 2385         2820 $tree[$r] = {}; # define trees correctly
210             }
211             }
212              
213 3840         3778 $q = $tree[0]->{$token}; # including existing prefixes
214              
215 3840 100       5513 unless ($q == 0) {
216 2942 100       3930 unless (defined $bell[$q]) {
217 2458 100       5492 if (not defined $option{'others'}) {
    50          
218 12         20 $bell[$q] = $bell[0];
219             }
220             elsif (UNIVERSAL::isa($option{'others'}, 'CODE')) {
221 2446         2992 $bell[$q] = [ $option{'others'}->($token) ];
222             }
223             else {
224 0         0 $bell[$q] = [ $option{'others'} ];
225             }
226             }
227              
228 2942         4834 $skip[$q] = 0;
229              
230 2942         3485 push @queue, $q;
231             }
232             }
233              
234 15         288 while (@queue) { # generate $skip[$q] backward function and complete $bell[$q]
235 188058         176722 $q = shift @queue;
236              
237 188058         131057 foreach $token (keys %{$tree[$q]}) {
  188058         390735  
238 185116         209656 $t = $tree[$q]->{$token};
239 185116         160159 push @queue, $t;
240              
241 185116 100       236339 if (defined $bell[$t]) {
242 161435         140607 $skip[$t] = 0;
243              
244 161435 100       414940 if (UNIVERSAL::isa($bell[$t]->[0], 'ARRAY')) { # shortening property of the rules
245 134568         110838 $s = $skip[$t];
246              
247 134568         267749 foreach $trick (split //, $bell[$t]->[0]->[1]) {
248 327467         541727 until (defined $tree[$s]->{$trick}) { # loops only if not in the root ^^
249 114135         83627 push @{$bell[$t]}, @{$bell[$s]};
  114135         114885  
  114135         174245  
250 114135         226376 $s = $skip[$s];
251             }
252 327467         379728 $s = $tree[$s]->{$trick};
253             }
254              
255 134568         148172 $skip[$t] = $s;
256 134568         233832 $bell[$t]->[0] = $bell[$t]->[0]->[0];
257             }
258             }
259             else {
260 23681         22085 $s = $skip[$q];
261 23681         17733 $bell[$t] = [ @{$bell[$q]} ]; # unique reference quite important ^^
  23681         53488  
262              
263 23681         50317 until (defined $tree[$s]->{$token}) { # extremely tricky ...
264 6147         5039 push @{$bell[$t]}, @{$bell[$s]};
  6147         6341  
  6147         9386  
265 6147         12845 $s = $skip[$s];
266             }
267              
268 23681         35243 $skip[$t] = $tree[$s]->{$token};
269             }
270             }
271              
272 188058 100       166219 $tree[$q] = $null_hash unless keys %{$tree[$q]}; # economize with memory
  188058         467595  
273             }
274              
275 15         68 for ($q = 1; $q < @bell; $q++) { # optimize the bell function for $q > 0
276              
277 188058 100       131953 if (grep { UNIVERSAL::isa($_, 'CODE') } @{$bell[$q]}) {
  316312 50       982464  
  188058         252886  
278 8         10 $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       142322 if (@{$bell[$q]} == 1) {
  188058         258660  
286 68344         166053 $bell[$q] = $bell[$q]->[0];
287             }
288             else {
289 119714 100       85713 $bell[$q] = $null_list if @{$bell[$q]} == 0;
  119714         178719  
290 119714         211691 $no_list = 0;
291             }
292             }
293              
294 15         69024 return bless {
295             'current' => 0,
296             'tree' => \@tree,
297             'bell' => \@bell,
298             'skip' => \@skip,
299             'null' => { 'list' => $null_list, 'hash' => $null_hash },
300             '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 48251 my $obj = shift @_;
309 18         21 my (@returns, $phrase, $token, $q);
310              
311 18         31 $q = $obj->{'current'};
312              
313 18 100       42 if ($obj->{'no_list'}) {
314 7         13 foreach $phrase (@_) {
315 118         220 foreach $token (split //, $phrase) {
316 724         1180 until (defined $obj->{'tree'}[$q]->{$token}) {
317 496         542 push @returns, $obj->{'bell'}[$q];
318 496         894 $q = $obj->{'skip'}[$q];
319             }
320 724         822 $q = $obj->{'tree'}[$q]->{$token};
321             }
322             }
323             }
324             else {
325 11         17 foreach $phrase (@_) {
326 21         100 foreach $token (split //, $phrase) {
327 572         986 until (defined $obj->{'tree'}[$q]->{$token}) {
328 27         53 push @returns, ref $obj->{'bell'}[$q] eq 'ARRAY' ?
329 386 100       668 @{$obj->{'bell'}[$q]} : $obj->{'bell'}[$q];
330 386         765 $q = $obj->{'skip'}[$q];
331             }
332 572         619 $q = $obj->{'tree'}[$q]->{$token};
333             }
334             }
335             }
336              
337 18         26 $obj->{'current'} = $q;
338              
339 18         236 return @returns;
340             }
341              
342              
343             sub recover ($;$$) { # returns the 'in-progress' search result and resets Mapper
344 18     18 1 31 my ($obj, $r, $q) = @_;
345 18         21 my (@returns);
346              
347 18 50       57 $q = $obj->{'current'} unless defined $q;
348              
349 18 100       41 if ($obj->{'no_list'}) {
350 7         20 until ($q == 0) {
351 7         13 push @returns, $obj->{'bell'}[$q];
352 7         21 $q = $obj->{'skip'}[$q];
353             }
354             }
355             else {
356 11         26 until ($q == 0) {
357 2         4 push @returns, ref $obj->{'bell'}[$q] eq 'ARRAY' ?
358 13 100       41 @{$obj->{'bell'}[$q]} : $obj->{'bell'}[$q];
359 13         31 $q = $obj->{'skip'}[$q];
360             }
361             }
362              
363 18 50       39 $obj->{'current'} = defined $r ? $r : 0;
364              
365 18         104 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' ) . " " . $_ }
  0         0  
469 0         0 grep { $_ ne 'lists' } keys %{$return} ) . "\n";
  0         0  
470             }
471              
472 0         0 return $return;
473             }
474              
475              
476             sub encode ($$$;$) {
477 6     6 1 14 my ($cls, $text, $encoder, $enc) = @_;
478 6         8 my ($mapper, $join);
479              
480 6 50       25 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  6         34  
481             # options be local due to whisper
482              
483 6         40 require Encode;
484              
485 6 50       1357 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       413 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     30 if (not UNIVERSAL::isa($encoder, 'ARRAY') or grep { defined $_ and not $_->isa($cls) } @{$encoder}) {
  26 50       687  
  6         16  
499 0         0 carp "Expecting a reference to an array of '$cls' objects";
500 0         0 return undef;
501             }
502              
503 6         11 foreach $mapper (@{$encoder}) {
  6         14  
504 10 100       25 last unless defined $mapper;
505              
506 6 100       48 $join = defined $mapper->{'join'} ? $mapper->{'join'} :
    50          
507             defined $option{'join'} ? $option{'join'} : "";
508              
509 6 50       14 if ($mapper->{'no_code'}) {
510 6         21 $text = join $join, $mapper->process($text), $mapper->recover();
511             }
512             else {
513 0 0       0 $text = join $join, map {
514 0         0 UNIVERSAL::isa($_, 'CODE') ? $_->() : $_
515             } $mapper->process($text), $mapper->recover();
516             }
517             }
518              
519 6         27 return $text;
520             }
521              
522              
523             sub decode ($$$;$) {
524 3     3 1 8 my ($cls, $text, $decoder, $enc) = @_;
525 3         5 my ($mapper, $join);
526              
527 3 50       12 local %option = exists $options{caller 0} ? %{$options{caller 0}} : ();
  3         14  
528             # options be local due to tradition ^^
529              
530 3         15 require Encode;
531              
532 3 50       10 $enc = 'utf8' unless $enc;
533              
534 3 50       12 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     81 if (not UNIVERSAL::isa($decoder, 'ARRAY') or grep { defined $_ and not $_->isa($cls) } @{$decoder}) {
  15 50       60  
  3         6  
540 0         0 carp "Expecting a reference to an array of $cls objects";
541 0         0 return undef;
542             }
543              
544 3         5 foreach $mapper (@{$decoder}) {
  3         83  
545 9 100       21 last unless defined $mapper;
546              
547 7 100       26 $join = defined $mapper->{'join'} ? $mapper->{'join'} :
    50          
548             defined $option{'join'} ? $option{'join'} : "";
549              
550 7 50       16 if ($mapper->{'no_code'}) {
551 7         15 $text = join $join, $mapper->process($text), $mapper->recover();
552             }
553             else {
554 0 0       0 $text = join $join, map {
555 0         0 UNIVERSAL::isa($_, 'CODE') ? $_->() : $_
556             } $mapper->process($text), $mapper->recover();
557             }
558             }
559              
560 3 50       26 return Encode::is_utf8($text) ? $text : Encode::decode($enc, $text);
561             }
562              
563              
564             1;
565              
566             __END__