File Coverage

blib/lib/Lingua/JA/Moji.pm
Criterion Covered Total %
statement 575 656 87.6
branch 170 224 75.8
condition 14 21 66.6
subroutine 92 97 94.8
pod 57 89 64.0
total 908 1087 83.5


line stmt bran cond sub pod time code
1             package Lingua::JA::Moji;
2              
3 25     25   2379862 use warnings;
  25         46  
  25         1405  
4 25     25   228 use strict;
  25         42  
  25         609  
5 25     25   10636 use utf8;
  25         5764  
  25         150  
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9              
10             our $VERSION = '0.61';
11              
12 25     25   1803 use Carp 'croak';
  25         54  
  25         1305  
13 25     25   9690 use Convert::Moji qw/make_regex length_one unambiguous/;
  25         47496  
  25         1886  
14 25     25   9771 use JSON::Parse 'read_json';
  25         33633  
  25         132500  
15              
16             our @EXPORT_OK = qw/
17             InHankakuKatakana
18             InKana
19             InWideAscii
20             ascii2wide
21             bad_kanji
22             bracketed2kanji
23             braille2kana
24             circled2kana
25             circled2kanji
26             cleanup_kana
27             cyrillic2katakana
28             hangul2kana
29             hentai2kana
30             hentai2kanji
31             hira2kata
32             hw2katakana
33             is_hiragana
34             is_kana
35             is_katakana
36             is_romaji
37             is_romaji_semistrict
38             is_romaji_strict
39             is_small
40             is_voiced
41             join_sound_marks
42             kana2braille
43             kana2circled
44             kana2cyrillic
45             kana2hangul
46             kana2hentai
47             kana2hw
48             kana2katakana
49             kana2morse
50             kana2romaji
51             kana_consonant
52             kana_to_large
53             kanji2bracketed
54             kanji2circled
55             kanji2hentai
56             kata2hira
57             katakana2hw
58             katakana2square
59             katakana2syllable
60             morse2kana
61             new2old_kanji
62             nigori_first
63             normalize_romaji
64             old2new_kanji
65             romaji2hiragana
66             romaji2kana
67             romaji_styles
68             romaji_vowel_styles
69             smallize_kana
70             split_sound_marks
71             strip_sound_marks
72             square2katakana
73             wide2ascii
74             yurei_moji
75             /;
76              
77             our %EXPORT_TAGS = (
78             'all' => \@EXPORT_OK,
79             );
80              
81             # Load a specified convertor from the shared directory.
82              
83             sub load_convertor
84             {
85 21     21 0 151994 my ($in, $out) = @_;
86 21         58 my $filename = $in."2".$out;
87 21         66 my $file = getdistfile ($filename);
88 21 100 66     836 if (! $file || ! -f $file) {
89 1         326 croak "Could not find distribution file '$filename'";
90             }
91 20         97 my $convertor = Convert::Moji::load_convertor ($file);
92 20         16354 return $convertor;
93             }
94              
95             sub add_boilerplate
96             {
97 16     16 0 37 my ($code, $name) = @_;
98 16         33 $code =<
99             sub convert_$name
100             {
101             my (\$conv,\$input,\$convert_type) = \@_;
102             $code
103             return \$input;
104             }
105             EOSUB
106 16         43 $code .= "\\\&".__PACKAGE__."::convert_$name;";
107 16         34 return $code;
108             }
109              
110             sub ambiguous_reverse
111             {
112 0     0 0 0 my ($table) = @_;
113 0         0 my %inverted;
114 0         0 for (keys %$table) {
115 0         0 my $val = $table->{$_};
116 0         0 push @{$inverted{$val}}, $_;
  0         0  
117             }
118 0         0 return \%inverted;
119             }
120              
121             # Callback
122              
123             sub split_match
124             {
125 0     0 0 0 my ($conv, $input, $convert_type) = @_;
126 0 0       0 if (!$convert_type) {
127 0         0 $convert_type = "all";
128             }
129 0         0 my @input = split '', $input;
130 0         0 my @output;
131 0         0 for (@input) {
132 0         0 my $in = $conv->{out2in}->{$_};
133             # No conversion defined.
134 0 0       0 if (! $in) {
135 0         0 push @output, $_;
136 0         0 next;
137             }
138             # Unambigous case
139 0 0       0 if (@{$in} == 1) {
  0         0  
140 0         0 push @output, $in->[0];
141 0         0 next;
142             }
143 0 0       0 if ($convert_type eq 'all') {
    0          
    0          
144 0         0 push @output, $in;
145             }
146             elsif ($convert_type eq 'first') {
147 0         0 push @output, $in->[0];
148             }
149             elsif ($convert_type eq 'random') {
150 0         0 my $pos = int rand @$in;
151 0         0 push @output, $in->[$pos];
152             }
153             }
154 0         0 return \@output;
155             }
156              
157             sub make_convertors
158             {
159 8     8 0 21 my ($in, $out, $table) = @_;
160 8         46 my $conv = {};
161 8 50       26 if (!$table) {
162 8         22 $table = load_convertor ($in, $out);
163             }
164 8         23 $conv->{in2out} = $table;
165 8         13 my @keys = keys %{$table};
  8         107  
166 8         15 my @values = values %{$table};
  8         142  
167 8         16 my $sub_in2out;
168             my $sub_out2in;
169 8 50       54 if (length_one (@keys)) {
170 8         787 my $lhs = join '', @keys;
171              
172             # Improvement: one way tr/// for the ambiguous case lhs/rhs only.
173              
174 8 100 66     18 if (length_one (@values) && unambiguous ($table)) {
175             # can use tr///;
176 2         566 my $rhs = join '', @values;
177 2         5 $sub_in2out = "\$input =~ tr/$lhs/$rhs/;";
178 2         4 $sub_out2in = "\$input =~ tr/$rhs/$lhs/;";
179             }
180             else {
181 6         61 $sub_in2out = "\$input =~ s/([$lhs])/\$conv->{in2out}->{\$1}/eg;";
182 6         23 my $rhs = make_regex (@values);
183 6 50       2543 if (unambiguous($conv->{in2out})) {
184 6         1703 my %out2in_table = reverse %{$conv->{in2out}};
  6         155  
185 6         20 $conv->{out2in} = \%out2in_table;
186 6         19 $sub_out2in = "\$input =~ s/($rhs)/\$conv->{out2in}->{\$1}/eg;";
187             }
188             else {
189 0         0 $conv->{out2in} = ambiguous_reverse ($conv->{in2out});
190 0         0 $sub_out2in = "\$input = \$conv->split_match (\$input, \$convert_type);";
191             }
192             }
193             }
194             else {
195 0         0 my $lhs = make_regex (@keys);
196 0         0 $sub_in2out = "\$input =~ s/($lhs)/\$conv->{in2out}->{\$1}/eg;";
197 0         0 my $rhs = make_regex (@values);
198 0 0       0 if (unambiguous($conv->{in2out})) {
199 0         0 my %out2in_table = reverse %{$conv->{in2out}};
  0         0  
200 0         0 $conv->{out2in} = \%out2in_table;
201 0         0 $sub_out2in = " \$input =~ s/($rhs)/\$conv->{out2in}->{\$1}/eg;";
202             }
203             }
204 8         28 $sub_in2out = add_boilerplate ($sub_in2out, "${in}2$out");
205 8     1 0 1509 my $sub1 = eval $sub_in2out;
  1     1 0 2  
  1     1 0 3  
  1     1 0 3  
  1         2  
  1         4  
  1         3  
  1         2  
  1         5  
  3         8  
  1         3  
  1         3  
  1         3  
  7         17  
  1         3  
206 8         21 $conv->{in2out_sub} = $sub1;
207 8 50       29 if ($sub_out2in) {
208 8         41 $sub_out2in = add_boilerplate ($sub_out2in, "${out}2$in");
209 8     1 0 2671 my $sub2 = eval $sub_out2in;
  1     1 0 3  
  1     8 0 3  
  1     7 0 3  
  1         2  
  1         17  
  1         3  
  8         16  
  8         50  
  33         104  
  8         35  
  7         10  
  7         20  
  7         15  
  7         16  
210 8 50       31 if ($@) {
211 0         0 print "Errors are ",$@,"\n";
212 0         0 print "\$sub2 = ",$sub2,"\n";
213             }
214 8         21 $conv->{out2in_sub} = $sub2;
215             }
216 8         14 bless $conv;
217 8         78 return $conv;
218             }
219              
220             sub convert
221             {
222 4     4 0 8 my ($conv, $input) = @_;
223 4         5 return &{$conv->{in2out_sub}}($conv, $input);
  4         92  
224             }
225              
226             sub invert
227             {
228 17     17 0 35 my ($conv, $input, $convert_type) = @_;
229 17         24 return &{$conv->{out2in_sub}}($conv, $input, $convert_type);
  17         338  
230             }
231              
232              
233             # Kana ordered by consonant. Adds two bogus gyous, a "q" gyou for
234             # small vowels and an "xy" gyou for youon (ya, yu, yo), to the usual
235             # ones.
236              
237             my @gyou = (
238             a => [qw/ア イ ウ エ オ/],
239             # Not a real gyou.
240             q => [qw/ァ ィ ゥ ェ ォ/],
241             k => [qw/カ キ ク ケ コ/],
242             g => [qw/ガ ギ グ ゲ ゴ/],
243             s => [qw/サ シ ス セ ソ/],
244             z => [qw/ザ ジ ズ ゼ ゾ/],
245             t => [qw/タ チ ツ テ ト/],
246             d => [qw/ダ ヂ ヅ デ ド/],
247             n => [qw/ナ ニ ヌ ネ ノ/],
248             h => [qw/ハ ヒ フ ヘ ホ/],
249             b => [qw/バ ビ ブ ベ ボ/],
250             p => [qw/パ ピ プ ペ ポ/],
251             m => [qw/マ ミ ム メ モ/],
252             y => [qw/ヤ ユ ヨ/],
253             xy => [qw/ャ ュ ョ/],
254             r => [qw/ラ リ ル レ ロ/],
255             w => [qw/ワ ヰ ヱ ヲ/],
256             v => [qw/ヴ/],
257             );
258              
259             my %gyou = @gyou;
260              
261             # Kana => consonant mapping.
262              
263             my %siin;
264              
265             for my $consonant (keys %gyou) {
266             for my $kana (@{$gyou{$consonant}}) {
267             if ($consonant eq 'a') {
268             $siin{$kana} = '';
269             }
270             else {
271             $siin{$kana} = $consonant;
272             }
273             }
274             }
275              
276             # Vowel => kana mapping.
277              
278             my %dan = (a => [qw/ア カ ガ サ ザ タ ダ ナ ハ バ パ マ ヤ ラ ワ ャ ァ/],
279             i => [qw/イ キ ギ シ ジ チ ヂ ニ ヒ ビ ピ ミ リ ヰ ィ/],
280             u => [qw/ウ ク グ ス ズ ツ ヅ ヌ フ ブ プ ム ユ ル ュ ゥ ヴ/],
281             e => [qw/エ ケ ゲ セ ゼ テ デ ネ ヘ ベ ペ メ レ ヱ ェ/],
282             o => [qw/オ コ ゴ ソ ゾ ト ド ノ ホ ボ ポ モ ヨ ロ ヲ ョ ォ/]);
283              
284             # Kana => vowel mapping
285              
286             my %boin;
287              
288             # List of kana with a certain vowel.
289              
290             my %vowelclass;
291              
292             for my $vowel (keys %dan) {
293             my @kana_list = @{$dan{$vowel}};
294             for my $kana (@kana_list) {
295             $boin{$kana} = $vowel;
296             }
297             $vowelclass{$vowel} = join '', @kana_list;
298             }
299              
300             # Kana gyou which can be preceded by a sokuon (small tsu).
301              
302             # Added d to the list for ウッド
303             # Added z for "badge" etc.
304             # Added g for ドッグ etc.
305              
306             #my @takes_sokuon_gyou = qw/s t k p d z g/;
307             #my @takes_sokuon = (map {@{$gyou{$_}}} @takes_sokuon_gyou);
308             #my $takes_sokuon = join '', @takes_sokuon;
309             #die @takes_sokuon;
310             my $takes_sokuon = 'サシスセソタチツテトカキクケコパピプペポダヂヅデドザジズゼゾガギグゲゴ';
311              
312             # Any kana except ん
313              
314             #my@b4s;push@b4s,@{$gyou{$_}}for sort keys%gyou;@b4s=grep!/ん/,@b4s;die join'',@b4s;
315              
316             my $before_sokuon = 'ヤユヨナニヌネノャュョガギグゲゴダヂヅデドカキクケコヴラリルレロワヰヱヲバビブベボタチツテトアイウエオパピプペポサシスセソァィゥェォマミムメモハヒフヘホザジズゼゾ';
317              
318             # N
319              
320             # Kana gyou which need an apostrophe when preceded by an "n" kana.
321              
322             my $need_apostrophe = join '', (map {@{$gyou{$_}}} qw/a y/);
323              
324             # Gyou which turn an "n" into an "m" in some kinds of romanization
325              
326             my $need_m = join '', (map {@{$gyou{$_}}} qw/p b m/);
327              
328             # YOUON
329              
330             # Small ya, yu, yo.
331              
332             my $youon = join '', (@{$gyou{xy}});
333             my %youon = qw/a ャ u ュ o ョ ou ョ/;
334              
335             # HEPBURN
336              
337             # Hepburn irregular romanization
338              
339             my %hepburn = qw/シ sh ツ ts チ ch ジ j ヅ z ヂ j フ f/;
340              
341             # Hepburn map from vowel to list of kana with that vowel.
342              
343             my %hep_vowel = (i => 'シチジヂ', u => 'ヅツフ');
344             my $hep_list = join '', keys %hepburn;
345              
346             # Hepburn irregular romanization of ッチ as "tch".
347              
348             my %hepburn_sokuon = qw/チ t/;
349             my $hep_sok_list = join '', keys %hepburn_sokuon;
350              
351             # Hepburn variants for the youon case.
352              
353             my %hepburn_youon = qw/シ sh チ ch ジ j ヂ j/;
354             my $is_hepburn_youon = join '', keys %hepburn_youon;
355              
356             # Kunrei variants for the youon case.
357              
358             my %kunrei_youon = qw/ヂ z/;
359             my $is_kunrei_youon = join '', keys %kunrei_youon;
360              
361             # Kunrei romanization
362              
363             my %kunrei = qw/ヅ z ヂ z/;
364              
365             my $kun_list = join '', keys %kunrei;
366              
367             # LONG VOWELS
368              
369             # Long vowels, another bugbear of Japanese romanization.
370              
371             my @aiueo = qw/a i u e o ou/;
372              
373             # Various ways to display the long vowels.
374              
375             my %chouonhyouki;
376             @{$chouonhyouki{circumflex}}{@aiueo} = qw/â î û ê ô ô/;
377             @{$chouonhyouki{macron}}{@aiueo} = qw/ā ii ū ē ō ō/;
378             @{$chouonhyouki{wapuro}}{@aiueo} = qw/aa ii uu ee oo ou/;
379             @{$chouonhyouki{passport}}{@aiueo} = qw/a i u e oh oh/;
380             @{$chouonhyouki{none}}{@aiueo} = qw/a ii u e o o/;
381              
382             my $vowel_re = qr/[aeiouâêîôûāēōū]/i;
383             my $no_u_vowel_re = qr/[aeioâêîôāēō]/i;
384             my $u_re = qr/[uūû]/i;
385              
386             sub kana2romaji
387             {
388 190     190 1 248447 my ($input, $options) = @_;
389 190         387 $input = kana2katakana ($input);
390 190 100       370 if (! $options) {
391 10         14 $options = {};
392             }
393             # Parse the options
394 190         471 my $kunrei;
395             my $hepburn;
396 190         0 my $passport;
397 190         0 my $common;
398 190 100       347 if ($options->{style}) {
399 29         62 my $style = $options->{style};
400 29 100       63 if ($style eq 'kunrei') {
401 2         5 $kunrei = 1;
402             }
403 29 50       76 if ($style eq 'passport') {
404 0         0 $passport = 1;
405             }
406 29 100       43 if ($style eq 'hepburn') {
407 18         19 $hepburn = 1;
408             }
409 29 100       44 if ($style eq 'common') {
410 8         9 $hepburn = 1;
411 8         7 $common = 1;
412             }
413 29 50 66     136 if (!$kunrei && !$passport && !$hepburn && $style ne "nihon" &&
      66        
      33        
414             $style ne 'nippon') {
415 0         0 croak "Unknown romanization style '$options->{style}'";
416             }
417             }
418 190         274 my $wapuro;
419 190 100       340 if ($options->{wapuro}) {
420 158         181 $wapuro = 1;
421             }
422 190         218 my $wikipedia;
423 190 100       347 if ($options->{wikipedia}) {
424 1         1 $wikipedia = 1;
425             }
426 190         199 my $truck;
427 190 100       289 if ($options->{truck}) {
428 1         1 $truck = 1;
429             }
430 190         245 my $use_m = 0;
431 190 100 66     584 if ($hepburn || $passport) {
432 26         24 $use_m = 1;
433             }
434 190 100       345 if (defined $options->{use_m}) {
435             $use_m = $options->{use_m}
436 2         2 }
437 190         239 my $ve_type = 'circumflex'; # type of vowel extension to use.
438 190 100       267 if ($hepburn) {
439 26         28 $ve_type = 'macron';
440             }
441 190 100       260 if ($wapuro) {
442 158         208 $ve_type = 'wapuro';
443             }
444 190 50       295 if ($passport) {
445 0         0 $hepburn = 1;
446 0         0 $ve_type = 'passport';
447 0         0 $use_m = 1;
448             }
449 190 100       287 if ($options->{ve_type}) {
450 10         15 $ve_type = $options->{ve_type};
451             }
452 190 50       375 if (! $chouonhyouki{$ve_type}) {
453 0         0 print STDERR "Warning: unrecognized long vowel type '$ve_type'\n";
454 0         0 $ve_type = 'circumflex';
455             }
456 190         219 my $wo;
457 190 100       281 if ($options->{wo}) {
458 1         1 $wo = 1;
459             }
460              
461             # Start of conversion
462              
463             # 撥音 (ん)
464 190         999 $input =~ s/ン(?=[$need_apostrophe])/n\'/g;
465 190 100       333 if ($use_m) {
466 24         133 $input =~ s/ン(?=[$need_m])/m/g;
467             }
468 190         317 $input =~ s/ン/n/g;
469             # 促音 (っ)
470 190 100       277 if ($hepburn) {
471 26         109 $input =~ s/ッ([$hep_sok_list])/$hepburn_sokuon{$1}$1/g;
472             }
473 190         720 $input =~ s/ッ([$takes_sokuon])/$siin{$1}$1/g;
474 190 100       366 if ($ve_type eq 'wapuro') {
475 167         239 $input =~ s/ー/-/g;
476             }
477 190 100       351 if ($ve_type eq 'none') {
478 1         4 $input =~ s/ー//g;
479             }
480             # Convert long vowels plus consonants into romaji
481 190         321 for my $vowel (@aiueo) {
482 1140         2165 my $ve = $chouonhyouki{$ve_type}->{$vowel};
483 1140         1409 my $vowelclass;
484             my $vowel_kana;
485 1140 100       1647 if ($vowel eq 'ou') {
486 190         234 $vowelclass = $vowelclass{o};
487 190         240 $vowel_kana = 'ウ';
488             }
489             else {
490 950         1176 $vowelclass = $vowelclass{$vowel};
491 950         1395 $vowel_kana = $dan{$vowel}->[0];
492             }
493             # 拗音 (きょ) + 長音 (ー)
494 1140         1393 my $y = $youon{$vowel};
495 1140 100       1760 if ($y) {
496 760 100       1075 if ($hepburn) {
497 104         2211 $input =~ s/([$is_hepburn_youon])${y}[ー$vowel_kana]/$hepburn_youon{$1}$ve/g;
498             }
499 760 100       1168 if ($kunrei) {
500 8         192 $input =~ s/([$is_kunrei_youon])${y}[ー$vowel_kana]/$kunrei_youon{$1}y$ve/g;
501             }
502 760         20019 $input =~ s/([$vowelclass{i}])${y}[ー$vowel_kana]/$siin{$1}y$ve/g;
503             }
504 1140 100 100     2272 if ($hepburn && $hep_vowel{$vowel}) {
505 52         823 $input =~ s/([$hep_vowel{$vowel}])[ー$vowel_kana]/$hepburn{$1}$ve/g;
506             }
507 1140         15652 $input =~ s/${vowel_kana}[ー$vowel_kana]/$ve/g;
508 1140         24900 $input =~ s/([$vowelclass])[ー$vowel_kana]/$siin{$1}$ve/g;
509             }
510             # 拗音 (きょ)
511 190 100       455 if ($hepburn) {
    100          
512 26         121 $input =~ s/([$is_hepburn_youon])([$youon])/$hepburn_youon{$1}$boin{$2}/g;
513             }
514             elsif ($kunrei) {
515 2         41 $input =~ s/([$is_kunrei_youon])([$youon])/$kunrei_youon{$1}y$boin{$2}/g;
516             }
517 190         886 $input =~ s/([$vowelclass{i}])([$youon])/$siin{$1}y$boin{$2}/g;
518             # その他
519 190 100       313 if ($wo) {
520 1         4 $input =~ s/ヲ/wo/g;
521 1         3 $input =~ s/([アイウエオ])/$boin{$1}/g;
522             }
523             else {
524 189         527 $input =~ s/([アイウエオヲ])/$boin{$1}/g;
525             }
526 190         778 $input =~ s/([ァィゥェォ])/q$boin{$1}/g;
527 190         336 $input =~ s/ヮ/xwa/g;
528 190 100       389 if ($hepburn) {
    100          
529 26         163 $input =~ s/([$hep_list])/$hepburn{$1}$boin{$1}/g;
530             }
531             elsif ($kunrei) {
532 2         32 $input =~ s/([$kun_list])/$kunrei{$1}$boin{$1}/g;
533             }
534 190         1031 $input =~ s/([カ-ヂツ-ヱヴ])/$siin{$1}$boin{$1}/g;
535 190         1253 $input =~ s/q($vowel_re)/x$1/g;
536 190 100       344 if ($common) {
537             # Convert kana + small vowel into thingumibob, if there is a
538             # consonant before.
539 8         301 $input =~ s/([^\Waiueo])$vowel_re[x]($vowel_re)/$1$2/;
540             # Convert u + small kana into w + vowel
541 8         137 $input =~ s/($vowel_re|\b)ux([iue])/$1w$2/i;
542             }
543 190 100       346 if ($wikipedia) {
544 1         4 $input =~ s/ii/ī/g;
545             }
546 190 100       277 if ($truck) {
547 1         45 $input =~ s/j($vowel_re)/jy$1/g;
548             }
549 190         962 return $input;
550             }
551              
552             sub romaji2hiragana
553             {
554 53     53 1 139229 my ($input, $options) = @_;
555 53 100       149 if (! $options) {
556 50         68 $options = {};
557             }
558 53         193 my $katakana = romaji2kana ($input, {wapuro => 1, %$options});
559 53         138 return kata2hira ($katakana);
560             }
561              
562             sub romaji_styles
563             {
564 1     1 1 4 my ($check) = @_;
565 1         11 my @styles = (
566             {
567             abbrev => "hepburn",
568             full_name => "Hepburn",
569             }, {
570             abbrev => 'nihon',
571             full_name => 'Nihon-shiki',
572             }, {
573             abbrev => 'kunrei',
574             full_name => 'Kunrei-shiki',
575             }, {
576             abbrev => 'common',
577             full_name => 'common',
578             });
579 1 50       34 if (! defined ($check)) {
580 0         0 return (@styles);
581             }
582             else {
583 1         5 for my $style (@styles) {
584 2 100       8 if ($check eq $style->{abbrev}) {
585 1         11 return 1;
586             }
587             }
588 0         0 return;
589             }
590             }
591              
592             my %styles = (
593             macron => 1,
594             circumflex => 1,
595             wapuro => 1,
596             passport => 1,
597             none => 1,
598             );
599              
600             # Check whether this vowel style is allowed.
601              
602             sub romaji_vowel_styles
603             {
604 1     1 1 3 my ($check) = @_;
605 1 50       4 if (! defined ($check)) {
606 1         5 return [keys %styles];
607             }
608             else {
609 0         0 return $styles{$check};
610             }
611             }
612              
613             my $romaji2katakana;
614             my $romaji_regex;
615              
616             my %longvowels;
617             @longvowels{qw/â î û ê ô/} = qw/aー iー uー eー oー/;
618             @longvowels{qw/ā ī ū ē ō/} = qw/aー iー uー eー oー/;
619             my $longvowels = join '|', sort {length($a)<=>length($b)} keys %longvowels;
620              
621             sub romaji2kana
622             {
623 363     363 1 288208 my ($input, $options) = @_;
624 363 100       726 if (! defined $romaji2katakana) {
625 8         52 $romaji2katakana = load_convertor ('romaji', 'katakana');
626 8         528 $romaji_regex = make_regex (keys %$romaji2katakana);
627             }
628             # Set to true if we want long o to be オウ rather than オー
629 363         11792 my $wapuro;
630             # Set to true if we want gumma to be ぐっま and onnna to be おんな.
631             my $ime;
632 363 100       811 if ($options) {
633 269         458 $wapuro = $options->{wapuro};
634 269         400 $ime = $options->{ime};
635             }
636              
637 363 50       604 if (! defined $input) {
638 0         0 return;
639             }
640 363         700 $input = lc $input;
641             # Deal with long vowels
642 363 100       622 if ($wapuro) {
643 269         535 $input =~ s/[âā]/aa/g;
644 269         370 $input =~ s/[îī]/ii/g;
645 269         341 $input =~ s/[êē]/ee/g;
646 269         361 $input =~ s/[ûū]/uu/g;
647 269         362 $input =~ s/[ôō]/ou/g;
648             }
649             else {
650 94         633 $input =~ s/($longvowels)/$longvowels{$1}/g;
651             # Doubled vowels to chouon
652 94         229 $input =~ s/([aiueo])\1/$1ー/g;
653             }
654             # Deal with double consonants
655             # danna -> だんな
656 363 100       556 if ($ime) {
657             # IME romaji rules:
658             # Allow double n for ん:
659             # gunnma -> グンマ, dannna -> ダンナ
660 3         15 $input =~ s/n{1,2}(?=[nm][aiueo])/ン/g;
661             # Substitute sokuon for mm + vowel:
662             # gumma -> グッマ
663 3         29 $input =~ s/m(?=[nm][aiueo])/ッ/g;
664             }
665             else {
666             # Usual romaji rules: Don't allow double n for ん, change
667             # gumma to グンマ.
668 360         627 $input =~ s/[nm](?=[nm][aiueo])/ン/g;
669             }
670             # shimbun -> しんぶん
671 363         621 $input =~ s/m(?=[pb]y?[aiueo])/ン/g;
672             # tcha -> っちゃ
673 363         567 $input =~ s/t(?=ch[aiueo])/ッ/g;
674             # ccha -> っちゃ
675 363         447 $input =~ s/c(?=ch[aiueo])/ッ/g;
676             # kkya -> っきゃ etc.
677 363         932 $input =~ s/([kstfhmrgzdbpjqvwy])(?=\1y?[aiueo])/ッ/g;
678             # kkya -> っきゃ etc.
679 363         527 $input =~ s/ttsu/ッツ/g;
680             # xtsu -> っ
681 363         418 $input =~ s/xtsu/ッ/g;
682             # ssha -> っしゃ
683 363         509 $input =~ s/([s])(?=\1h[aiueo])/ッ/g;
684             # Passport romaji,
685             # oh{consonant} -> oo
686 363 100       619 if (! $ime) {
687             # IMEs do not recognize passport romaji.
688 360 100       503 if ($wapuro) {
689 266         399 $input =~ s/oh(?=[ksthmrgzdbp])/オウ/g;
690             }
691             else {
692 94         141 $input =~ s/oh(?=[ksthmrgzdbp])/オー/g;
693             }
694             }
695             # All the special cases have been dealt with, now substitute all
696             # the kana.
697 363         9040 $input =~ s/($romaji_regex)/$romaji2katakana->{$1}/g;
698 363         1180 return $input;
699             }
700              
701             sub is_small
702             {
703 0     0 1 0 my ($input) = @_;
704 0 0       0 if ($input =~/[ぁぃぅぇぉっゃゅょゕゖゎヮァィゥェォッャュョヵヶ]/) {
705 0         0 return 1;
706             }
707 0         0 return undef;
708             }
709              
710              
711             sub is_voiced
712             {
713 2     2 1 5 my ($sound) = @_;
714 2 50       5 if (is_kana ($sound)) {
    0          
715 2         5 $sound = kana2romaji ($sound);
716             }
717             elsif (my $romaji = is_romaji ($sound)) {
718             # Normalize to nihon shiki so that we don't have to worry
719             # about ch, j, ts, etc. at the start of the sound.
720 0         0 $sound = $romaji;
721             }
722 2 100       8 if ($sound =~ /^[aiueogzbpmnry]/) {
723 1         4 return 1;
724             }
725             else {
726 1         4 return undef;
727             }
728             }
729              
730             sub is_romaji
731             {
732 218     218 1 323104 my ($romaji) = @_;
733 218 50       567 if (length ($romaji) == 0) {
734 0         0 return undef;
735             }
736             # Test that $romaji contains only characters which may be
737             # romanized Japanese.
738 218 100       1069 if ($romaji =~ /[^\sa-zāīūēōâîûêô'-]|^-/i) {
739 4         9 return undef;
740             }
741 214         721 my $kana = romaji2kana ($romaji, {wapuro => 1});
742 214 100       1082 if ($kana =~ /^[ア-ンッー\s]+$/) {
743 143         379 return kana2romaji ($kana, {wapuro => 1});
744             }
745 71         200 return undef;
746             }
747              
748              
749             sub is_romaji_semistrict
750             {
751 94     94 1 187 my ($romaji) = @_;
752 94 100       171 if (! is_romaji ($romaji)) {
753 34         129 return undef;
754             }
755 60 100       929 if ($romaji =~ /
756             # Don't allow small vowels, small tsu, or fya,
757             # fye etc.
758             (fy|l|x|v)y?($vowel_re|ts?u|wa|ka|ke)
759             |
760             # Don't allow hyi, hye, yi, ye.
761             [zh]?y[ieêîē]
762             |
763             # Don't allow tye
764             ty[eêē]
765             |
766             # Don't allow wh-, kw-, gw-, dh-, etc.
767             (wh|kw|gw|dh|thy)$vowel_re
768             |
769             # Don't allow "t'i"
770             [dt]'(i|y?$u_re)
771             |
772             # Don't allow dwu, twu
773             [dt](w$u_re)
774             |
775             hwy$u_re
776             |
777             # Don't allow "wi" or "we".
778             w(i|e)
779             |
780             # Don't allow some non-Japanese double consonants.
781             (?:rr|yy)
782             |
783             # Don't allow 'thi'
784             thi
785             /ix) {
786 51         249 return undef;
787             }
788 9         42 return 1;
789             }
790              
791             sub is_romaji_strict
792             {
793 107     107 1 180145 my ($romaji) = @_;
794 107         224 my $canonical = is_romaji ($romaji);
795 107 100       200 if (! $canonical) {
796 34         132 return undef;
797             }
798 73         144 my $kana = romaji2kana ($romaji);
799 73 100       338 if ($kana =~ m!
800             # Don't allow tanggono
801             ンッ
802             |
803             # Don't allow "nmichi".
804             ^ン
805             |
806             # Don't allow ffun etc.
807             ^ッ
808             !x) {
809 6         28 return undef;
810             }
811 67 100       1596 if ($romaji =~ m!
812             (fy|l|x|v)y?($vowel_re|ts?u|wa|ka|ke)
813             |
814             # Don't allow hyi, hye, yi, ye.
815             [zh]?y[ieêîē]
816             |
817             # Don't allow tye
818             ty[eêē]
819             |
820             # Don't allow wh-, kw-, gw-, dh-, etc.
821             (wh|kw|gw|dh|thy)$vowel_re
822             |
823             # Don't allow tsa, tsi, tse, tso, fa, fe, fi, fo.
824             (ts|f)$no_u_vowel_re
825             |
826             # Don't allow "t'i"
827             [dt]'(i|y?$u_re)
828             |
829             # Don't allow dwu, twu
830             [dt](w$u_re)
831             |
832             hwy$u_re
833             |
834             # Don't allow "wi" or "we".
835             w(i|e)
836             |
837             # Don't allow 'je', 'che', 'she'
838             (?:[cs]h|j)e
839             |
840             # Don't allow some non-Japanese double consonants.
841             (?:rr|yy)
842             |
843             # Don't allow 'thi'/'thu'
844             th[iu]
845             |
846             # Don't allow 'johann'
847             nn$
848             |
849             # Don't allow 'ridzuan' etc.
850             dz
851             |
852             # Qs are out.
853             q
854             |
855             # Double ws, hs, etc. are out
856             ww|hh|bb
857             |
858             # This is allowed by IMEs as "ちゃ" etc.
859             cy
860             !ix) {
861 64         410 return undef;
862             }
863 3         36 return $canonical;
864             }
865              
866             sub hira2kata
867             {
868 208     208 1 984 my (@input) = @_;
869 208 50       431 if (!@input) {
870 0         0 return;
871             }
872 208         385 for (@input) {
873 208 50       412 if ($_) {
874 208         604 tr/ぁ-んゔ/ァ-ンヴ/;
875             }
876             }
877 208 50       689 return wantarray ? @input : "@input";
878             }
879              
880             sub kata2hira
881             {
882 57     57 1 118 my (@input) = @_;
883 57         89 for (@input) {tr/ァ-ンヴ/ぁ-んゔ/}
  57         157  
884 57 50       317 return wantarray ? @input : "@input";
885             }
886              
887             # Make the list of dakuon stuff.
888              
889             sub make_dak_list
890             {
891 0     0 0 0 my @dak_list;
892 0         0 for (@_) {
893 0         0 push @dak_list, @{$gyou{$_}};
  0         0  
894 0         0 push @dak_list, hira2kata (@{$gyou{$_}});
  0         0  
895             }
896 0         0 return @dak_list;
897             }
898              
899             sub load_kana2hw2
900             {
901 3     3 0 14 my $conv = Convert::Moji->new (
902             ["oneway", "tr", "あ-ん", "ア-ン"],
903             ["file",
904             getdistfile ("katakana2hw_katakana")]
905             );
906 3         5674 return $conv;
907             }
908              
909             my $kata2hw;
910              
911             sub make_kata2hw
912             {
913 1 50   1 0 4 if (!$kata2hw) {
914 0         0 $kata2hw = make_convertors ('katakana','hw_katakana');
915             }
916             }
917              
918             my $kana2hw;
919              
920             sub kana2hw
921             {
922 3     3 1 177791 my ($input) = @_;
923 3 50       12 if (! $kana2hw) {
924 3         11 $kana2hw = load_kana2hw2 ();
925             }
926 3         14 return $kana2hw->convert ($input);
927             }
928              
929             sub katakana2hw
930             {
931 1     1 1 225 my ($input) = @_;
932 1         3 make_kata2hw ();
933 1         5 return $kata2hw->convert ($input);
934             }
935              
936             sub hw2katakana
937             {
938 8     8 1 134101 my ($input) = @_;
939 8 100       35 if (!$kata2hw) {
940 5         21 $kata2hw = make_convertors ('katakana','hw_katakana');
941             }
942 8         29 return $kata2hw->invert ($input);
943             }
944              
945             sub InHankakuKatakana
946             {
947 39     39 1 267639 return <<'END';
948             +utf8::Katakana
949             &utf8::InHalfwidthAndFullwidthForms
950             END
951             }
952              
953             # The two lists in wide2ascii and ascii2wide have exactly the same
954             # length.
955             #
956             # The warnings produced by Perl versions later than 22 are bugs in
957             # Perl:
958             #
959             # https://rt.perl.org/Public/Bug/Display.html?id=125493
960             #
961             # To save problems for users, switch off warnings in these routines.
962             #
963             # I have no idea what command to use to switch off just the
964             # "Replacement list is longer than search list" warning and leave the
965             # others intact.
966              
967 25     25   230 no warnings 'misc';
  25         45  
  25         4313  
968              
969             sub wide2ascii
970             {
971 4     4 1 134583 my ($input) = @_;
972 4         14 $input =~ tr/\x{3000}\x{FF01}-\x{FF5E}/ -~/;
973 4         10 return $input;
974             }
975              
976             sub ascii2wide
977             {
978 2     2 1 139024 my ($input) = @_;
979 2         11 $input =~ tr/ -~/\x{3000}\x{FF01}-\x{FF5E}/;
980 2         5 return $input;
981             }
982              
983 25     25   275 use warnings;
  25         74  
  25         31176  
984              
985             sub InWideAscii
986             {
987 1     1 1 19469 return <<'END';
988             FF01 FF5E
989             3000
990             END
991             }
992              
993             my $kana2morse;
994              
995             sub load_kana2morse
996             {
997 2 100   2 0 6 if (!$kana2morse) {
998 1         6 $kana2morse = make_convertors ('katakana', 'morse');
999             }
1000             }
1001              
1002             sub kana2morse
1003             {
1004 1     1 1 527 my ($input) = @_;
1005 1         4 load_kana2morse;
1006 1         4 $input = hira2kata ($input);
1007 1         3 $input =~ tr/ァィゥェォャュョッ/アイウエオヤユヨツ/;
1008 1         3 $input = split_sound_marks ($input);
1009 1         5 $input = join ' ', (split '', $input);
1010 1         5 $input = $kana2morse->convert ($input);
1011 1         3 return $input;
1012             }
1013              
1014              
1015             sub getdistfile
1016             {
1017 31     31 0 68 my ($filename) = @_;
1018 31         56 my $dir = __FILE__;
1019 31         167 $dir =~ s!\.pm$!/!;
1020 31         74 my $file = "$dir$filename.txt";
1021 31         123 return $file;
1022             }
1023              
1024             sub morse2kana
1025             {
1026 1     1 1 333 my ($input) = @_;
1027 1         3 load_kana2morse;
1028 1         4 my @input = split ' ',$input;
1029 1         2 for (@input) {
1030 7         11 $_ = $kana2morse->invert ($_);
1031             }
1032 1         3 $input = join '', @input;
1033 1         4 $input = join_sound_marks ($input);
1034 1         3 return $input;
1035             }
1036              
1037             my $kana2braille;
1038              
1039             sub load_kana2braille
1040             {
1041 2 100   2 0 6 if (!$kana2braille) {
1042 1         3 $kana2braille = make_convertors ('katakana', 'braille');
1043             }
1044             }
1045              
1046             my %nippon2kana;
1047              
1048             for my $k (keys %gyou) {
1049             for my $ar (@{$gyou{$k}}) {
1050             my $vowel = $boin{$ar};
1051             my $nippon = $k.$vowel;
1052             $nippon2kana{$nippon} = $ar;
1053             }
1054             }
1055              
1056             sub is_kana
1057             {
1058 7     7 1 448 my ($may_be_kana) = @_;
1059 7 100       64 if ($may_be_kana =~ /^[あ-んア-ン]+$/) {
1060 5         21 return 1;
1061             }
1062 2         10 return;
1063             }
1064              
1065             sub is_katakana
1066             {
1067 0     0 1 0 my ($may_be_kana) = @_;
1068 0 0       0 if ($may_be_kana =~ /^[ア-ン]+$/) {
1069 0         0 return 1;
1070             }
1071 0         0 return;
1072             }
1073              
1074             sub is_hiragana
1075             {
1076 2     2 1 5 my ($may_be_kana) = @_;
1077 2 100       14 if ($may_be_kana =~ /^[あ-ん]+$/) {
1078 1         4 return 1;
1079             }
1080 1         4 return;
1081             }
1082              
1083             my %daku2not = (qw/
1084             が か
1085             ぎ き
1086             ぐ く
1087             げ け
1088             ご こ
1089             だ た
1090             ぢ ち
1091             づ つ
1092             で て
1093             ど と
1094             ざ さ
1095             じ し
1096             ず す
1097             ぜ せ
1098             ぞ そ
1099             ば は
1100             び ひ
1101             ぶ ふ
1102             べ へ
1103             ぼ ほ
1104             ガ カ
1105             ギ キ
1106             グ ク
1107             ゲ ケ
1108             ゴ コ
1109             ダ タ
1110             ヂ チ
1111             ヅ ツ
1112             デ テ
1113             ド ト
1114             ザ サ
1115             ジ シ
1116             ズ ス
1117             ゼ セ
1118             ゾ ソ
1119             バ ハ
1120             ビ ヒ
1121             ブ フ
1122             ベ ヘ
1123             ボ ホ
1124             /);
1125              
1126             my %not2daku = reverse %daku2not;
1127              
1128             my $daku = qr![がぎぐげごだぢづでどざじずぜぞばびぶべぼガギグゲゴダヂヅデドザジズゼゾバビブベボ]!;
1129              
1130             my $nodaku = qr![かきくけこたしつてとさしすせそはひふへほカキクケコタシツテトサシスセソハヒフヘホ]!;
1131              
1132             my %handaku2not = (qw!
1133             ぱ は
1134             ぴ ひ
1135             ぷ ふ
1136             ぺ へ
1137             ぽ ほ
1138             パ ハ
1139             ピ ヒ
1140             プ フ
1141             ペ ヘ
1142             ポ ホ
1143             !);
1144              
1145             my %not2handaku = reverse %handaku2not;
1146              
1147             my $handaku = qr![ぱぴぷぺぽパピプペポ]!;
1148              
1149             my $nohandaku = qr![はひふへほハヒフヘホ]!;
1150              
1151             sub join_sound_marks
1152             {
1153 210     210 1 162076 my ($input) = @_;
1154 210         1529 $input =~ s!($nohandaku)(゚|゜)!$not2handaku{$1}!g;
1155 210         1260 $input =~ s!($nodaku)(゙|゛)!$not2daku{$1}!g;
1156             # Remove strays.
1157 210         396 $input =~ s![゙゛゚゜]!!g;
1158 210         379 return $input;
1159             }
1160              
1161             sub split_sound_marks
1162             {
1163 7     7 1 13 my ($input) = @_;
1164 7         117 $input =~ s!($handaku)!$handaku2not{$1}゜!g;
1165 7         96 $input =~ s!($daku)!$daku2not{$1}゛!g;
1166 7         18 return $input;
1167             }
1168              
1169             sub strip_sound_marks
1170             {
1171 2     2 1 5 my ($input) = @_;
1172 2         44 $input =~ s!($handaku)!$handaku2not{$1}!g;
1173 2         28 $input =~ s!($daku)!$daku2not{$1}!g;
1174 2         7 return $input;
1175             }
1176              
1177             sub kana2katakana
1178             {
1179 205     205 1 520 my ($input) = @_;
1180 205         424 $input = join_sound_marks ($input);
1181 205         431 $input = hira2kata($input);
1182 205 100       735 if ($input =~ /\p{InHankakuKatakana}/) {
1183 2         6 $input = hw2katakana($input);
1184             }
1185 205         340 return $input;
1186             }
1187              
1188             sub kana2braille
1189             {
1190 1     1 1 246 my ($input) = @_;
1191 1         4 load_kana2braille;
1192 1         4 $input = kana2katakana ($input);
1193 1         2 $input = split_sound_marks ($input);
1194 1         4 $input =~ s/([キシチヒ])゛([ャュョ])/'⠘'.$nippon2kana{$siin{$1}.$boin{$2}}/eg;
  0         0  
1195 1         2 $input =~ s/(ヒ)゜([ャュョ])/'⠨'.$nippon2kana{$siin{$1}.$boin{$2}}/eg;
  0         0  
1196 1         4 $input =~ s/([キシチニヒミリ])([ャュョ])/'⠈'.$nippon2kana{$siin{$1}.$boin{$2}}/eg;
  1         7  
1197 1         27 $input =~ s/([$vowelclass{o}])ウ/$1ー/g;
1198 1         3 $input = $kana2braille->convert ($input);
1199 1         7 $input =~ s/(.)([⠐⠠])/$2$1/g;
1200 1         4 return $input;
1201             }
1202              
1203             sub braille2kana
1204             {
1205 1     1 1 427 my ($input) = @_;
1206 1         4 load_kana2braille;
1207 1         11 $input =~ s/([⠐⠠])(.)/$2$1/g;
1208 1         3 $input = $kana2braille->invert ($input);
1209 1         3 $input =~ s/⠘(.)/$nippon2kana{$siin{$1}.'i'}.'゛'.$youon{$boin{$1}}/eg;
  0         0  
1210 1         2 $input =~ s/⠨(.)/$nippon2kana{$siin{$1}.'i'}.'゜'.$youon{$boin{$1}}/eg;
  0         0  
1211 1         39 $input =~ s/⠈(.)/$nippon2kana{$siin{$1}.'i'}.$youon{$boin{$1}}/eg;
  1         7  
1212 1         3 $input = join_sound_marks ($input);
1213 1         3 return $input;
1214             }
1215              
1216             my $circled_conv;
1217              
1218             sub load_circled_conv
1219             {
1220 2 100   2 0 6 if (!$circled_conv) {
1221 1         3 $circled_conv = make_convertors ("katakana", "circled");
1222             }
1223             }
1224              
1225             sub kana2circled
1226             {
1227 1     1 1 2 my ($input) = @_;
1228 1         3 $input = kana2katakana($input);
1229 1         3 $input = split_sound_marks ($input);
1230 1         4 load_circled_conv;
1231 1         3 $input = $circled_conv->convert ($input);
1232 1         3 return $input;
1233             }
1234              
1235             sub circled2kana
1236             {
1237 1     1 1 326 my ($input) = @_;
1238 1         3 load_circled_conv;
1239 1         3 $input = $circled_conv->invert ($input);
1240 1         3 $input = join_sound_marks ($input);
1241 1         4 return $input;
1242             }
1243              
1244             sub normalize_romaji
1245             {
1246 2     2 1 280 my ($romaji) = @_;
1247 2         7 my $kana = romaji2kana ($romaji, {wapuro => 1});
1248 2         8 $kana =~ s/[っッ]/xtu/g;
1249 2         8 my $romaji_out = kana2romaji ($kana, {ve_type => 'wapuro'});
1250 2         7 return $romaji_out;
1251             }
1252              
1253             my $new2old_kanji;
1254              
1255             sub load_new2old_kanji
1256             {
1257 1     1 0 5 $new2old_kanji = Convert::Moji->new (
1258             ['file', getdistfile ('new_kanji2old_kanji')],
1259             );
1260             }
1261              
1262             sub new2old_kanji
1263             {
1264 1     1 1 465 my ($new_kanji) = @_;
1265 1 50       4 if (! $new2old_kanji) {
1266 0         0 load_new2old_kanji ();
1267             }
1268 1         4 my $old_kanji = $new2old_kanji->convert ($new_kanji);
1269 1         115 return $old_kanji;
1270             }
1271              
1272             sub old2new_kanji
1273             {
1274 1     1 1 135632 my ($old_kanji) = @_;
1275 1 50       5 if (! $new2old_kanji) {
1276 1         3 load_new2old_kanji ();
1277             }
1278 1         3379 my $new_kanji = $new2old_kanji->invert ($old_kanji);
1279 1         119 return $new_kanji;
1280             }
1281              
1282             my $katakana2cyrillic;
1283              
1284             sub load_katakana2cyrillic
1285             {
1286 1     1 0 11 $katakana2cyrillic = Convert::Moji->new (['file', getdistfile ('katakana2cyrillic')]);
1287             }
1288              
1289             sub kana2cyrillic
1290             {
1291 4     4 1 154268 my ($kana) = @_;
1292 4         12 my $katakana = kana2katakana ($kana);
1293 4         25 $katakana =~ s/ン([アイウエオヤユヨ])/ンъ$1/g;
1294 4 100       8 if (! $katakana2cyrillic) {
1295 1         3 load_katakana2cyrillic ();
1296             }
1297 4         2398 my $cyrillic = $katakana2cyrillic->convert ($katakana);
1298 4         431 $cyrillic =~ s/н([пбм])/м$1/g;
1299 4         8 return $cyrillic;
1300             }
1301              
1302             sub cyrillic2katakana
1303             {
1304 6     6 1 1393 my ($cyrillic) = @_;
1305             # Convert the Cyrillic letters to lower case versions of the
1306             # letters. This table of conversions was made from the one in
1307             # Wikipedia at
1308             # using Emacs, the revision being
1309             # .
1310             # I do not know if it covers the alphabets perfectly.
1311 6         20 $cyrillic =~ tr/АБВГДЕЖЗИЙIКЛМНОПРСТУФХЦЧШЩЬЮЯ/абвгдежзийiклмнопрстуфхцчшщьюя/;
1312 6 50       14 if (! $katakana2cyrillic) {
1313 0         0 load_katakana2cyrillic ();
1314             }
1315 6         14 my $katakana = $katakana2cyrillic->invert ($cyrillic);
1316 6         453 $katakana =~ s/м/ン/g;
1317 6         20 $katakana =~ s/ンъ([アイウエオヤユヨ])/ン$1/g;
1318 6         10 return $katakana;
1319             }
1320              
1321             my $first2hangul;
1322             my $rest2hangul;
1323              
1324             my $first2hangul_re;
1325             my $rest2hangul_re;
1326              
1327             sub load_kana2hangul
1328             {
1329 2     2 0 7 $first2hangul = load_convertor ('first', 'hangul');
1330 2         6 $rest2hangul = load_convertor ('rest', 'hangul');
1331 2         28 $first2hangul_re = '\b' . make_regex (keys %$first2hangul);
1332 2         1458 $rest2hangul_re = make_regex (keys %$rest2hangul);
1333             }
1334              
1335             sub kana2hangul
1336             {
1337 2     2 1 172974 my ($kana) = @_;
1338 2         7 my $katakana = kana2katakana ($kana);
1339 2 100       5 if (! $first2hangul) {
1340 1         3 load_kana2hangul ();
1341             }
1342 2         663 $katakana =~ s/($first2hangul_re)/$first2hangul->{$1}/g;
1343 2         300 $katakana =~ s/($rest2hangul_re)/$rest2hangul->{$1}/g;
1344 2         22 return $katakana;
1345             }
1346              
1347             my $firsth2k_re;
1348             my $resth2k_re;
1349             my $firsth2k;
1350             my $resth2k;
1351              
1352             sub load_hangul2kana
1353             {
1354 1     1 0 3 load_kana2hangul ();
1355 1         500 $firsth2k = { reverse %$first2hangul };
1356 1         29 $resth2k = { reverse %$rest2hangul };
1357 1         14 $firsth2k_re = '\b' . make_regex (keys %$firsth2k);
1358 1         188 $resth2k_re = make_regex (keys %$resth2k);
1359             }
1360              
1361             sub hangul2kana
1362             {
1363 1     1 0 2 my ($hangul) = @_;
1364 1 50       3 if (! $firsth2k) {
1365 1         3 load_hangul2kana ();
1366             }
1367 1         227 $hangul =~ s/($firsth2k_re)/$firsth2k->{$1}/;
1368 1         36 $hangul =~ s/($resth2k_re)/$resth2k->{$1}/;
1369 1         5 return $hangul;
1370             }
1371              
1372             my %small2large = qw!
1373             ゃ や
1374             ゅ ゆ
1375             ょ よ
1376             ぁ あ
1377             ぃ い
1378             ぅ う
1379             ぇ え
1380             ぉ お
1381             っ つ
1382             ゎ わ
1383             !;
1384              
1385             sub kana_to_large
1386             {
1387 2     2 1 1108 my ($kana) = @_;
1388 2         9 $kana =~ tr/ゃゅょぁぃぅぇぉっゎ/やゆよあいうえおつわ/;
1389 2         4 $kana =~ tr/ャュョァィゥェォッヮ/ヤユヨアイウエオツワ/;
1390             # Katakana phonetic extensions.
1391 2         3 $kana =~ tr/ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ/クシストヌハヒフヘホムラリルレロ/;
1392 2         6 return $kana;
1393             }
1394              
1395             my $circled2kanji;
1396              
1397             sub load_circled2kanji
1398             {
1399 4 100   4 0 14 if (! $circled2kanji) {
1400 1         5 $circled2kanji =
1401             Convert::Moji->new (["file",
1402             getdistfile ('circled2kanji')]);
1403             }
1404 4 50       937 if (! $circled2kanji) {
1405 0         0 die "ERROR";
1406             }
1407             }
1408              
1409             sub circled2kanji
1410             {
1411 2     2 1 139642 my ($input) = @_;
1412 2         6 load_circled2kanji ();
1413 2         15 return $circled2kanji->convert ($input);
1414             }
1415              
1416             sub kanji2circled
1417             {
1418 2     2 1 1972 my ($input) = @_;
1419 2         10 load_circled2kanji ();
1420 2         10 return $circled2kanji->invert ($input);
1421             }
1422              
1423             my $bracketed2kanji;
1424              
1425             sub load_bracketed2kanji
1426             {
1427 2 100   2 0 7 if (! $bracketed2kanji) {
1428 1         3 $bracketed2kanji =
1429             Convert::Moji->new (["file",
1430             getdistfile ('bracketed2kanji')]);
1431             }
1432             }
1433              
1434             sub bracketed2kanji
1435             {
1436 1     1 1 794 my ($input) = @_;
1437 1         3 load_bracketed2kanji ();
1438 1         668 return $bracketed2kanji->convert ($input);
1439             }
1440              
1441             sub kanji2bracketed
1442             {
1443 1     1 1 743 my ($input) = @_;
1444 1         4 load_bracketed2kanji ();
1445 1         4 return $bracketed2kanji->invert ($input);
1446             }
1447              
1448             sub InKana
1449             {
1450 30     30 1 162949 return <<"END";
1451             +utf8::Katakana
1452             +utf8::InKatakana
1453             +utf8::InHiragana
1454             FF9E\tFF9F
1455             FF70
1456             -utf8::IsCn
1457             -30FB
1458             END
1459             # Explanation of the above gibberish: The funny hex is for dakuten
1460             # and handakuten half width. The "Katakana" catches halfwidth
1461             # katakana, and the "InKatakana" catches the chouon mark. "IsCn"
1462             # means "other, not assigned", so we remove this to prevent
1463             # matching non-kana characters floating around near to real
1464             # ones. 30FB is "Katakana middle dot", which is not kana as far as
1465             # I know, so that's also removed.
1466             }
1467              
1468             # お
1469              
1470             # Match zero or one sokuons, one full-sized kana character, then zero
1471             # or one each of small kana, chouon, and syllabic n, in that order.
1472              
1473             my $kana2syllable_re = qr/ッ?[アイウエオ-モヤユヨ-ヴ](?:[ャュョァィゥェォ])?ー?ン?/;
1474              
1475             sub katakana2syllable
1476             {
1477 4     4 1 151863 my ($kana) = @_;
1478 4         4 my @pieces;
1479 4         90 while ($kana =~ /($kana2syllable_re)/g) {
1480 22         79 push @pieces, $1;
1481             }
1482 4         35 return \@pieces;
1483             }
1484              
1485             my $square2katakana;
1486              
1487             sub load_square2katakana
1488             {
1489 2 100   2 0 8 if (! $square2katakana) {
1490 1         6 $square2katakana =
1491             Convert::Moji->new (["file",
1492             getdistfile ('square-katakana')]);
1493             }
1494             }
1495              
1496             sub square2katakana
1497             {
1498 1     1 1 136510 load_square2katakana ();
1499 1         1589 return $square2katakana->convert (@_);
1500             }
1501              
1502             sub katakana2square
1503             {
1504 1     1 1 847 load_square2katakana ();
1505 1         5 return $square2katakana->invert (@_);
1506             }
1507              
1508             sub nigori_first
1509             {
1510 1     1 1 560 my ($list) = @_;
1511 1         2 my @nigori;
1512 1         2 for my $kana (@$list) {
1513 4         8 my ($first, $remaining) = split //, $kana, 2;
1514 4         23 my $nf = $not2daku{$first};
1515 4 100       6 if ($nf) {
1516 3         5 push @nigori, $nf.$remaining;
1517             }
1518 4         7 my $hf = $not2handaku{$first};
1519 4 100       8 if ($hf) {
1520 1         2 push @nigori, $hf.$remaining;
1521             }
1522             }
1523 1 50       4 if (@nigori) {
1524 1         5 push @$list, @nigori;
1525             }
1526             }
1527              
1528             # Hentaigana (Unicode 10.0) related
1529              
1530             my $hentai_file = __FILE__;
1531             $hentai_file =~ s!\.pm$!/!;
1532             $hentai_file .= "hentaigana.json";
1533             # Hentai to hiragana (one to one)
1534             my %hen2hi;
1535             # Hiragana to hentai (one to many)
1536             my %hi2hen;
1537             # Hentaigana to kanji
1538             my %hen2k;
1539             # Kanji to hentaigana
1540             my %k2hen;
1541             my $k2hen_re;
1542             # Hentai to hiragana/kanji regex (recycled for the kanji case).
1543             my $hen_re;
1544             # Hiragana to hentai regex
1545             my $hi2hen_re;
1546             # Hentai data
1547             my $hendat;
1548              
1549             sub load_hentai
1550             {
1551 1     1 0 5 $hendat = read_json ($hentai_file);
1552 1         752 for my $h (@$hendat) {
1553 285         307 my $hi = $h->{hi};
1554 285         320 my $hen = chr ($h->{u});
1555 285         847 $hen2hi{$hen} = $hi;
1556 285         319 for my $hiragana (@$hi) {
1557 298         269 push @{$hi2hen{$hiragana}}, $hen;
  298         495  
1558             }
1559 285         413 $hen2k{$hen} = $h->{ka};
1560 285         261 push @{$k2hen{$h->{ka}}}, $hen;
  285         597  
1561             }
1562 1         52 $hen_re = make_regex (keys %hen2hi);
1563 1         662 $hi2hen_re = make_regex (keys %hi2hen);
1564 1         125 $k2hen_re = make_regex (keys %k2hen);
1565             }
1566              
1567             sub hentai2kana
1568             {
1569 1     1 1 142929 my ($text) = @_;
1570 1 50       3 if (! $hendat) {
1571 1         4 load_hentai ();
1572             }
1573 1         602 $text =~ s/$hen_re/join ('・', @{$hen2hi{$1}})/ge;
  4         5  
  4         15  
1574 1         7 return $text;
1575             }
1576              
1577             sub kana2hentai
1578             {
1579 2     2 1 4 my ($text) = @_;
1580 2 50       6 if (! $hendat) {
1581 0         0 load_hentai ();
1582             }
1583             # Make it all-hiragana.
1584 2         7 $text = split_sound_marks ($text);
1585 2         7 $text = kata2hira ($text);
1586 2         26 $text =~ s/$hi2hen_re/join ('・', @{$hi2hen{$1}})/ge;
  8         9  
  8         26  
1587 2         24 return $text;
1588             # what to do?
1589             }
1590              
1591             sub hentai2kanji
1592             {
1593 1     1 1 3 my ($text) = @_;
1594 1 50       3 if (! $hendat) {
1595 0         0 load_hentai ();
1596             }
1597             # This uses the same regex as the kanji case.
1598 1         68 $text =~ s/$hen_re/$hen2k{$1}/g;
1599 1         5 return $text;
1600             }
1601              
1602             sub kanji2hentai
1603             {
1604 1     1 1 3 my ($text) = @_;
1605 1 50       4 if (! $hendat) {
1606 0         0 load_hentai ();
1607             }
1608 1         94 $text =~ s/$k2hen_re/join ('・', @{$k2hen{$1}})/ge;
  4         10  
  4         20  
1609 1         7 return $text;
1610             }
1611              
1612             my %yayuyo = (qw/
1613             ヤ ャ
1614             ユ ュ
1615             ヨ ョ
1616             /);
1617              
1618             my %l2s = qw!ア ァ イ ィ ウ ゥ エ ェ オ ォ!;
1619              
1620             sub smallize_kana
1621             {
1622 10     10 1 178549 my ($kana) = @_;
1623 10         12 my $orig = $kana;
1624 10         74 $kana =~ s/([キギシジチヂニヒビピミリ])([ヤユヨ])/$1$yayuyo{$2}/g;
1625             # Don't make "ツル" into "ッル".
1626 10         141 $kana =~ s/([$before_sokuon])ツ([$takes_sokuon])/$1ッ$2/g;
1627 10         32 $kana =~ s/フ([アイエオ])/フ$l2s{$1}/g;
1628 10 100       27 if ($kana ne $orig) {
1629 7         32 return $kana;
1630             }
1631 3         8 return undef;
1632             }
1633              
1634             sub cleanup_kana
1635             {
1636 3     3 1 487 my ($kana) = @_;
1637 3 50       10 if (! $kana) {
1638 0         0 return $kana;
1639             }
1640 3 100       23 if ($kana =~ /[\x{ff01}-\x{ff5e}]/) {
    50          
1641 1         4 $kana = wide2ascii ($kana);
1642 1         3 $kana = romaji2kana ($kana);
1643             }
1644             elsif ($kana =~ /[a-zâîûêôôāūēō]/i) {
1645 0         0 $kana = romaji2kana ($kana);
1646             }
1647             # This calls join_sound_marks, so that call is not necessary.
1648 3         9 $kana = kana2katakana ($kana);
1649             # Translate kanjis into katakana where a "naive user" has inserted
1650             # kanji not kana. Because the following expression is visually
1651             # confusing, note that the LHS are all kanji, and the RHS are all
1652             # kana/chouon
1653 3         11 $kana =~ tr/囗口八力二一/ロロハカニー/;
1654             # Turn silly small youon kana into big ones
1655 3         18 $kana =~ s/([^きぎしじちぢにひびぴみり]|^)([ゃゅょ])/$1$small2large{$2}/g;
1656 3         15 return $kana;
1657             }
1658              
1659             sub load_kanji
1660             {
1661 2     2 0 6 my ($file) = @_;
1662 2         9 my $bkfile = getdistfile ($file);
1663 2 50       119 open my $in, "<:encoding(utf8)", $bkfile
1664             or die "Error opening '$bkfile': $!";
1665 2         117 my @bk;
1666 2         54 while (<$in>) {
1667 20         113 while (/(\p{InCJKUnifiedIdeographs})/g) {
1668 268         599 push @bk, $1;
1669             }
1670             }
1671 2 50       31 close $in or die $!;
1672 2         65 return @bk;
1673             }
1674              
1675             sub yurei_moji
1676             {
1677 1     1 1 1017 return load_kanji ('yurei-moji')
1678             }
1679              
1680             sub bad_kanji
1681             {
1682 1     1 1 7 return load_kanji ('bad-kanji');
1683             }
1684              
1685             sub kana_consonant
1686             {
1687 5     5 1 149724 my ($kana) = @_;
1688 5 100       20 if (length ($kana) < 1) {
1689 1         261 croak "Empty input to kana_consonant";
1690             }
1691 4         12 my $first = substr ($kana, 0, 1);
1692 4 100       35 if ($first !~ /\p{InKana}/) {
1693 1         133 croak "First character '$first' of '$kana' is not kana";
1694             }
1695 3         10 $first = kana2katakana ($first);
1696 3         9 my $not = $daku2not{$first};
1697 3 100       6 if (defined $not) {
1698 1         2 $first = $not;
1699             }
1700 3         6 my $con = $siin{$first};
1701 3         15 return $con;
1702             }
1703              
1704             1;