File Coverage

blib/lib/Lingua/UKR/Word2Num.pm
Criterion Covered Total %
statement 33 48 68.7
branch 0 6 0.0
condition 2 22 9.0
subroutine 10 11 90.9
pod 3 3 100.0
total 48 90 53.3


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8 -*-
2              
3             package Lingua::UKR::Word2Num;
4             # ABSTRACT: Word to number conversion in Ukrainian
5              
6 1     1   130345 use 5.16.0;
  1         4  
7 1     1   6 use utf8;
  1         2  
  1         16  
8 1     1   32 use warnings;
  1         4  
  1         78  
9              
10             # {{{ use block
11              
12 1     1   690 use Export::Attrs;
  1         12981  
  1         8  
13 1     1   90 use Carp;
  1         2  
  1         120  
14 1     1   1347 use Parse::RecDescent;
  1         49703  
  1         6  
15              
16             # }}}
17             # {{{ variable declarations
18             our $VERSION = '0.2603300';
19             my $parser = uk_numerals();
20              
21             # }}}
22              
23             # {{{ w2n convert text to number
24              
25             sub w2n :Export {
26 2   100 2 1 257995 my $input = shift // return;
27              
28 1         4 $input .= " "; # Grant end space before normalizing
29              
30 1         5 $input =~ s/’/'/g; # Normalize Unicode apostrophe to ASCII
31 1         4 $input =~ s/‘/'/g; # (both left and right single quote)
32              
33 1         2 $input =~ s/тисячі /тисяч /g; # Thousand variations. Normalize to тисяч
34 1         3 $input =~ s/тисяча /тисяч /g;
35              
36 1         4 $input =~ s/мільйони /мільйон /g; # Million variations. Normalize to мільйон
37 1         3 $input =~ s/мільйонів /мільйон /g;
38              
39 1         15 return $parser->numeral($input);
40 1     1   199 }
  1         2  
  1         9  
41              
42             # }}}
43             # {{{ uk_numerals create parser for numerals
44              
45             sub uk_numerals {
46 1     1 1 4 return Parse::RecDescent->new(q{
47             numeral:
48             numeral: million { return $item[1]; } # root parse. go from maximum to minimum value
49             | millenium { return $item[1]; }
50             | century { return $item[1]; }
51             | decade { return $item[1]; }
52             | { return undef; }
53              
54             number: /дев'ятнадцять / { $return = 19; } # try to find a word from 0 to 19
55             | 'вісімнадцять ' { $return = 18; }
56             | 'сімнадцять ' { $return = 17; }
57             | 'шістнадцять ' { $return = 16; }
58             | /п'ятнадцять / { $return = 15; }
59             | 'чотирнадцять ' { $return = 14; }
60             | 'тринадцять ' { $return = 13; }
61             | 'дванадцять ' { $return = 12; }
62             | 'одинадцять ' { $return = 11; }
63             | 'десять ' { $return = 10; }
64             | /дев'ять / { $return = 9; }
65             | 'вісім ' { $return = 8; }
66             | 'сім ' { $return = 7; }
67             | 'шість ' { $return = 6; }
68             | /п'ять / { $return = 5; }
69             | 'чотири ' { $return = 4; }
70             | 'три ' { $return = 3; }
71             | 'два ' { $return = 2; }
72             | 'дві ' { $return = 2; }
73             | 'одна ' { $return = 1; }
74             | 'один ' { $return = 1; }
75             | 'нуль ' { $return = 0; }
76              
77             tens: 'двадцять ' { $return = 20; } # try to find a word that represents
78             | 'тридцять ' { $return = 30; } # values 20,30,..,90
79             | 'сорок ' { $return = 40; }
80             | /п'ятдесят / { $return = 50; }
81             | 'шістдесят ' { $return = 60; }
82             | 'сімдесят ' { $return = 70; }
83             | 'вісімдесят ' { $return = 80; }
84             | /дев'яносто / { $return = 90; }
85              
86             hundreds: 'сто ' { $return = 100; } # try to find a word that represents
87             | 'двісті ' { $return = 200; } # values 100,200,..,900
88             | 'триста ' { $return = 300; }
89             | 'чотириста ' { $return = 400; }
90             | /п'ятсот / { $return = 500; }
91             | 'шістсот ' { $return = 600; }
92             | 'сімсот ' { $return = 700; }
93             | 'вісімсот ' { $return = 800; }
94             | /дев'ятсот / { $return = 900; }
95              
96             decade: tens(?) number(?) # try to find words that represents values
97             { $return = -1; # from 0 to 99
98             for (@item) {
99             if (ref $_ && defined $$_[0]) {
100             $return += $$_[0] if ($return != -1);
101             $return = $$_[0] if ($return == -1);
102             }
103             }
104             $return = undef if ($return == -1);
105             }
106              
107             century: hundreds(?) decade(?) # try to find words that represents values
108             { $return = 0; # from 100 to 999
109             for (@item) {
110             $return += $$_[0] if (ref $_ && defined $$_[0]);
111             }
112             $return ||= undef;
113             }
114              
115             millenium: century(?) decade(?) 'тисяч ' century(?) decade(?) # try to find words that represents values
116             { $return = 0; # from 1.000 to 999.999
117             for (@item) {
118             if (ref $_ && defined $$_[0]) {
119             $return += $$_[0];
120             } elsif ($_ eq "тисяч ") {
121             $return = ($return>0) ? $return * 1000 : 1000;
122             }
123             }
124             $return ||= undef;
125             }
126              
127             million: century(?) decade(?) # try to find words that represents values
128             'мільйон ' # from 1.000.000 to 999.999.999
129             millenium(?) century(?) decade(?)
130             { $return = 0;
131             for (@item) {
132             if (ref $_ && defined $$_[0]) {
133             $return += $$_[0];
134             } elsif ($_ eq "мільйон ") {
135             $return = ($return>0) ? $return * 1000000 : 1000000;
136             }
137             }
138             $return ||= undef;
139             }
140             });
141             }
142              
143             # }}}
144             # {{{ ordinal2cardinal convert ordinal text to cardinal text
145              
146             sub ordinal2cardinal :Export {
147 0   0 0 1   my $input = shift // return;
148              
149             # Ukrainian (Cyrillic) ordinals: strip gender/case suffixes, then map stems.
150             # Inflection: -ий/-ій/-а/-е/-ого/-ому/-им/-ім.
151             # Note: apostrophe in п'ятий etc. is normalized to ASCII ' by w2n already;
152             # we accept both Unicode and ASCII apostrophes here.
153              
154 0           my %irregular = (
155             'нульовий' => 'нуль',
156             'перший' => 'один',
157             'другий' => 'два',
158             'третій' => 'три',
159             'четвертий' => 'чотири',
160             "п'ятий" => "п'ять",
161             'шостий' => 'шість',
162             'сьомий' => 'сім',
163             'восьмий' => 'вісім',
164             "дев'ятий" => "дев'ять",
165             'десятий' => 'десять',
166             'одинадцятий' => 'одинадцять',
167             'дванадцятий' => 'дванадцять',
168             'тринадцятий' => 'тринадцять',
169             'чотирнадцятий' => 'чотирнадцять',
170             "п'ятнадцятий" => "п'ятнадцять",
171             'шістнадцятий' => 'шістнадцять',
172             'сімнадцятий' => 'сімнадцять',
173             'вісімнадцятий' => 'вісімнадцять',
174             "дев'ятнадцятий" => "дев'ятнадцять",
175             'двадцятий' => 'двадцять',
176             'тридцятий' => 'тридцять',
177             'сороковий' => 'сорок',
178             "п'ятдесятий" => "п'ятдесят",
179             'шістдесятий' => 'шістдесят',
180             'сімдесятий' => 'сімдесят',
181             'вісімдесятий' => 'вісімдесят',
182             "дев'яностий" => "дев'яносто",
183             'двохсотий' => 'двісті',
184             'трьохсотий' => 'триста',
185             'чотирьохсотий' => 'чотириста',
186             "п'ятисотий" => "п'ятсот",
187             'шестисотий' => 'шістсот',
188             'семисотий' => 'сімсот',
189             'восьмисотий' => 'вісімсот',
190             "дев'ятисотий" => "дев'ятсот",
191             'сотий' => 'сто',
192             'тисячний' => 'тисяч',
193             'мільйонний' => 'мільйон',
194             );
195              
196             # Compound ordinals: ALL components are ordinal forms.
197             # Normalize each word individually, then look up in the mapping.
198 0           my @words = split /\s+/, $input;
199 0           my @result;
200 0           my $matched = 0;
201              
202 0           for my $word (@words) {
203             # Normalize Unicode apostrophes to ASCII
204 0           $word =~ s/\x{2018}/'/g;
205 0           $word =~ s/\x{2019}/'/g;
206              
207             # Normalize gender/case to masculine nominative
208 0           my $norm = $word;
209 0 0 0       $norm =~ s{(і)його\z}{$1й}xms # третього → третій
      0        
      0        
      0        
      0        
      0        
210             or $norm =~ s{ого\z}{ий}xms # п'ятого → п'ятий
211             or $norm =~ s{ому\z}{ий}xms # п'ятому → п'ятий
212             or $norm =~ s{им\z}{ий}xms # п'ятим → п'ятий
213             or $norm =~ s{ім\z}{ій}xms # третім → третій
214             or $norm =~ s{а\z}{ий}xms # п'ята → п'ятий
215             or $norm =~ s{е\z}{ий}xms # п'яте → п'ятий
216             or $norm =~ s{є\z}{ій}xms; # третє → третій
217              
218 0 0         if (exists $irregular{$norm}) {
219 0           push @result, $irregular{$norm};
220 0           $matched = 1;
221             }
222             else {
223 0           push @result, $word; # pass through unchanged (connectors, etc.)
224             }
225             }
226              
227 0 0         return $matched ? join(' ', @result) : undef;
228 1     1   721 }
  1         4  
  1         4  
229              
230             # }}}
231              
232             1;
233              
234             __END__