File Coverage

blib/lib/Lingua/LAT/Word2Num.pm
Criterion Covered Total %
statement 23 57 40.3
branch 0 18 0.0
condition 2 7 28.5
subroutine 9 10 90.0
pod 3 3 100.0
total 37 95 38.9


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8 -*-
2             package Lingua::LAT::Word2Num;
3             # ABSTRACT: Word to number conversion in Latin
4              
5 1     1   124593 use 5.16.0;
  1         25  
6 1     1   8 use utf8;
  1         3  
  1         18  
7 1     1   36 use warnings;
  1         2  
  1         84  
8              
9             # {{{ use block
10              
11 1     1   700 use Export::Attrs;
  1         14177  
  1         9  
12 1     1   1484 use Parse::RecDescent;
  1         56218  
  1         12  
13              
14             # }}}
15             # {{{ var block
16             our $VERSION = '0.2603300';
17             my $parser = lat_numerals();
18              
19             # }}}
20              
21             # {{{ w2n convert text to number
22              
23             sub w2n :Export {
24 31   100 31 1 732003 my $input = shift // return;
25              
26 30         332 return $parser->numeral($input);
27 1     1   163 }
  1         24  
  1         10  
28              
29             # }}}
30             # {{{ lat_numerals create parser for latin numerals
31              
32             sub lat_numerals {
33 1     1 1 7 return Parse::RecDescent->new(q{
34            
35              
36             numeral: kOhOd
37             | { }
38              
39             number: 'quattuordecim' { 14 }
40             | 'quattuor' { 4 }
41             | 'quindecim' { 15 }
42             | 'quinque' { 5 }
43             | 'tredecim' { 13 }
44             | 'duodeviginti' { 18 }
45             | 'undeviginti' { 19 }
46             | 'duodecim' { 12 }
47             | 'undecim' { 11 }
48             | 'septendecim' { 17 }
49             | 'sedecim' { 16 }
50             | 'nulla' { 0 }
51             | 'unus' { 1 }
52             | 'duo' { 2 }
53             | 'tres' { 3 }
54             | 'sex' { 6 }
55             | 'septem' { 7 }
56             | 'octo' { 8 }
57             | 'novem' { 9 }
58             | 'decem' { 10 }
59              
60             tens: 'viginti' { 20 }
61             | 'triginta' { 30 }
62             | 'quadraginta' { 40 }
63             | 'quinquaginta' { 50 }
64             | 'sexaginta' { 60 }
65             | 'septuaginta' { 70 }
66             | 'octoginta' { 80 }
67             | 'nonaginta' { 90 }
68              
69             # Subtractive forms: duode/unde + next decade (written as one word)
70             subtractive: 'duodetriginta' { 28 }
71             | 'undetriginta' { 29 }
72             | 'duodequadraginta' { 38 }
73             | 'undequadraginta' { 39 }
74             | 'duodequinquaginta' { 48 }
75             | 'undequinquaginta' { 49 }
76             | 'duodesexaginta' { 58 }
77             | 'undesexaginta' { 59 }
78             | 'duodeseptuaginta' { 68 }
79             | 'undeseptuaginta' { 69 }
80             | 'duodeoctoginta' { 78 }
81             | 'undeoctoginta' { 79 }
82             | 'duodenonaginta' { 88 }
83             | 'undenonaginta' { 89 }
84             | 'undecentum' { 99 }
85              
86             deca: tens number { $item[1] + $item[2] }
87             | subtractive
88             | tens
89             | number
90              
91             hecto: /ducenti/ deca { 200 + $item[2] }
92             | /ducenti/ { 200 }
93             | /trecenti/ deca { 300 + $item[2] }
94             | /trecenti/ { 300 }
95             | /quadringenti/ deca { 400 + $item[2] }
96             | /quadringenti/ { 400 }
97             | /quingenti/ deca { 500 + $item[2] }
98             | /quingenti/ { 500 }
99             | /sescenti/ deca { 600 + $item[2] }
100             | /sescenti/ { 600 }
101             | /septingenti/ deca { 700 + $item[2] }
102             | /septingenti/ { 700 }
103             | /octingenti/ deca { 800 + $item[2] }
104             | /octingenti/ { 800 }
105             | /nongenti/ deca { 900 + $item[2] }
106             | /nongenti/ { 900 }
107             | 'centum' deca { 100 + $item[2] }
108             | 'centum' { 100 }
109              
110             hOd: hecto
111             | deca
112              
113             kilo: hOd 'milia' hOd { $item[1] * 1000 + $item[3] }
114             | hOd 'milia' { $item[1] * 1000 }
115             | 'mille' hOd { 1000 + $item[2] }
116             | 'mille' { 1000 }
117              
118             kOhOd: kilo
119             | hOd
120             });
121             }
122              
123             # }}}
124              
125             # {{{ ordinal2cardinal convert ordinal text to cardinal text
126              
127             sub ordinal2cardinal :Export {
128 0   0 0 1   my $input = shift // return;
129              
130             # Latin ordinals are entirely table-driven with suppletive stems.
131             # Includes subtractive forms (duodevicesimus → duodeviginti).
132             # Compounds: space-separated, each part is converted.
133              
134 0           my %ordinal_to_cardinal = (
135             # Units
136             'primus' => 'unus',
137             'prima' => 'unus',
138             'primum' => 'unus',
139             'secundus' => 'duo',
140             'secunda' => 'duo',
141             'secundum' => 'duo',
142             'tertius' => 'tres',
143             'tertia' => 'tres',
144             'tertium' => 'tres',
145             'quartus' => 'quattuor',
146             'quarta' => 'quattuor',
147             'quartum' => 'quattuor',
148             'quintus' => 'quinque',
149             'quinta' => 'quinque',
150             'quintum' => 'quinque',
151             'sextus' => 'sex',
152             'sexta' => 'sex',
153             'sextum' => 'sex',
154             'septimus' => 'septem',
155             'septima' => 'septem',
156             'septimum' => 'septem',
157             'octavus' => 'octo',
158             'octava' => 'octo',
159             'octavum' => 'octo',
160             'nonus' => 'novem',
161             'nona' => 'novem',
162             'nonum' => 'novem',
163             'decimus' => 'decem',
164             'decima' => 'decem',
165             'decimum' => 'decem',
166             # Teens
167             'undecimus' => 'undecim',
168             'duodecimus' => 'duodecim',
169             # Subtractive (both -censimus and -cesimus spellings)
170             'duodevicesimus' => 'duodeviginti',
171             'undevicesimus' => 'undeviginti',
172             'duodetricensimus' => 'duodetriginta',
173             'duodetricesimus' => 'duodetriginta',
174             'undetricensimus' => 'undetriginta',
175             'undetricesimus' => 'undetriginta',
176             'duodequadragesimus' => 'duodequadraginta',
177             'undequadragesimus' => 'undequadraginta',
178             'duodequinquagesimus' => 'duodequinquaginta',
179             'undequinquagesimus' => 'undequinquaginta',
180             'duodesexagesimus' => 'duodesexaginta',
181             'undesexagesimus' => 'undesexaginta',
182             'duodeseptuagesimus' => 'duodeseptuaginta',
183             'undeseptuagesimus' => 'undeseptuaginta',
184             'duodeoctogesimus' => 'duodeoctoginta',
185             'undeoctogesimus' => 'undeoctoginta',
186             'duodenonagesimus' => 'duodenonaginta',
187             'undenonagesimus' => 'undenonaginta',
188             'duodecentesimus' => 'nonaginta octo',
189             'undecentesimus' => 'undecentum',
190             # Tens (both -censimus/-gesimus and -cesimus spellings)
191             'vicesimus' => 'viginti',
192             'vicensimus' => 'viginti',
193             'tricensimus' => 'triginta',
194             'trigesimus' => 'triginta',
195             'tricesimus' => 'triginta',
196             'quadragesimus' => 'quadraginta',
197             'quinquagesimus' => 'quinquaginta',
198             'sexagesimus' => 'sexaginta',
199             'septuagesimus' => 'septuaginta',
200             'octogesimus' => 'octoginta',
201             'nonagesimus' => 'nonaginta',
202             # Hundreds
203             'centesimus' => 'centum',
204             'ducentesimus' => 'ducenti',
205             'trecentesimus' => 'trecenti',
206             'quadringentesimus' => 'quadringenti',
207             'quingentesimus' => 'quingenti',
208             'sescentesimus' => 'sescenti',
209             'septingentesimus' => 'septingenti',
210             'octingentesimus' => 'octingenti',
211             'nongentesimus' => 'nongenti',
212             # Thousands
213             'millesimus' => 'mille',
214             );
215              
216             # Compound teen ordinals (13-17): two-word ordinal → fused cardinal
217             # e.g. "tertius decimus" → "tredecim" (not "tres decem")
218 0           my %compound_teens = (
219             'tertius decimus' => 'tredecim',
220             'tertia decima' => 'tredecim',
221             'quartus decimus' => 'quattuordecim',
222             'quarta decima' => 'quattuordecim',
223             'quintus decimus' => 'quindecim',
224             'quinta decima' => 'quindecim',
225             'sextus decimus' => 'sedecim',
226             'sexta decima' => 'sedecim',
227             'septimus decimus' => 'septendecim',
228             'septima decima' => 'septendecim',
229             );
230 0 0         return $compound_teens{$input} if exists $compound_teens{$input};
231              
232             # Compound: "centesimus quartus" → convert each part.
233             # In Latin compound ordinals, each component is an ordinal form.
234             # However, for thousands like "quinque millesimus", the multiplier
235             # ("quinque") is already cardinal — pass it through if not recognized.
236             # For compounds ending in a two-word teen ordinal, handle the last two words together.
237 0 0         if ($input =~ m{\s}xms) {
238 0           my @words = split /\s+/, $input;
239              
240             # Check if last two words form a compound teen ordinal
241 0 0         if (@words >= 2) {
242 0           my $last_two = $words[-2] . ' ' . $words[-1];
243 0 0         if (exists $compound_teens{$last_two}) {
244 0           pop @words; pop @words;
  0            
245 0           my @cardinals;
246 0           my $any = 0;
247 0           for my $word (@words) {
248 0           my $card = ordinal2cardinal($word);
249 0 0         if (defined $card) {
250 0           push @cardinals, $card;
251 0           $any = 1;
252             } else {
253 0           push @cardinals, $word; # pass through cardinal multipliers
254             }
255             }
256 0           push @cardinals, $compound_teens{$last_two};
257 0           return join ' ', @cardinals;
258             }
259             }
260              
261 0           my @cardinals;
262 0           my $any = 0;
263 0           for my $word (@words) {
264 0           my $card = ordinal2cardinal($word);
265 0 0         if (defined $card) {
266             # "mille" after a multiplier must become "milia" (plural)
267 0 0 0       if ($card eq 'mille' && @cardinals > 0) {
268 0           $card = 'milia';
269             }
270 0           push @cardinals, $card;
271 0           $any = 1;
272             } else {
273 0           push @cardinals, $word; # pass through cardinal multipliers
274             }
275             }
276 0 0         return $any ? join(' ', @cardinals) : undef;
277             }
278              
279 0 0         return $ordinal_to_cardinal{$input} if exists $ordinal_to_cardinal{$input};
280              
281 0           return; # not an ordinal
282 1     1   1270 }
  1         3  
  1         6  
283              
284             # }}}
285              
286             1;
287              
288             __END__