File Coverage

blib/lib/Lingua/JPN/Num2Word.pm
Criterion Covered Total %
statement 108 111 97.3
branch 42 52 80.7
condition 22 43 51.1
subroutine 16 16 100.0
pod 4 4 100.0
total 192 226 84.9


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8; -*-
2             #
3             # Copyright (c) Mike Schilli, 2001 (m@perlmeister.com)
4             # Copyright (c) PetaMem, s.r.o. 2002-present
5              
6             package Lingua::JPN::Num2Word;
7             # ABSTRACT: Number to word conversion in Japanese
8              
9 2     2   97773 use 5.16.0;
  2         8  
10 2     2   703 use utf8;
  2         314  
  2         17  
11 2     2   57 use warnings;
  2         12  
  2         103  
12              
13             # {{{ use block
14              
15 2     2   14 use Carp;
  2         4  
  2         204  
16 2     2   1255 use Export::Attrs;
  2         21345  
  2         11  
17              
18             # }}}
19             # {{{ variables declaration
20             our $VERSION = '0.2604300';
21              
22             # {{{ lexicons — kanji / hiragana / romaji per script
23              
24             my %DIGIT = (
25             kanji => [ qw( 〇 一 二 三 四 五 六 七 八 九 ) ],
26             hiragana => [ qw( ぜろ いち に さん よん ご ろく なな はち きゅう ) ],
27             romaji => [ qw( zero ichi ni san yon go roku nana hachi kyu ) ],
28             );
29              
30             # Scale words at each magnitude. Hundred (百), thousand (千), ten-thousand (万),
31             # hundred-million (億), trillion (兆) — the canonical Japanese scale ladder.
32             my %SCALE = (
33             kanji => { 10 => '十', 100 => '百', 1000 => '千', 10_000 => '万',
34             100_000_000 => '億', 1_000_000_000_000 => '兆' },
35             hiragana => { 10 => 'じゅう', 100 => 'ひゃく', 1000 => 'せん', 10_000 => 'まん',
36             100_000_000 => 'おく', 1_000_000_000_000 => 'ちょう' },
37             romaji => { 10 => 'ju', 100 => 'hyaku', 1000 => 'sen', 10_000 => 'man',
38             100_000_000 => 'oku', 1_000_000_000_000 => 'cho' },
39             );
40              
41             # Irregular hundreds (rendaku/gemination):
42             # 300 さんびゃく sanbyaku
43             # 600 ろっぴゃく roppyaku
44             # 800 はっぴゃく happyaku
45             my %HUNDRED_IRREGULAR = (
46             kanji => { 3 => '三百', 6 => '六百', 8 => '八百' },
47             hiragana => { 3 => 'さんびゃく', 6 => 'ろっぴゃく', 8 => 'はっぴゃく' },
48             romaji => { 3 => 'sanbyaku', 6 => 'roppyaku', 8 => 'happyaku' },
49             );
50              
51             # Irregular thousands:
52             # 3000 さんぜん sanzen (rendaku)
53             # 8000 はっせん hassen (gemination)
54             my %THOUSAND_IRREGULAR = (
55             kanji => { 3 => '三千', 8 => '八千' },
56             hiragana => { 3 => 'さんぜん', 8 => 'はっせん' },
57             romaji => { 3 => 'sanzen', 8 => 'hassen' },
58             );
59              
60             # Irregular trillion-block leaders (cho-prefixes):
61             # 1兆 いっちょう itcho
62             # 8兆 はっちょう hatcho
63             # 10兆 じゅっちょう jutcho
64             my %CHO_IRREGULAR = (
65             kanji => { 1 => '一兆', 8 => '八兆', 10 => '十兆' },
66             hiragana => { 1 => 'いっちょう', 8 => 'はっちょう', 10 => 'じゅっちょう' },
67             romaji => { 1 => 'itcho', 8 => 'hatcho', 10 => 'jutcho' },
68             );
69              
70             # }}}
71              
72             # }}}
73              
74             # {{{ num2jpn_cardinal number → text in chosen script
75              
76             sub num2jpn_cardinal :Export {
77 31     31 1 204535 my $n = shift;
78 31   100     133 my $script = shift // 'kanji';
79              
80             croak "Unknown script '$script' (expected: kanji, hiragana, romaji)"
81 31 50       96 unless exists $DIGIT{$script};
82              
83 31 50 33     394 croak "Number must be in range [1, 1E16)"
      33        
      33        
84             if !defined $n || $n !~ m{\A\d+\z}xms || $n < 1 || $n >= 1E16;
85              
86 31         85 return _render($n, $script);
87 2     2   909 }
  2         5  
  2         14  
88              
89             # }}}
90             # {{{ num2jpn_ordinal number → ordinal text in chosen script
91              
92             sub num2jpn_ordinal :Export {
93 8     8 1 22 my $n = shift;
94 8   100     35 my $script = shift // 'kanji';
95              
96             croak "Unknown script '$script' (expected: kanji, hiragana, romaji)"
97 8 50       27 unless exists $DIGIT{$script};
98              
99 8 50 33     126 croak "Number must be in range [1, 1E16)"
      33        
      33        
100             if !defined $n || $n !~ m{\A\d+\z}xms || $n < 1 || $n >= 1E16;
101              
102 8         40 my $cardinal = _render($n, $script);
103 8 100       28 if ($script eq 'romaji') {
104             # Conventional ordinal romaji is fully hyphen-joined: spaces between
105             # block-tokens become hyphens, and the suffix attaches with a hyphen.
106 3         9 $cardinal =~ tr/ /-/;
107 3         25 return $cardinal . '-ban-me';
108             }
109 5         18 my %suffix = (kanji => '番目', hiragana => 'ばんめ');
110 5         42 return $cardinal . $suffix{$script};
111 2     2   917 }
  2         3  
  2         10  
112              
113             # }}}
114             # {{{ to_string legacy romaji-list interface (DEPRECATED)
115              
116             # Mike Schilli's 2001 lexicon — preserved verbatim so to_string output is
117             # bit-for-bit identical to the historical contract. New code should use
118             # num2jpn_cardinal($n, 'romaji') instead.
119             my %_LEGACY_N2J = qw(
120             1 ichi 2 ni 3 san 4 yon 5 go 6 roku 7 nana
121             8 hachi 9 kyu 10 ju 100 hyaku 1000 sen);
122              
123             my %_LEGACY_N2J_EXCP = qw(
124             300 san-byaku 600 ro-p-pyaku 800 ha-p-pyaku
125             3000 san-zen 8000 ha-s-sen);
126              
127             my @_LEGACY_N2J_BLOCK = ('', 'man', 'oku', 'cho');
128              
129             my %_LEGACY_N2J_BLOCK_EXCP = qw( 1 i-t-cho 8 ha-t-cho 0 ju-t-cho );
130              
131             sub to_string :Export {
132 5     5 1 2204 my $n = shift;
133              
134 5 50 33     62 if (!defined $n || $n < 1 || $n >= 1E16) {
      33        
135 0         0 warn "$n needs to be >=1 and <1E16.\n";
136 0         0 return;
137             }
138              
139 5         9 my @result;
140 5         13 $n = reverse $n;
141 5         6 my $bix = 0;
142              
143 5         84 while ($n =~ /(\d{1,4})/g) {
144 9         20 my $b = scalar reverse($1);
145 9         15 my @r = _legacy_blockof4($b);
146              
147 9 100 100     22 if ($bix && @r) {
148 2 100 66     9 if ($bix == 3 && $b =~ /[1-9]0$|[18]$/) {
149 1         4 $r[-1] = $_LEGACY_N2J_BLOCK_EXCP{$b % 10};
150             }
151             else {
152 1         2 push @r, $_LEGACY_N2J_BLOCK[$bix];
153             }
154             }
155 9         13 unshift @result, @r;
156 9         20 $bix++;
157             }
158              
159 5         30 return @result;
160 2     2   1158 }
  2         4  
  2         8  
161              
162             sub _legacy_blockof4 {
163 9     9   12 my $n = shift;
164 9 50 33     27 return if $n > 9999 or $n < 0;
165 9 50       14 return '' unless $n;
166              
167 9         11 my @result;
168 9         29 my @digits = split //, sprintf('%04d', $n);
169 9         15 my @weights = (1000, 100, 10, 1);
170              
171 9         16 for my $i (0..3) {
172 36 100       48 next unless $digits[$i];
173 13         19 my $v = $digits[$i] * $weights[$i];
174             push @result, $_LEGACY_N2J_EXCP{$v}
175             || $_LEGACY_N2J{$v}
176 13   66     54 || ($_LEGACY_N2J{$digits[$i]}, $_LEGACY_N2J{$weights[$i]});
177             }
178              
179 9         22 return @result;
180             }
181              
182             # }}}
183             # {{{ _render core: number → string in given script
184              
185             sub _render {
186 39     39   86 my ($n, $script) = @_;
187              
188             # Decompose into 4-digit blocks, weighted by 10^(4*k):
189             # block 0 → 1
190             # block 1 → 万 (10^4)
191             # block 2 → 億 (10^8)
192             # block 3 → 兆 (10^12)
193 39         83 my @block_scales = (1, 10_000, 100_000_000, 1_000_000_000_000);
194 39         73 my @parts;
195 39         58 my $bix = 0;
196              
197 39         91 while ($n > 0) {
198 46         109 my $block = $n % 10_000;
199 46         105 $n = int($n / 10_000);
200 46 100       86 if ($block) {
201 39         56 my $piece;
202 39 100 66     113 if ($bix == 3 && exists $CHO_IRREGULAR{$script}{$block}) {
203             # Whole-block chō irregulars: 1兆, 8兆, 10兆 fuse digit+scale.
204 1         4 $piece = $CHO_IRREGULAR{$script}{$block};
205             }
206             else {
207 38         76 $piece = _render_block4($block, $script);
208 38 100       105 $piece .= $SCALE{$script}{$block_scales[$bix]} if $bix > 0;
209             }
210 39         91 unshift @parts, $piece;
211             }
212 46         102 $bix++;
213             }
214              
215 39         103 return _join_parts(\@parts, $script);
216             }
217              
218             # }}}
219             # {{{ _render_block4 render integer 1..9999 in given script
220              
221             sub _render_block4 {
222 38     38   63 my ($n, $script) = @_;
223              
224 38 50       74 return '' unless $n;
225              
226             # In romaji, digit and scale fuse into a single word ("nihyaku", "sanju",
227             # "yonsen"). Block boundaries get whitespace ("sen nihyaku sanju yon"
228             # for 1234). Kanji and hiragana have no whitespace at all.
229 38         45 my @parts;
230              
231             # Thousands (1000..9999)
232 38 100       92 if (my $th = int($n / 1000)) {
233 11 100       42 if (exists $THOUSAND_IRREGULAR{$script}{$th}) {
    50          
234 6         20 push @parts, $THOUSAND_IRREGULAR{$script}{$th};
235             }
236             elsif ($th == 1) {
237 5         17 push @parts, $SCALE{$script}{1000};
238             }
239             else {
240 0         0 push @parts, $DIGIT{$script}[$th] . $SCALE{$script}{1000};
241             }
242 11         20 $n %= 1000;
243             }
244              
245             # Hundreds (100..999)
246 38 100       102 if (my $h = int($n / 100)) {
247 14 100       44 if (exists $HUNDRED_IRREGULAR{$script}{$h}) {
    100          
248 8         53 push @parts, $HUNDRED_IRREGULAR{$script}{$h};
249             }
250             elsif ($h == 1) {
251 3         9 push @parts, $SCALE{$script}{100};
252             }
253             else {
254 3         14 push @parts, $DIGIT{$script}[$h] . $SCALE{$script}{100};
255             }
256 14         21 $n %= 100;
257             }
258              
259             # Tens (10..99)
260 38 100       78 if (my $t = int($n / 10)) {
261 9 100       19 if ($t == 1) {
262 5         19 push @parts, $SCALE{$script}{10};
263             }
264             else {
265 4         13 push @parts, $DIGIT{$script}[$t] . $SCALE{$script}{10};
266             }
267 9         15 $n %= 10;
268             }
269              
270             # Units (1..9)
271 38 100       116 push @parts, $DIGIT{$script}[$n] if $n;
272              
273 38         86 return _join_parts(\@parts, $script);
274             }
275              
276             # }}}
277             # {{{ _join_parts concatenate parts with script-appropriate sep
278              
279             sub _join_parts {
280 77     77   126 my ($parts_lr, $script) = @_;
281 77 50       100 return '' unless @{$parts_lr};
  77         165  
282             # Romaji is the only script that uses whitespace as a word separator.
283             # Kanji and hiragana are written as a continuous string.
284 77 100       144 return $script eq 'romaji' ? join(' ', @{$parts_lr}) : join('', @{$parts_lr});
  26         135  
  51         279  
285             }
286              
287             # }}}
288             # {{{ capabilities declare supported features
289              
290             sub capabilities {
291             return {
292 1     1 1 5 cardinal => 1,
293             ordinal => 1,
294             scripts => [ 'kanji', 'hiragana', 'romaji' ],
295             };
296             }
297              
298             # }}}
299             1;
300              
301             __END__