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   14618 use warnings;
  2         2  
  2         53  
4 2     2   7 use strict;
  2         2  
  2         95  
5              
6             require Exporter;
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw/make_regex length_one unambiguous/;
9              
10 2     2   7 use Carp;
  2         5  
  2         2563  
11              
12             our $VERSION = '0.09';
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 1 my ($file) = @_;
20 1         1 my $file_in;
21 1 50   1   5 if (! open $file_in, "<:encoding(utf8)", $file) {
  1         1  
  1         5  
  1         39  
22 0         0 carp "Could not open '$file' for reading: $!";
23 0         0 return;
24             }
25 1         8258 my %converter;
26 1         16 while (my $line = <$file_in>) {
27 2         13 chomp $line;
28 2         9 my ($left, $right) = split /\s+/, $line;
29 2         11 $converter{$left} = $right;
30             }
31 1 50       10 close $file_in or croak "Could not close '$file': $!";
32 1         4 return \%converter;
33             }
34              
35             sub length_one
36             {
37 10     10 1 14 for (@_) {
38 73 100       124 return if !/^.$/;
39             }
40 9         11 return 1;
41             }
42              
43             sub make_regex
44             {
45 10     10 1 33 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         14 @inputs = map {quotemeta} @inputs;
  74         150  
49 10 100       21 if (length_one (@inputs)) {
50 9         35 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         4 return '('.join ('|',sort { length($b) <=> length($a) } @inputs).')';
  1         31  
57             }
58             }
59              
60             sub unambiguous
61             {
62 4     4 1 4 my ($table) = @_;
63 4         5 my %inverted;
64 4         8 for (keys %$table) {
65 33         23 my $v = $$table{$_};
66 33 100       53 return if $inverted{$v};
67 32         38 $inverted{$v} = $_;
68             }
69             # Is not ambiguous
70 3         9 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         1 my %inverted;
82 1         2 for (keys %$table) {
83 8         3 my $val = $table->{$_};
84 8         6 push @{$inverted{$val}}, $_;
  8         18  
85             }
86 1         3 for (keys %inverted) {
87 4         3 @{$inverted{$_}} = sort @{$inverted{$_}};
  4         6  
  4         7  
88             }
89 1         1 return \%inverted;
90             }
91              
92             # Callback
93              
94             sub split_match
95             {
96 1     1 0 2 my ($erter, $input, $convert_type) = @_;
97 1 50       4 if (! $convert_type) {
98 0         0 $convert_type = 'first';
99             }
100 1         2 my $lhs = $erter->{rhs};
101 1         8 my $rhs = $erter->{out2in};
102 1 50 33     13 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         14 my @output = grep {length($_) > 0} (split /$lhs/, $input);
  8         10  
113 1         3 for my $o (@output) {
114 6 100       17 if ($o =~ /$lhs/) {
115 4         6 $o = $$rhs{$1};
116             }
117             }
118 1 50       3 if ($convert_type eq 'all') {
119 0         0 return \@output;
120             }
121             else {
122 1 100       1 return join ('',map {ref($_) eq 'ARRAY' ? "[@$_]" : $_} @output);
  6         16  
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 4 my ($table, $noinvert) = @_;
135 5         5 my $erter = {};
136 5         8 $erter->{type} = "table";
137 5         6 $erter->{in2out} = $table;
138 5         17 my @keys = keys %$table;
139 5         16 my @values = values %$table;
140 5         12 $erter->{lhs} = make_regex @keys;
141 5 100       8 if (!$noinvert) {
142 4         6 $erter->{unambiguous} = unambiguous($table);
143 4 100       5 if ($erter->{unambiguous}) {
144 3         4 my %out2in_table = reverse %{$table};
  3         13  
145 3         6 $erter->{out2in} = \%out2in_table;
146             }
147             else {
148 1         3 $erter->{out2in} = ambiguous_reverse ($table);
149 1         1 @values = keys %{$erter->{out2in}};
  1         4  
150             }
151 4         6 $erter->{rhs} = make_regex @values;
152             }
153 5         10 return $erter;
154             }
155              
156             # Make a converter from a tr instruction.
157              
158             sub tr_erter
159             {
160 1     1 0 2 my ($lhs, $rhs) = @_;
161 1         1 my $erter = {};
162 1         2 $erter->{type} = "tr";
163 1         1 $erter->{lhs} = $lhs;
164 1         1 $erter->{rhs} = $rhs;
165 1         2 return $erter;
166             }
167              
168             # Add a code-based converter
169              
170             sub code
171             {
172 1     1 1 3 my ($convert, $invert) = @_;
173 1         1 my $erter = {};
174 1         2 $erter->{type} = "code";
175 1         1 $erter->{convert} = $convert;
176 1         1 $erter->{invert} = $invert;
177 1         2 return $erter;
178             }
179              
180             sub new
181             {
182 6     6 1 246 my ($package, @conversions) = @_;
183 6         7 my $conv = {};
184 6         7 bless $conv;
185 6         24 $conv->{erter} = [];
186 6         9 $conv->{erters} = 0;
187 6         7 for my $c (@conversions) {
188 7         6 my $noinvert;
189             my $erter;
190 7 100       14 if ($c->[0] eq "oneway") {
191 1         2 shift @$c;
192 1         1 $noinvert = 1;
193             }
194 7 100       17 if ($c->[0] eq "table") {
    100          
    100          
    50          
195 4         8 $erter = table ($c->[1], $noinvert);
196             }
197             elsif ($c->[0] eq "file") {
198 1         1 my $file = $c->[1];
199 1         3 my $table = Convert::Moji::load_convertor ($file);
200 1 50       3 return if !$table;
201 1         5 $erter = table ($table, $noinvert);
202             }
203             elsif ($c->[0] eq 'tr') {
204 1         4 $erter = tr_erter ($c->[1], $c->[2]);
205             }
206             elsif ($c->[0] eq 'code') {
207 1         3 $erter = code ($c->[1], $c->[2]);
208 1 50       2 if (!$c->[2]) {
209 1         2 $noinvert = 1;
210             }
211             }
212 7         12 my $o = $conv->{erters};
213 7         7 $conv->{erter}->[$o] = $erter;
214 7         8 $conv->{noinvert}->[$o] = $noinvert;
215 7         8 $conv->{erters}++;
216             }
217 6         10 return $conv;
218             }
219              
220             sub convert
221             {
222 6     6 1 881 my ($conv, $input) = @_;
223 6         16 for (my $i = 0; $i < $conv->{erters}; $i++) {
224 7         7 my $erter = $conv->{erter}->[$i];
225 7 100       18 if ($erter->{type} eq "table") {
    100          
    50          
226 5         7 my $lhs = $erter->{lhs};
227 5         5 my $rhs = $erter->{in2out};
228 5         118 $input =~ s/$lhs/$$rhs{$1}/g;
229             }
230             elsif ($erter->{type} eq 'tr') {
231 1         2 my $lhs = $erter->{lhs};
232 1         1 my $rhs = $erter->{rhs};
233 1     1   6 eval ("\$input =~ tr/$lhs/$rhs/");
  1         1  
  1         10  
  1         124  
234             }
235             elsif ($erter->{type} eq 'code') {
236 1         1 $_ = $input;
237 1         2 $input = &{$erter->{convert}};
  1         3  
238             }
239             }
240 6         38 return $input;
241             }
242              
243             sub invert
244             {
245 4     4 1 8 my ($conv, $input, $convert_type) = @_;
246 4         14 for (my $i = $conv->{erters} - 1; $i >= 0; $i--) {
247 5 50       25 next if $conv->{noinvert}->[$i];
248 5         5 my $erter = $conv->{erter}->[$i];
249 5 100       12 if ($erter->{type} eq "table") {
    50          
    0          
250 4 100       7 if ($erter->{unambiguous}) {
251 3         2 my $lhs = $erter->{rhs};
252 3         2 my $rhs = $erter->{out2in};
253 3         51 $input =~ s/$lhs/$$rhs{$1}/g;
254             }
255             else {
256 1         2 $input = split_match ($erter, $input, $convert_type);
257             }
258             }
259             elsif ($erter->{type} eq 'tr') {
260 1         1 my $lhs = $erter->{rhs};
261 1         2 my $rhs = $erter->{lhs};
262 1         33 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         13 return $input;
270             }
271              
272             1;
273              
274              
275