File Coverage

blib/lib/Convert/Moji.pm
Criterion Covered Total %
statement 149 164 90.8
branch 43 58 74.1
condition 2 6 33.3
subroutine 17 17 100.0
pod 8 12 66.6
total 219 257 85.2


line stmt bran cond sub pod time code
1             package Convert::Moji;
2              
3 2     2   75255 use warnings;
  2         11  
  2         108  
4 2     2   12 use strict;
  2         3  
  2         122  
5              
6             require Exporter;
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw/make_regex length_one unambiguous/;
9              
10 2     2   23 use Carp;
  2         5  
  2         3925  
11              
12             our $VERSION = '0.11';
13              
14             # Load a converter from a file and return a hash reference containing
15             # the left/right pairs.
16              
17             sub load_convertor
18             {
19 1     1 0 3 my ($file) = @_;
20 1         1 my $file_in;
21 1 50   1   9 if (! open $file_in, "<:encoding(utf8)", $file) {
  1         2  
  1         7  
  1         36  
22 0         0 carp "Could not open '$file' for reading: $!";
23 0         0 return;
24             }
25 1         12829 my %converter;
26 1         29 while (my $line = <$file_in>) {
27 2         18 chomp $line;
28 2         14 my ($left, $right) = split /\s+/, $line;
29 2         17 $converter{$left} = $right;
30             }
31 1 50       19 close $file_in or croak "Could not close '$file': $!";
32 1         7 return \%converter;
33             }
34              
35             sub length_one
36             {
37 10     10 1 19 for (@_) {
38 73 100       155 return if !/^.$/;
39             }
40 9         15 return 1;
41             }
42              
43             sub make_regex
44             {
45 10     10 1 108 my @inputs = @_;
46             # Quote any special characters. We could also do this with join
47             # '\E|\Q', but the regexes then become even longer.
48 10         19 @inputs = map {quotemeta} @inputs;
  74         112  
49 10 100       46 if (length_one (@inputs)) {
50 9         47 return '(['.(join '', @inputs).'])';
51             }
52             else {
53             # Sorting is essential, otherwise shorter characters match before
54             # longer ones, causing errors if the shorter character is part of
55             # a longer one.
56 1         6 return '('.join ('|',sort { length($b) <=> length($a) } @inputs).')';
  1         9  
57             }
58             }
59              
60             sub unambiguous
61             {
62 4     4 1 6 my ($table) = @_;
63 4         4 my %inverted;
64 4         12 for (keys %$table) {
65 33         37 my $v = $$table{$_};
66 33 100       49 return if $inverted{$v};
67 32         71 $inverted{$v} = $_;
68             }
69             # Is not ambiguous
70 3         11 return 1;
71             }
72              
73             # If the table is unambiguous, we can use Perl's built-in "reverse"
74             # function. However, if the table is ambiguous, "reverse" will lose
75             # information. The method applied here is to make a hash with the
76             # values of $table as keys and the values are array references.
77              
78             sub ambiguous_reverse
79             {
80 1     1 0 2 my ($table) = @_;
81 1         2 my %inverted;
82 1         3 for (keys %$table) {
83 8         13 my $val = $table->{$_};
84 8         8 push @{$inverted{$val}}, $_;
  8         24  
85             }
86 1         5 for (keys %inverted) {
87 4         5 @{$inverted{$_}} = sort @{$inverted{$_}};
  4         8  
  4         9  
88             }
89 1         3 return \%inverted;
90             }
91              
92             # Callback
93              
94             sub split_match
95             {
96 1     1 0 3 my ($erter, $input, $convert_type) = @_;
97 1 50       3 if (! $convert_type) {
98 0         0 $convert_type = 'first';
99             }
100 1         2 my $lhs = $erter->{rhs};
101 1         2 my $rhs = $erter->{out2in};
102 1 50 33     15 if (!$convert_type || $convert_type eq 'first') {
    50 33        
    50          
103 0         0 $input =~ s/$lhs/$$rhs{$1}->[0]/eg;
  0         0  
104 0         0 return $input;
105             }
106             elsif ($convert_type eq 'random') {
107 0         0 my $size = @$rhs;
108 0         0 $input =~ s/$lhs/$$rhs{$1}->[int rand $size]/eg;
  0         0  
109 0         0 return $input;
110             }
111             elsif ($convert_type eq 'all' || $convert_type eq 'all_joined') {
112 1         21 my @output = grep {length($_) > 0} (split /$lhs/, $input);
  8         16  
113 1         3 for my $o (@output) {
114 6 100       22 if ($o =~ /$lhs/) {
115 4         8 $o = $$rhs{$1};
116             }
117             }
118 1 50       16 if ($convert_type eq 'all') {
119 0         0 return \@output;
120             }
121             else {
122 1 100       4 return join ('',map {ref($_) eq 'ARRAY' ? "[@$_]" : $_} @output);
  6         32  
123             }
124             }
125             else {
126 0         0 carp "Unknown convert_type $convert_type";
127             }
128             }
129              
130             # Attach a table to a Convert::Moji object.
131              
132             sub table
133             {
134 5     5 1 9 my ($table, $noinvert) = @_;
135 5         7 my $erter = {};
136 5         13 $erter->{type} = "table";
137 5         7 $erter->{in2out} = $table;
138 5         21 my @keys = keys %$table;
139 5         18 my @values = values %$table;
140 5         15 $erter->{lhs} = make_regex @keys;
141 5 100       14 if (!$noinvert) {
142 4         7 $erter->{unambiguous} = unambiguous($table);
143 4 100       8 if ($erter->{unambiguous}) {
144 3         4 my %out2in_table = reverse %{$table};
  3         15  
145 3         8 $erter->{out2in} = \%out2in_table;
146             }
147             else {
148 1         4 $erter->{out2in} = ambiguous_reverse ($table);
149 1         2 @values = keys %{$erter->{out2in}};
  1         4  
150             }
151 4         9 $erter->{rhs} = make_regex @values;
152             }
153 5         14 return $erter;
154             }
155              
156             # Make a converter from a tr instruction.
157              
158             sub tr_erter
159             {
160 1     1 0 3 my ($lhs, $rhs) = @_;
161 1         2 my $erter = {};
162 1         2 $erter->{type} = "tr";
163 1         3 $erter->{lhs} = $lhs;
164 1         2 $erter->{rhs} = $rhs;
165 1         3 return $erter;
166             }
167              
168             # Add a code-based converter
169              
170             sub code
171             {
172 1     1 1 4 my ($convert, $invert) = @_;
173 1         2 my $erter = {};
174 1         2 $erter->{type} = "code";
175 1         3 $erter->{convert} = $convert;
176 1         2 $erter->{invert} = $invert;
177 1         2 return $erter;
178             }
179              
180             sub new
181             {
182 6     6 1 866 my ($package, @conversions) = @_;
183 6         14 my $conv = {};
184 6         9 bless $conv;
185 6         17 $conv->{erter} = [];
186 6         10 $conv->{erters} = 0;
187 6         13 for my $c (@conversions) {
188 7         11 my $noinvert;
189             my $erter;
190 7 100       19 if ($c->[0] eq "oneway") {
191 1         5 shift @$c;
192 1         2 $noinvert = 1;
193             }
194 7 100       26 if ($c->[0] eq "table") {
    100          
    100          
    50          
195 4         10 $erter = table ($c->[1], $noinvert);
196             }
197             elsif ($c->[0] eq "file") {
198 1         4 my $file = $c->[1];
199 1         4 my $table = Convert::Moji::load_convertor ($file);
200 1 50       5 return if !$table;
201 1         4 $erter = table ($table, $noinvert);
202             }
203             elsif ($c->[0] eq 'tr') {
204 1         3 $erter = tr_erter ($c->[1], $c->[2]);
205             }
206             elsif ($c->[0] eq 'code') {
207 1         4 $erter = code ($c->[1], $c->[2]);
208 1 50       3 if (!$c->[2]) {
209 1         2 $noinvert = 1;
210             }
211             }
212 7         19 my $o = $conv->{erters};
213 7         11 $conv->{erter}->[$o] = $erter;
214 7         23 $conv->{noinvert}->[$o] = $noinvert;
215 7         15 $conv->{erters}++;
216             }
217 6         16 return $conv;
218             }
219              
220             sub convert
221             {
222 6     6 1 1442 my ($conv, $input) = @_;
223 6         19 for (my $i = 0; $i < $conv->{erters}; $i++) {
224 7         12 my $erter = $conv->{erter}->[$i];
225 7 100       25 if ($erter->{type} eq "table") {
    100          
    50          
226 5         8 my $lhs = $erter->{lhs};
227 5         6 my $rhs = $erter->{in2out};
228 5         124 $input =~ s/$lhs/$$rhs{$1}/g;
229             }
230             elsif ($erter->{type} eq 'tr') {
231 1         3 my $lhs = $erter->{lhs};
232 1         2 my $rhs = $erter->{rhs};
233 1     1   9 eval ("\$input =~ tr/$lhs/$rhs/");
  1         2  
  1         14  
  1         131  
234             }
235             elsif ($erter->{type} eq 'code') {
236 1         2 $_ = $input;
237 1         2 $input = &{$erter->{convert}};
  1         3  
238             }
239             }
240 6         63 return $input;
241             }
242              
243             sub invert
244             {
245 4     4 1 14 my ($conv, $input, $convert_type) = @_;
246 4         16 for (my $i = $conv->{erters} - 1; $i >= 0; $i--) {
247 5 50       14 next if $conv->{noinvert}->[$i];
248 5         10 my $erter = $conv->{erter}->[$i];
249 5 100       14 if ($erter->{type} eq "table") {
    50          
    0          
250 4 100       6 if ($erter->{unambiguous}) {
251 3         4 my $lhs = $erter->{rhs};
252 3         5 my $rhs = $erter->{out2in};
253 3         70 $input =~ s/$lhs/$$rhs{$1}/g;
254             }
255             else {
256 1         4 $input = split_match ($erter, $input, $convert_type);
257             }
258             }
259             elsif ($erter->{type} eq 'tr') {
260 1         3 my $lhs = $erter->{rhs};
261 1         3 my $rhs = $erter->{lhs};
262 1         48 eval ("\$input =~ tr/$lhs/$rhs/");
263             }
264             elsif ($erter->{type} eq 'code') {
265 0         0 $_ = $input;
266 0         0 $input = &{$erter->{invert}};
  0         0  
267             }
268             }
269 4         26 return $input;
270             }
271              
272             1;
273              
274              
275