File Coverage

blib/lib/Lingua/FIN/Word2Num.pm
Criterion Covered Total %
statement 23 71 32.3
branch 0 38 0.0
condition 2 10 20.0
subroutine 9 10 90.0
pod 3 3 100.0
total 37 132 28.0


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8 -*-
2             package Lingua::FIN::Word2Num;
3             # ABSTRACT: Word to number conversion in Finnish
4              
5 1     1   130868 use 5.16.0;
  1         5  
6 1     1   7 use utf8;
  1         3  
  1         15  
7 1     1   38 use warnings;
  1         2  
  1         108  
8              
9             # {{{ use block
10              
11 1     1   720 use Export::Attrs;
  1         13664  
  1         14  
12 1     1   1460 use Parse::RecDescent;
  1         63216  
  1         11  
13              
14             # }}}
15             # {{{ var block
16             our $VERSION = '0.2603300';
17             my $parser = fin_numerals();
18              
19             # }}}
20              
21             # {{{ w2n convert text to number
22              
23             sub w2n :Export {
24 2   100 2 1 287862 my $input = shift // return;
25              
26 1         18 return $parser->numeral($input);
27 1     1   154 }
  1         2  
  1         12  
28              
29             # }}}
30             # {{{ fin_numerals create parser for finnish numerals
31              
32             sub fin_numerals {
33 1     1 1 7 return Parse::RecDescent->new(q{
34            
35              
36             numeral: mega
37             | kOhOd
38             | { }
39              
40             number: 'yksitoista' { 11 }
41             | 'kaksitoista' { 12 }
42             | 'kolmetoista' { 13 }
43             | 'neljätoista' { 14 }
44             | 'viisitoista' { 15 }
45             | 'kuusitoista' { 16 }
46             | 'seitsemäntoista' { 17 }
47             | 'kahdeksantoista' { 18 }
48             | 'yhdeksäntoista' { 19 }
49             | 'kymmenen' { 10 }
50             | 'nolla' { 0 }
51             | 'yksi' { 1 }
52             | 'kaksi' { 2 }
53             | 'kolme' { 3 }
54             | 'neljä' { 4 }
55             | 'viisi' { 5 }
56             | 'kuusi' { 6 }
57             | 'seitsemän' { 7 }
58             | 'kahdeksan' { 8 }
59             | 'yhdeksän' { 9 }
60              
61             tens: 'kaksikymmentä' { 20 }
62             | 'kolmekymmentä' { 30 }
63             | 'neljäkymmentä' { 40 }
64             | 'viisikymmentä' { 50 }
65             | 'kuusikymmentä' { 60 }
66             | 'seitsemänkymmentä' { 70 }
67             | 'kahdeksankymmentä' { 80 }
68             | 'yhdeksänkymmentä' { 90 }
69              
70             deca: tens number { $item[1] + $item[2] }
71             | tens
72             | number
73              
74             hecto: number 'sataa' deca { $item[1] * 100 + $item[3] }
75             | number 'sataa' { $item[1] * 100 }
76             | 'sata' deca { 100 + $item[2] }
77             | 'sata' { 100 }
78              
79             hOd: hecto
80             | deca
81              
82             kilo: hOd 'tuhatta' hOd { $item[1] * 1000 + $item[3] }
83             | hOd 'tuhatta' { $item[1] * 1000 }
84             | 'tuhat' hOd { 1000 + $item[2] }
85             | 'tuhat' { 1000 }
86              
87             kOhOd: kilo
88             | hOd
89              
90             mega: hOd /miljoona[a]?/ kOhOd { $item[1] * 1_000_000 + $item[3] }
91             | hOd /miljoona[a]?/ { $item[1] * 1_000_000 }
92             | 'miljoona' kOhOd { 1_000_000 + $item[2] }
93             | 'miljoona' { 1_000_000 }
94             });
95             }
96              
97             # }}}
98              
99             # {{{ ordinal2cardinal convert ordinal text to cardinal text
100              
101             sub ordinal2cardinal :Export {
102 0   0 0 1   my $input = shift // return;
103              
104             # Finnish ordinals: suppletive 1st/2nd, regular -s suffix for 3+.
105             # Compounds: cardinal prefix + ordinal tail.
106             # Order of matching matters: hundreds before tens to avoid
107             # false matches on prefixed forms.
108              
109 0           my %irregular = (
110             'ensimmäinen' => 'yksi',
111             'toinen' => 'kaksi',
112             );
113              
114 0 0         return $irregular{$input} if exists $irregular{$input};
115              
116             # Ordinal stem → cardinal mappings for units
117 0           my %ord_stem_to_cardinal = (
118             'yhde' => 'yksi',
119             'kahde' => 'kaksi',
120             'kolma' => 'kolme',
121             'neljä' => 'neljä',
122             'viide' => 'viisi',
123             'kuude' => 'kuusi',
124             'seitsemä' => 'seitsemän',
125             'kahdeksa' => 'kahdeksan',
126             'yhdeksä' => 'yhdeksän',
127             );
128              
129             # Ordinal hundreds prefix → cardinal hundreds prefix
130             # "toinensadas" (200th) → "kaksi", "kolmassadas" → "kolme", etc.
131             # The stem before "sadas" is the ordinal form of the multiplier.
132 0           my %ord_hundreds_pfx = (
133             'toinen' => 'kaksi', # 200th uses suppletive "toinen"
134             'kolmas' => 'kolme',
135             'neljäs' => 'neljä',
136             'viides' => 'viisi',
137             'kuudes' => 'kuusi',
138             'seitsemäs' => 'seitsemän',
139             'kahdeksas' => 'kahdeksan',
140             'yhdeksäs' => 'yhdeksän',
141             );
142              
143             # === HUNDREDS: check before tens to avoid false prefix matches ===
144              
145             # Round hundreds ordinal: "sadas" (100th), "toinensadas" (200th), etc.
146 0 0         if ($input eq 'sadas') {
147 0           return 'sata';
148             }
149 0 0         if ($input =~ m{\A (.+) sadas \z}xms) {
150 0           my $pfx = $1;
151 0           my $card_pfx = $ord_hundreds_pfx{$pfx};
152 0 0         return $card_pfx . 'sataa' if defined $card_pfx;
153             }
154              
155             # Compound 100+rest: "sata" + ordinal(rest) → "sata" + cardinal(rest)
156 0 0         if ($input =~ m{\A sata (?.+) \z}xms) {
157 0           my $rest = $+{rest};
158 0           my $cardinal = ordinal2cardinal($rest);
159 0 0         return defined $cardinal ? 'sata' . $cardinal : undef;
160             }
161              
162             # Compound N*100+rest: cardinal(N) + "sata" + ordinal(rest)
163             # Cardinal form uses "sataa" for N≥2: "kaksisataayksi" (201)
164 0           for my $stem (sort { length($b) <=> length($a) } keys %ord_stem_to_cardinal) {
  0            
165 0           my $card = $ord_stem_to_cardinal{$stem};
166 0 0         if ($input =~ m{\A \Q$card\E sata (?.+) \z}xms) {
167 0           my $rest = $+{rest};
168 0           my $cardinal = ordinal2cardinal($rest);
169 0 0         return defined $cardinal ? $card . 'sataa' . $cardinal : undef;
170             }
171             }
172              
173             # === TENS ===
174              
175             # Standalone "kymmenes" → "kymmenen" (10th)
176 0 0         return 'kymmenen' if $input eq 'kymmenes';
177              
178             # Compound tens + ones: "kahdeskymmenesviides" (25th)
179 0 0         if ($input =~ m{\A (?.+?) s? kymmenes (?.+) \z}xms) {
180 0           my $tens_stem = $+{tpfx};
181 0           my $rest = $+{rest};
182 0   0       my $tens_cardinal = $ord_stem_to_cardinal{$tens_stem} // return;
183 0           my $tens_word = $tens_cardinal . 'kymmentä';
184 0   0       my $ones_cardinal = ordinal2cardinal($rest) // return;
185 0           return $tens_word . $ones_cardinal;
186             }
187              
188             # Round tens ordinal: "kahdeskymmenes" (20th)
189 0 0         if ($input =~ m{\A (?.+?) s? kymmenes \z}xms) {
190 0           my $tens_stem = $+{tpfx};
191 0   0       my $tens_cardinal = $ord_stem_to_cardinal{$tens_stem} // return;
192 0           return $tens_cardinal . 'kymmentä';
193             }
194              
195             # === TEENS ===
196              
197 0 0         if ($input =~ m{\A (?.+) stoista \z}xms) {
198 0           my $stem = $+{stem};
199             return $ord_stem_to_cardinal{$stem} . 'toista'
200 0 0         if exists $ord_stem_to_cardinal{$stem};
201             }
202 0 0         if ($input =~ m{\A (?.+?) s toista \z}xms) {
203 0           my $stem = $+{stem};
204             return $ord_stem_to_cardinal{$stem} . 'toista'
205 0 0         if exists $ord_stem_to_cardinal{$stem};
206             }
207 0 0         if ($input =~ m{\A (?.+?) toista \z}xms) {
208 0           my $stem = $+{stem};
209 0           $stem =~ s{s\z}{}xms;
210             return $ord_stem_to_cardinal{$stem} . 'toista'
211 0 0         if exists $ord_stem_to_cardinal{$stem};
212             }
213              
214             # === REGULAR: strip -s suffix and map stem ===
215 0 0         if ($input =~ s{s\z}{}xms) {
216 0 0         return $ord_stem_to_cardinal{$input} if exists $ord_stem_to_cardinal{$input};
217 0           return $input;
218             }
219              
220 0           return; # not an ordinal
221 1     1   1381 }
  1         2  
  1         6  
222              
223             # }}}
224              
225             1;
226              
227             __END__