File Coverage

blib/lib/Lingua/FIN/Num2Word.pm
Criterion Covered Total %
statement 27 99 27.2
branch 2 70 2.8
condition 4 57 7.0
subroutine 9 12 75.0
pod 3 3 100.0
total 45 241 18.6


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8 -*-
2              
3             package Lingua::FIN::Num2Word;
4             # ABSTRACT: Number to word conversion in Finnish
5              
6 1     1   112229 use 5.16.0;
  1         4  
7 1     1   6 use utf8;
  1         2  
  1         13  
8 1     1   27 use warnings;
  1         1  
  1         89  
9              
10             # {{{ use block
11              
12 1     1   6 use Carp;
  1         2  
  1         80  
13 1     1   634 use Export::Attrs;
  1         11260  
  1         8  
14 1     1   766 use Readonly;
  1         4682  
  1         567  
15              
16             # }}}
17             # {{{ variable declarations
18              
19             my Readonly::Scalar $COPY = 'Copyright (c) PetaMem, s.r.o. 2002-present';
20             our $VERSION = '0.2603300';
21              
22             # }}}
23              
24             # {{{ num2fin_cardinal convert number to text
25              
26             sub num2fin_cardinal :Export {
27 2     2 1 173646 my $positive = shift;
28              
29 2 50 33     27 croak 'You should specify a number from interval [0, 999_999_999]'
      33        
      33        
30             if !defined $positive
31             || $positive !~ m{\A\d+\z}xms
32             || $positive < 0
33             || $positive > 999_999_999;
34              
35 2         11 my @ones = qw(nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän);
36              
37 2 50 33     14 return $ones[$positive] if ($positive >= 0 && $positive < 10);
38 0 0         return 'kymmenen' if ($positive == 10);
39 0 0 0       return $ones[$positive - 10] . 'toista' if ($positive > 10 && $positive < 20);
40              
41 0           my $out;
42             my $remain;
43              
44 0 0 0       if ($positive > 19 && $positive < 100) { # 20 .. 99
    0 0        
    0 0        
    0 0        
    0          
45 0           my $tens_idx = int($positive / 10);
46 0           $remain = $positive % 10;
47              
48 0           $out = $ones[$tens_idx] . 'kymmentä';
49 0 0         $out .= $ones[$remain] if ($remain);
50             }
51             elsif ($positive == 100) { # 100
52 0           $out = 'sata';
53             }
54             elsif ($positive > 100 && $positive < 1000) { # 101 .. 999
55 0           my $hundreds = int($positive / 100);
56 0           $remain = $positive % 100;
57              
58 0 0         $out = $hundreds == 1 ? 'sata' : $ones[$hundreds] . 'sataa';
59 0 0         $out .= num2fin_cardinal($remain) if ($remain);
60             }
61             elsif ($positive >= 1000 && $positive < 1_000_000) { # 1000 .. 999_999
62 0           my $thousands = int($positive / 1000);
63 0           $remain = $positive % 1000;
64              
65 0 0         $out = $thousands == 1 ? 'tuhat' : num2fin_cardinal($thousands) . 'tuhatta';
66 0 0         $out .= num2fin_cardinal($remain) if ($remain);
67             }
68             elsif ($positive >= 1_000_000 && $positive < 1_000_000_000) { # 1_000_000 .. 999_999_999
69 0           my $millions = int($positive / 1_000_000);
70 0           $remain = $positive % 1_000_000;
71              
72 0 0         if ($millions == 1) {
73 0           $out = 'miljoona';
74             }
75             else {
76 0           $out = num2fin_cardinal($millions) . ' miljoonaa';
77             }
78 0 0         $out .= ' ' . num2fin_cardinal($remain) if ($remain);
79             }
80              
81 0           return $out;
82 1     1   9 }
  1         2  
  1         9  
83              
84             # }}}
85              
86             # {{{ num2fin_ordinal convert number to ordinal text
87              
88             sub num2fin_ordinal :Export {
89 0     0 1   my $number = shift;
90              
91 0 0 0       croak 'You should specify a number from interval [1, 999_999_999]'
      0        
      0        
92             if !defined $number
93             || $number !~ m{\A\d+\z}xms
94             || $number < 1
95             || $number > 999_999_999;
96              
97             # In Finnish, ordinals transform ALL components of a compound number.
98             # 1st and 2nd are fully irregular; 3rd onward use the -s suffix
99             # on a (sometimes modified) stem.
100              
101             # Irregular/base ordinal forms for 1-10
102 0           my @ones_ord = (
103             q{}, # 0 unused
104             'ensimmäinen', # 1
105             'toinen', # 2
106             'kolmas', # 3
107             'neljäs', # 4
108             'viides', # 5
109             'kuudes', # 6
110             'seitsemäs', # 7
111             'kahdeksas', # 8
112             'yhdeksäs', # 9
113             );
114              
115             # Ordinal forms used as prefix in compound numbers (stem form)
116             # In compounds like 21st, 1st becomes ensimmäinen, 2nd becomes toinen
117             # but in teens (11-19), 1st->yhdes, 2nd->kahdes
118 0           my @ones_compound = (
119             q{}, # 0
120             'yhdes', # 1 (used in 11th: yhdestoista)
121             'kahdes', # 2 (used in 12th: kahdestoista)
122             'kolmas', # 3
123             'neljäs', # 4
124             'viides', # 5
125             'kuudes', # 6
126             'seitsemäs', # 7
127             'kahdeksas', # 8
128             'yhdeksäs', # 9
129             );
130              
131             # Simple 1-9
132 0 0 0       return $ones_ord[$number] if $number >= 1 && $number <= 9;
133              
134             # 10
135 0 0         return 'kymmenes' if $number == 10;
136              
137             # 11-19: compound_ones + 'toista'
138 0 0 0       if ($number > 10 && $number < 20) {
139 0           return $ones_compound[$number - 10] . 'toista';
140             }
141              
142             # Round tens: 20-90
143 0           my @tens_prefix = (
144             q{}, # 0
145             q{}, # 10 handled above
146             'kahdes', # 20
147             'kolmas', # 30
148             'neljäs', # 40
149             'viides', # 50
150             'kuudes', # 60
151             'seitsemäs', # 70
152             'kahdeksas', # 80
153             'yhdeksäs', # 90
154             );
155              
156 0 0 0       if ($number >= 20 && $number < 100) {
157 0           my $ten_idx = int($number / 10);
158 0           my $remain = $number % 10;
159              
160 0 0         if ($remain == 0) {
161 0           return $tens_prefix[$ten_idx] . 'kymmenes';
162             }
163              
164             # Compound: tens-ordinal + ones-ordinal
165 0           return $tens_prefix[$ten_idx] . 'kymmenes' . $ones_ord[$remain];
166             }
167              
168             # 100-999
169 0 0 0       if ($number >= 100 && $number < 1000) {
170 0           my $hundreds = int($number / 100);
171 0           my $remain = $number % 100;
172              
173 0 0         if ($remain == 0) {
174 0 0         return 'sadas' if $hundreds == 1;
175 0           return $ones_ord[$hundreds] . 'sadas';
176             }
177              
178             # Non-terminal hundreds use cardinal prefix form + "sata"
179 0 0         my $prefix = $hundreds == 1 ? 'sata' : _fin_cardinal_prefix($hundreds) . 'sata';
180              
181 0           return $prefix . num2fin_ordinal($remain);
182             }
183              
184             # 1000-999_999
185 0 0 0       if ($number >= 1000 && $number < 1_000_000) {
186 0           my $thousands = int($number / 1000);
187 0           my $remain = $number % 1000;
188              
189 0 0         if ($remain == 0) {
190 0 0         return 'tuhannes' if $thousands == 1;
191 0           return _fin_cardinal_prefix($thousands) . 'tuhannes';
192             }
193              
194             # Non-terminal thousands use cardinal form
195 0 0         my $prefix = $thousands == 1 ? 'tuhat' : num2fin_cardinal($thousands) . 'tuhatta';
196              
197 0           return $prefix . num2fin_ordinal($remain);
198             }
199              
200             # 1_000_000-999_999_999
201 0 0 0       if ($number >= 1_000_000 && $number < 1_000_000_000) {
202 0           my $millions = int($number / 1_000_000);
203 0           my $remain = $number % 1_000_000;
204              
205 0 0         if ($remain == 0) {
206 0 0         return 'miljoonas' if $millions == 1;
207 0           return _fin_cardinal_prefix($millions) . 'miljoonas';
208             }
209              
210 0           my $prefix;
211 0 0         if ($millions == 1) {
212 0           $prefix = 'miljoona';
213             }
214             else {
215 0           $prefix = num2fin_cardinal($millions) . ' miljoonaa';
216             }
217              
218 0           return $prefix . ' ' . num2fin_ordinal($remain);
219             }
220              
221 0           return;
222 1     1   863 }
  1         2  
  1         8  
223              
224             # }}}
225             # {{{ _fin_cardinal_prefix cardinal form for non-terminal compound prefix
226              
227             sub _fin_cardinal_prefix {
228 0     0     my $n = shift;
229              
230             # For small numbers used as prefixes in ordinal compounds
231 0           my @card = qw(nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän);
232              
233 0 0 0       return $card[$n] if $n >= 1 && $n <= 9;
234              
235 0           return num2fin_cardinal($n);
236             }
237              
238             # }}}
239              
240              
241             # {{{ capabilities declare supported features
242              
243             sub capabilities {
244             return {
245 0     0 1   cardinal => 1,
246             ordinal => 1,
247             };
248             }
249              
250             # }}}
251             1;
252              
253             __END__