File Coverage

blib/lib/Lingua/MKD/Word2Num.pm
Criterion Covered Total %
statement 29 48 60.4
branch 0 8 0.0
condition 2 25 8.0
subroutine 10 11 90.9
pod 3 3 100.0
total 44 95 46.3


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; mode:folding; coding:utf-8; -*-
2              
3             package Lingua::MKD::Word2Num;
4             # ABSTRACT: Word to number conversion in Macedonian
5              
6 1     1   131246 use 5.16.0;
  1         5  
7 1     1   7 use utf8;
  1         3  
  1         17  
8 1     1   38 use warnings;
  1         4  
  1         143  
9              
10             # {{{ use block
11              
12 1     1   955 use Export::Attrs;
  1         13964  
  1         8  
13 1     1   93 use Carp;
  1         3  
  1         97  
14 1     1   1361 use Parse::RecDescent;
  1         53629  
  1         10  
15              
16             # }}}
17             # {{{ variable declarations
18             our $VERSION = '0.2603300';
19             my $parser = mkd_numerals();
20              
21             # }}}
22              
23             # {{{ w2n convert text to number
24              
25             sub w2n :Export {
26 31   100 31 1 1223571 my $input = shift // return;
27              
28 30         96 $input .= " "; # Grant end space before normalizing
29              
30 30         138 $input =~ s/илјади /илјада /g; # Thousand variations. Normalize to илјада
31 30         85 $input =~ s/милиони /милион /g; # Million variations. Normalize to милион
32              
33 30         341 return $parser->numeral($input);
34 1     1   268 }
  1         2  
  1         8  
35              
36             # }}}
37             # {{{ mkd_numerals create parser for numerals
38              
39             sub mkd_numerals {
40 1     1 1 10 return Parse::RecDescent->new(q{
41             numeral:
42             numeral: million { return $item[1]; } # root parse. go from maximum to minimum value
43             | millenium { return $item[1]; }
44             | century { return $item[1]; }
45             | decade { return $item[1]; }
46             | { return undef; }
47              
48             number: 'деветнаесет ' { $return = 19; } # try to find a word from 0 to 19
49             | 'осумнаесет ' { $return = 18; }
50             | 'седумнаесет ' { $return = 17; }
51             | 'шестнаесет ' { $return = 16; }
52             | 'петнаесет ' { $return = 15; }
53             | 'четиринаесет ' { $return = 14; }
54             | 'тринаесет ' { $return = 13; }
55             | 'дванаесет ' { $return = 12; }
56             | 'единаесет ' { $return = 11; }
57             | 'десет ' { $return = 10; }
58             | 'девет ' { $return = 9; }
59             | 'осум ' { $return = 8; }
60             | 'седум ' { $return = 7; }
61             | 'шест ' { $return = 6; }
62             | 'пет ' { $return = 5; }
63             | 'четири ' { $return = 4; }
64             | 'три ' { $return = 3; }
65             | 'две ' { $return = 2; }
66             | 'два ' { $return = 2; }
67             | 'еден ' { $return = 1; }
68             | 'една ' { $return = 1; }
69             | 'нула ' { $return = 0; }
70              
71             tens: 'дваесет ' { $return = 20; } # try to find a word that represents
72             | 'триесет ' { $return = 30; } # values 20,30,..,90
73             | 'четириесет ' { $return = 40; }
74             | 'педесет ' { $return = 50; }
75             | 'шеесет ' { $return = 60; }
76             | 'седумдесет ' { $return = 70; }
77             | 'осумдесет ' { $return = 80; }
78             | 'деведесет ' { $return = 90; }
79              
80             hundreds: 'деветстотини ' { $return = 900; } # try to find a word that represents
81             | 'осумстотини ' { $return = 800; } # values 100,200,..,900
82             | 'седумстотини ' { $return = 700; }
83             | 'шестстотини ' { $return = 600; }
84             | 'петстотини ' { $return = 500; }
85             | 'четиристотини ' { $return = 400; }
86             | 'триста ' { $return = 300; }
87             | 'двесте ' { $return = 200; }
88             | 'сто ' { $return = 100; }
89              
90             decade: tens 'и ' number # tens и units (e.g. дваесет и три)
91             { $return = $item[1] + $item[3]; }
92             | tens # plain tens (e.g. педесет)
93             { $return = $item[1]; }
94             | number # plain number 0-19
95             { $return = $item[1]; }
96              
97             century: hundreds 'и ' decade(?) # hundreds "и" decade (for remainder < 20 or tens only)
98             { $return = 0;
99             for (@item) {
100             if (ref $_ && defined $$_[0]) {
101             $return += $$_[0];
102             } elsif (!ref $_ && $_ =~ /^\d+$/) {
103             $return += $_;
104             }
105             }
106             $return ||= undef;
107             }
108             | hundreds tens 'и ' number # hundreds tens "и" units
109             { $return = 0;
110             for (@item) {
111             if (ref $_ && defined $$_[0]) {
112             $return += $$_[0];
113             } elsif (!ref $_ && $_ =~ /^\d+$/) {
114             $return += $_;
115             }
116             }
117             $return ||= undef;
118             }
119             | hundreds # plain hundreds (e.g. двесте)
120             { $return = $item[1]; }
121              
122             millenium: century(?) decade(?) 'илјада ' 'и ' century # thousand "и" hundred (e.g. илјада и сто)
123             { $return = 0;
124             for (@item) {
125             if (ref $_ && defined $$_[0]) {
126             $return += $$_[0];
127             } elsif (!ref $_ && $_ =~ /^\d+$/) {
128             $return += $_;
129             } elsif ($_ eq "\x{0438}\x{043b}\x{0458}\x{0430}\x{0434}\x{0430} ") {
130             $return = ($return>0) ? $return * 1000 : 1000;
131             }
132             }
133             $return ||= undef;
134             }
135             | century(?) decade(?) 'илјада ' 'и ' decade # thousand "и" remainder (e.g. илјада и еден)
136             { $return = 0;
137             for (@item) {
138             if (ref $_ && defined $$_[0]) {
139             $return += $$_[0];
140             } elsif (!ref $_ && $_ =~ /^\d+$/) {
141             $return += $_;
142             } elsif ($_ eq "\x{0438}\x{043b}\x{0458}\x{0430}\x{0434}\x{0430} ") {
143             $return = ($return>0) ? $return * 1000 : 1000;
144             }
145             }
146             $return ||= undef;
147             }
148             | century(?) decade(?) 'илјада ' century(?) decade(?) # thousand with hundreds remainder
149             { $return = 0;
150             for (@item) {
151             if (ref $_ && defined $$_[0]) {
152             $return += $$_[0];
153             } elsif ($_ eq "\x{0438}\x{043b}\x{0458}\x{0430}\x{0434}\x{0430} ") {
154             $return = ($return>0) ? $return * 1000 : 1000;
155             }
156             }
157             $return ||= undef;
158             }
159              
160             million: century(?) decade(?) # try to find words that represents values
161             'милион ' # from 1.000.000 to 999.999.999
162             millenium(?) century(?) decade(?)
163             { $return = 0;
164             for (@item) {
165             if (ref $_ && defined $$_[0]) {
166             $return += $$_[0];
167             } elsif ($_ eq "\x{043c}\x{0438}\x{043b}\x{0438}\x{043e}\x{043d} ") {
168             $return = ($return>0) ? $return * 1000000 : 1000000;
169             }
170             }
171             $return ||= undef;
172             }
173             });
174             }
175              
176             # }}}
177             # {{{ ordinal2cardinal convert ordinal text to cardinal text
178              
179             sub ordinal2cardinal :Export {
180 0   0 0 1   my $input = shift // return;
181              
182             # Macedonian (Cyrillic) ordinals: strip gender/definiteness suffixes, then map stems.
183             # Masc indef: -и, fem: -а, neut: -о, masc definite: -от/-иот.
184             # Stem-based lookup after stripping the single-char suffix.
185              
186 0           my %stem_to_cardinal = (
187             'прв' => 'еден',
188             'втор' => 'два',
189             'трет' => 'три',
190             'четврт' => 'четири',
191             'петт' => 'пет',
192             'шест' => 'шест',
193             'седм' => 'седум',
194             'осм' => 'осум',
195             'деветт' => 'девет',
196             'десетт' => 'десет',
197             'единаесетт' => 'единаесет',
198             'дванаесетт' => 'дванаесет',
199             'тринаесетт' => 'тринаесет',
200             'четиринаесетт' => 'четиринаесет',
201             'петнаесетт' => 'петнаесет',
202             'шестнаесетт' => 'шестнаесет',
203             'седумнаесетт' => 'седумнаесет',
204             'осумнаесетт' => 'осумнаесет',
205             'деветнаесетт' => 'деветнаесет',
206             'дваесетт' => 'дваесет',
207             'триесетт' => 'триесет',
208             'четириесетт' => 'четириесет',
209             'педесетт' => 'педесет',
210             'шеесетт' => 'шеесет',
211             'седумдесетт' => 'седумдесет',
212             'осумдесетт' => 'осумдесет',
213             'деведесетт' => 'деведесет',
214             'двестот' => 'двесте',
215             'тристот' => 'триста',
216             'четиристотинит' => 'четиристотини',
217             'петстотинит' => 'петстотини',
218             'шестстотинит' => 'шестстотини',
219             'седумстотинит' => 'седумстотини',
220             'осумстотинит' => 'осумстотини',
221             'деветстотинит' => 'деветстотини',
222             'стот' => 'сто',
223             'илјадит' => 'илјада',
224             'милионт' => 'милион',
225             'нулт' => 'нула',
226             );
227              
228             # Hundreds cardinals that require "и" connector in cardinal form
229 0           my %is_hundred = map { $_ => 1 } qw(
  0            
230             сто двесте триста четиристотини петстотини
231             шестстотини седумстотини осумстотини деветстотини
232             );
233              
234             # Compound ordinals: ALL components are ordinal forms.
235             # Normalize each word individually, then look up in the mapping.
236 0           my @words = split /\s+/, $input;
237 0           my @result;
238 0           my $matched = 0;
239              
240 0           for my $word (@words) {
241             # Strip gender/definiteness suffixes to get ordinal stem
242 0           my $norm = $word;
243 0 0 0       $norm =~ s{иот\z}{}xms # masc definite: првиот → прв
      0        
      0        
      0        
      0        
244             or $norm =~ s{от\z}{}xms # masc definite short: првот → прв
245             or $norm =~ s{ата\z}{}xms # fem definite: првата → прв
246             or $norm =~ s{ото\z}{}xms # neut definite: првото → прв
247             or $norm =~ s{и\z}{}xms # masc indef: први → прв
248             or $norm =~ s{а\z}{}xms # fem indef: прва → прв
249             or $norm =~ s{о\z}{}xms; # neut indef: прво → прв
250              
251 0 0         if (exists $stem_to_cardinal{$norm}) {
252 0           push @result, $stem_to_cardinal{$norm};
253 0           $matched = 1;
254             }
255             else {
256 0           push @result, $word; # pass through unchanged (connectors like "и")
257             }
258             }
259              
260 0 0         return undef unless $matched;
261              
262             # Macedonian cardinals require "и" between hundreds and what follows
263             # when there's a single-value remainder (unit/teen/round-tens).
264             # Insert "и" after hundreds token when no "и" follows yet.
265 0 0 0       if (@result >= 2 && exists $is_hundred{$result[0]}
      0        
266 0           && !grep { $_ eq 'и' } @result) {
267 0           splice @result, 1, 0, 'и';
268             }
269              
270 0           return join(' ', @result);
271 1     1   1216 }
  1         3  
  1         5  
272              
273             # }}}
274              
275             1;
276              
277             __END__