File Coverage

blib/lib/Lingua/SPA/Word2Num.pm
Criterion Covered Total %
statement 23 40 57.5
branch 0 8 0.0
condition 2 6 33.3
subroutine 9 10 90.0
pod 3 3 100.0
total 37 67 55.2


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8; -*-
2              
3             package Lingua::SPA::Word2Num;
4             # ABSTRACT: Word to number conversion in Spanish
5              
6 1     1   96580 use 5.16.0;
  1         3  
7 1     1   5 use utf8;
  1         2  
  1         12  
8 1     1   21 use warnings;
  1         1  
  1         59  
9              
10             # {{{ use block
11              
12 1     1   1071 use Parse::RecDescent;
  1         35192  
  1         8  
13 1     1   1095 use Export::Attrs;
  1         9115  
  1         6  
14              
15             # }}}
16             # {{{ var block
17             our $VERSION = '0.2603300';
18             my $parser = spa_numerals();
19              
20             # }}}
21              
22             # {{{ w2n convert text to number
23              
24             sub w2n :Export {
25 4   100 4 1 238640 my $input = shift // return;
26              
27 3         33 return $parser->numeral($input);
28 1     1   116 }
  1         4  
  1         5  
29             # }}}
30             # {{{ spa_numerals create parser for numerals
31              
32             sub spa_numerals {
33 1     1 1 9 return Parse::RecDescent->new(q{
34            
35            
36              
37             numeral: mega
38             | kOhOd
39             | 'cero' { 0 }
40             | { }
41              
42             number: 'un' { 1 }
43             | 'dos' { 2 }
44             | 'tres' { 3 }
45             | 'cuatro' { 4 }
46             | 'cinco' { 5 }
47             | 'seis' { 6 }
48             | 'siete' { 7 }
49             | 'ocho' { 8 }
50             | 'nueve' { 9 }
51             | 'diez' { 10 }
52             | 'once' { 11 }
53             | 'doce' { 12 }
54             | 'trece' { 13 }
55             | 'catorce' { 14 }
56             | 'quince' { 15 }
57             | 'dieciséis' { 16 }
58             | 'diecisiete' { 17 }
59             | 'dieciocho' { 18 }
60             | 'diecinueve' { 19 }
61             | 'veinte' { 20 }
62             | 'veintiun' { 21 }
63             | 'veintidós' { 22 }
64             | 'veintitrés' { 23 }
65             | 'veinticuatro' { 24 }
66             | 'veinticinco' { 25 }
67             | 'veintiséis' { 26 }
68             | 'veintisiete' { 27 }
69             | 'veintiocho' { 28 }
70             | 'veintinueve' { 29 }
71              
72             tens: 'treinta' { 30 }
73             | 'cuarenta' { 40 }
74             | 'cincuenta' { 50 }
75             | 'sesenta' { 60 }
76             | 'setenta' { 70 }
77             | 'ochenta' { 80 }
78             | 'noventa' { 90 }
79              
80             hundreds: 'quinientos' { 500 }
81             | 'setecientos' { 700 }
82             | 'novecientos' { 900 }
83             | number /cientos?/ { $item[1] * 100 }
84             | /cien(to)?/ { 100 }
85              
86             deca: tens 'y' number { $item[1] + $item[3] }
87             | tens
88             | number
89              
90             hecto: hundreds deca { $item[1] + $item[2] }
91             | hundreds
92              
93             hOd: hecto
94             | deca
95              
96             kilo: hOd milnotmeg hOd { $item[1] * 1000 + $item[3] }
97             | hOd milnotmeg { $item[1] * 1000 }
98             | milnotmeg hOd { 1000 + $item[2] }
99             | milnotmeg { 1000 }
100              
101             milnotmeg: ...!'mill' 'mil'
102              
103             kOhOd: kilo
104             | hOd
105              
106             mega: hOd /mill(ones|ón)/ kOhOd { $item[1] * 1_000_000 + $item[3] }
107             | hOd /mill(ones|ón)/ { $item[1] * 1_000_000 }
108             });
109             }
110              
111             # }}}
112              
113             # {{{ ordinal2cardinal convert ordinal text to cardinal text
114              
115             sub ordinal2cardinal :Export {
116 0   0 0 1   my $input = shift // return;
117              
118             # Spanish ordinals have gender: -o (masc), -a (fem), and sometimes
119             # apocopated forms (primer, tercer). All unique stems.
120             # For 11+, compositional ordinals exist (undécimo, duodécimo, vigésimo, etc.)
121             # Compounds: "vigésimo primero" → convert each part.
122              
123 0           my %ordinal_to_cardinal = (
124             # Units (masculine, feminine, apocopated)
125             'primero' => 'un',
126             'primera' => 'un',
127             'primer' => 'un',
128             'segundo' => 'dos',
129             'segunda' => 'dos',
130             'tercero' => 'tres',
131             'tercera' => 'tres',
132             'tercer' => 'tres',
133             'cuarto' => 'cuatro',
134             'cuarta' => 'cuatro',
135             'quinto' => 'cinco',
136             'quinta' => 'cinco',
137             'sexto' => 'seis',
138             'sexta' => 'seis',
139             'séptimo' => 'siete',
140             'séptima' => 'siete',
141             'octavo' => 'ocho',
142             'octava' => 'ocho',
143             'noveno' => 'nueve',
144             'novena' => 'nueve',
145             'décimo' => 'diez',
146             'décima' => 'diez',
147             # Teens (11-19)
148             'undécimo' => 'once',
149             'undécima' => 'once',
150             'duodécimo' => 'doce',
151             'duodécima' => 'doce',
152             'decimotercero' => 'trece',
153             'decimotercera' => 'trece',
154             'decimocuarto' => 'catorce',
155             'decimocuarta' => 'catorce',
156             'decimoquinto' => 'quince',
157             'decimoquinta' => 'quince',
158             'decimosexto' => 'dieciséis',
159             'decimosexta' => 'dieciséis',
160             'decimoséptimo' => 'diecisiete',
161             'decimoséptima' => 'diecisiete',
162             'decimoctavo' => 'dieciocho',
163             'decimoctava' => 'dieciocho',
164             'decimonoveno' => 'diecinueve',
165             'decimonovena' => 'diecinueve',
166             # Fused twenties (21-29): Num2Word produces "vigesimoprimero" etc.
167             'vigesimoprimero' => 'veintiun',
168             'vigesimoprimera' => 'veintiun',
169             'vigesimoprimer' => 'veintiun',
170             'vigesimosegundo' => 'veintidós',
171             'vigesimosegunda' => 'veintidós',
172             'vigesimotercero' => 'veintitrés',
173             'vigesimotercera' => 'veintitrés',
174             'vigesimotercer' => 'veintitrés',
175             'vigesimocuarto' => 'veinticuatro',
176             'vigesimocuarta' => 'veinticuatro',
177             'vigesimoquinto' => 'veinticinco',
178             'vigesimoquinta' => 'veinticinco',
179             'vigesimosexto' => 'veintiséis',
180             'vigesimosexta' => 'veintiséis',
181             'vigesimoséptimo' => 'veintisiete',
182             'vigesimoséptima' => 'veintisiete',
183             'vigesimooctavo' => 'veintiocho',
184             'vigesimooctava' => 'veintiocho',
185             'vigesimoctavo' => 'veintiocho',
186             'vigesimoctava' => 'veintiocho',
187             'vigesimonoveno' => 'veintinueve',
188             'vigesimonovena' => 'veintinueve',
189             # Tens
190             'vigésimo' => 'veinte',
191             'vigésima' => 'veinte',
192             'trigésimo' => 'treinta',
193             'trigésima' => 'treinta',
194             'cuadragésimo' => 'cuarenta',
195             'cuadragésima' => 'cuarenta',
196             'quincuagésimo' => 'cincuenta',
197             'quincuagésima' => 'cincuenta',
198             'sexagésimo' => 'sesenta',
199             'sexagésima' => 'sesenta',
200             'septuagésimo' => 'setenta',
201             'septuagésima' => 'setenta',
202             'octogésimo' => 'ochenta',
203             'octogésima' => 'ochenta',
204             'nonagésimo' => 'noventa',
205             'nonagésima' => 'noventa',
206             # Hundreds
207             'centésimo' => 'cien',
208             'centésima' => 'cien',
209             'ducentésimo' => 'dos cientos',
210             'ducentésima' => 'dos cientos',
211             'tricentésimo' => 'tres cientos',
212             'tricentésima' => 'tres cientos',
213             'cuadringentésimo' => 'cuatro cientos',
214             'cuadringentésima' => 'cuatro cientos',
215             'quingentésimo' => 'quinientos',
216             'quingentésima' => 'quinientos',
217             'sexcentésimo' => 'seis cientos',
218             'sexcentésima' => 'seis cientos',
219             'septingentésimo' => 'siete cientos',
220             'septingentésima' => 'siete cientos',
221             'octingentésimo' => 'ocho cientos',
222             'octingentésima' => 'ocho cientos',
223             'noningentésimo' => 'nueve cientos',
224             'noningentésima' => 'nueve cientos',
225             # Thousands
226             'milésimo' => 'mil',
227             'milésima' => 'mil',
228             'millonésimo' => 'un millón',
229             'millonésima' => 'un millón',
230             );
231              
232             # Compound: "vigésimo primero" → convert each part
233             # For 30+, the parser expects "tens y number" (e.g. "treinta y un"),
234             # so we insert "y" between the tens and units cardinal parts.
235 0 0         if ($input =~ m{\s}xms) {
236 0           my @words = split /\s+/, $input;
237 0           my @cardinals;
238 0           for my $word (@words) {
239 0   0       my $card = ordinal2cardinal($word) // return;
240 0           push @cardinals, $card;
241             }
242              
243             # Insert "y" between tens (30+) and units in the final position.
244             # Works for both 2-word ("trigésimo primero" → "treinta y un")
245             # and 3-word ("centésimo trigésimo primero" → "cien treinta y un").
246 0 0         if (@cardinals >= 2) {
247 0           my %tens_needing_y = map { $_ => 1 }
  0            
248             qw(treinta cuarenta cincuenta sesenta setenta ochenta noventa);
249 0           my $pen = $cardinals[-2]; # penultimate
250 0 0         if ($tens_needing_y{$pen}) {
251 0           splice @cardinals, -1, 0, 'y';
252             }
253             }
254              
255 0           return join ' ', @cardinals;
256             }
257              
258 0 0         return $ordinal_to_cardinal{$input} if exists $ordinal_to_cardinal{$input};
259              
260 0           return; # not an ordinal
261 1     1   734 }
  1         1  
  1         5  
262              
263             # }}}
264              
265             1;
266              
267             __END__