File Coverage

blib/lib/Lingua/RUS/Word2Num.pm
Criterion Covered Total %
statement 32 42 76.1
branch 0 4 0.0
condition 2 28 7.1
subroutine 10 11 90.9
pod 3 3 100.0
total 47 88 53.4


line stmt bran cond sub pod time code
1             # For Emacs: -*- mode:cperl; eval: (folding-mode 1); coding:utf-8 -*-
2              
3             package Lingua::RUS::Word2Num;
4             # ABSTRACT: Word to number conversion in Russian
5              
6 1     1   117199 use 5.16.0;
  1         5  
7 1     1   7 use utf8;
  1         4  
  1         14  
8 1     1   76 use warnings;
  1         2  
  1         69  
9              
10             # {{{ use block
11              
12 1     1   619 use Export::Attrs;
  1         9088  
  1         6  
13 1     1   67 use Carp;
  1         2  
  1         53  
14 1     1   1112 use Parse::RecDescent;
  1         38758  
  1         7  
15              
16             # }}}
17             # {{{ variable declarations
18             our $VERSION = '0.2603300';
19             my $parser = ru_numerals();
20              
21             # }}}
22              
23             # {{{ w2n convert text to number
24              
25             sub w2n :Export {
26 7   100 7 1 249874 my $input = shift // return;
27              
28 6         14 $input .= " "; # Grant end space before normalizing
29              
30 6         26 $input =~ s/тысячи /тысяч /g; # Thousand variations. Normalize to тысяч
31 6         18 $input =~ s/тысячa /тысяч /g;
32 6         17 $input =~ s/тысяча /тысяч /g;
33              
34 6         12 $input =~ s/миллиона /миллион /g; # Million variations. Normalize to миллион
35 6         15 $input =~ s/миллионов /миллион /g;
36              
37 6         53 return $parser->numeral($input);
38 1     1   169 }
  1         2  
  1         23  
39              
40             # }}}
41             # {{{ ru_numerals create parser for numerals
42              
43             sub ru_numerals {
44 1     1 1 4 return Parse::RecDescent->new(q{
45             numeral:
46             numeral: million { return $item[1]; } # root parse. go from maximum to minimum value
47             | millenium { return $item[1]; }
48             | century { return $item[1]; }
49             | decade { return $item[1]; }
50             | { return undef; }
51              
52             number: 'девятнадцать ' { $return = 19; } # try to find a word from 0 to 19
53             | 'восемнадцать ' { $return = 18; }
54             | 'семнадцать ' { $return = 17; }
55             | 'шестнадцать ' { $return = 16; }
56             | 'пятнадцать ' { $return = 15; }
57             | 'четырнадцать ' { $return = 14; }
58             | 'тринадцать ' { $return = 13; }
59             | 'двенадцать ' { $return = 12; }
60             | 'одинадцать ' { $return = 11; }
61             | 'десять ' { $return = 10; }
62             | 'девять ' { $return = 9; }
63             | 'восемь ' { $return = 8; }
64             | 'семь ' { $return = 7; }
65             | 'шесть ' { $return = 6; }
66             | 'пять ' { $return = 5; }
67             | 'четыре ' { $return = 4; }
68             | 'три ' { $return = 3; }
69             | 'два ' { $return = 2; }
70             | 'две ' { $return = 2; }
71             | 'одна ' { $return = 1; }
72             | 'один ' { $return = 1; }
73             | 'ноль ' { $return = 0; }
74              
75             tens: 'двадцать ' { $return = 20; } # try to find a word that represents
76             | 'тридцать ' { $return = 30; } # values 20,30,..,90
77             | 'сорок ' { $return = 40; }
78             | 'пятьдесят ' { $return = 50; }
79             | 'шестьдесят ' { $return = 60; }
80             | 'семьдесят ' { $return = 70; }
81             | 'восемьдесят ' { $return = 80; }
82             | 'девяносто ' { $return = 90; }
83              
84             hundreds: 'сто ' { $return = 100; } # try to find a word that represents
85             | 'сотня ' { $return = 100; } # values 200,300,..,900
86             | 'двести ' { $return = 200; }
87             | 'триста ' { $return = 300; }
88             | 'четыреста ' { $return = 400; }
89             | 'пятьсот ' { $return = 500; }
90             | 'шестьсот ' { $return = 600; }
91             | 'семьсот ' { $return = 700; }
92             | 'восемьсот ' { $return = 800; }
93             | 'девятьсот ' { $return = 900; }
94              
95             decade: tens(?) number(?) # try to find words that represents values
96             { $return = -1; # from 0 to 99
97             for (@item) {
98             if (ref $_ && defined $$_[0]) {
99             $return += $$_[0] if ($return != -1);
100             $return = $$_[0] if ($return == -1);
101             }
102             }
103             $return = undef if ($return == -1);
104             }
105              
106             century: hundreds(?) decade(?) # try to find words that represents values
107             { $return = 0; # from 100 to 999
108             for (@item) {
109             $return += $$_[0] if (ref $_ && defined $$_[0]);
110             }
111             $return ||= undef;
112             }
113              
114             millenium: century(?) decade(?) 'тысяч ' century(?) decade(?) # try to find words that represents values
115             { $return = 0; # from 1.000 to 999.999
116             for (@item) {
117             if (ref $_ && defined $$_[0]) {
118             $return += $$_[0];
119             } elsif ($_ eq "тысяч ") {
120             $return = ($return>0) ? $return * 1000 : 1000;
121             }
122             }
123             $return ||= undef;
124             }
125              
126             million: century(?) decade(?) # try to find words that represents values
127             'миллион ' # from 1.000.000 to 999.999.999
128             millenium(?) century(?) decade(?)
129             { $return = 0;
130             for (@item) {
131             if (ref $_ && defined $$_[0]) {
132             $return += $$_[0];
133             } elsif ($_ eq "миллион ") {
134             $return = ($return>0) ? $return * 1000000 : 1000000;
135             }
136             }
137             $return ||= undef;
138             }
139             });
140             }
141              
142             # }}}
143             # {{{ ordinal2cardinal convert ordinal text to cardinal text
144              
145             sub ordinal2cardinal :Export {
146 0   0 0 1   my $input = shift // return;
147              
148             # Russian (Cyrillic) ordinals: strip gender/case suffixes, then map stems.
149             # Inflection: -ый/-ой/-ий/-ая/-ое/-ого/-ому/-ым/-ом.
150              
151 0           my %irregular = (
152             'нулевой' => 'ноль',
153             'первый' => 'один',
154             'второй' => 'два',
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             # Normalize gender/case to masculine nominative
194 0           my $norm = $input;
195 0 0 0       $norm =~ s{(и)его\z}{$1й}xms # третьего → третий
      0        
      0        
      0        
      0        
      0        
      0        
      0        
196             or $norm =~ s{ого\z}{ый}xms # пятого → пятый
197             or $norm =~ s{ому\z}{ый}xms # пятому → пятый
198             or $norm =~ s{ым\z}{ый}xms # пятым → пятый
199             or $norm =~ s{ом\z}{ой}xms # шестом → шестой
200             or $norm =~ s{ая\z}{ый}xms # пятая → пятый
201             or $norm =~ s{яя\z}{ий}xms # третья → третий (gen fem → masc)
202             or $norm =~ s{ое\z}{ый}xms # пятое → пятый
203             or $norm =~ s{ее\z}{ий}xms # третье → третий
204             or $norm =~ s{ей\z}{ий}xms; # третьей → третий
205              
206             # Handle -ой endings that should stay as -ой (второй, шестой, седьмой, восьмой)
207             # They already match as-is in the hash.
208              
209             # Compound ordinal: only last word is ordinal
210 0           my @words = split /\s+/, $norm;
211 0           my $last = pop @words;
212              
213 0 0         if (exists $irregular{$last}) {
214 0           push @words, $irregular{$last};
215 0           return join ' ', @words;
216             }
217              
218 0           return; # not a recognized ordinal
219 1     1   795 }
  1         2  
  1         3  
220              
221             # }}}
222              
223             1;
224              
225             __END__