File Coverage

blib/lib/Encode/Mapper.pm
Criterion Covered Total %
statement 174 328 53.0
branch 85 186 45.7
condition 16 31 51.6
subroutine 13 18 72.2
pod 9 11 81.8
total 297 574 51.7


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