File Coverage

blib/lib/Lingua/ID/Words2Nums.pm
Criterion Covered Total %
statement 104 110 94.5
branch 42 60 70.0
condition 9 18 50.0
subroutine 13 13 100.0
pod 2 2 100.0
total 170 203 83.7


line stmt bran cond sub pod time code
1             package Lingua::ID::Words2Nums;
2              
3             our $DATE = '2015-09-03'; # DATE
4             our $VERSION = '0.17'; # VERSION
5              
6 1     1   13442 use 5.010001;
  1         2  
7 1     1   2 use strict;
  1         1  
  1         16  
8 1     1   2 use warnings;
  1         1  
  1         55  
9             #use Log::Any qw($log);
10              
11             our %SPEC;
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(words2nums words2nums_simple $Pat);
16              
17 1     1   378 use Parse::Number::ID qw(parse_number_id);
  1         444  
  1         55  
18 1     1   4 use Scalar::Util qw(looks_like_number);
  1         1  
  1         1271  
19              
20             my %Digits = (
21             nol => 0, kosong => 0, ksg => 0, ksng => 0,
22             se => 1, satu => 1, st => 1,
23             dua => 2,
24             tiga => 3, tg => 3,
25             empat => 4, pat => 4, ampat => 4, mpat => 4,
26             lima => 5, lm => 5,
27             enam => 6, nam => 6,
28             tujuh => 7, tjh => 7,
29             delapan => 8, dlpn => 8, lapan => 8,
30             sembilan => 9, smbln => 9,
31             );
32              
33             my %Small_Mults = (
34             puluh => 1e1, plh => 1e1,
35             lusin => 12,
36             kodi => 20,
37             ratus => 1e2, rts => 1e2,
38             gros => 144, gross => 144,
39             ribu => 1e3, rb => 1e3,
40             juta => 1e6, jt => 1e6,
41             milyar => 1e9, milyard => 1e9, miliar => 1e9, miliard => 1e9,
42             triliun => 1e12, trilyun => 1e12,
43             );
44              
45             my %Big_Mults = (
46             # -yun / kw- / etc variants?
47             kuadriliun => 1e15,
48             kuintiliun => 1e18,
49             sekstiliun => 1e21,
50             septiliun => 1e24,
51             oktiliun => 1e27,
52             noniliun => 1e30,
53             desiliun => 1e33,
54             undesiliun => 1e36,
55             duodesiliun => 1e39,
56             tredesiliun => 1e42,
57             kuatuordesiliun => 1e45,
58             kuindesiliun => 1e48,
59             seksdesiliun => 1e51,
60             septendesiliun => 1e54,
61             oktodesiliun => 1e57,
62             novemdesiliun => 1e60,
63             vigintiliun => 1e63,
64             googol => 1e100, gugol => 1e100,
65             sentiliun => 1e303,
66             );
67              
68             my %Mults = (%Small_Mults, %Big_Mults);
69              
70             my %Teen_Words = (
71             belas => 0,
72             bls => 0,
73             );
74              
75             my %Words = (
76             %Digits, %Mults, %Teen_Words,
77             );
78              
79             # doesn't contain big multipliers (usually only used in scientific and not in
80             # daily common text)
81             my %Words2 = (
82             %Digits, %Small_Mults, %Teen_Words,
83             );
84              
85             my %Se = ("se" => 0, "s" => 0);
86              
87             # words that can be used with se- (or single digit), e.g. sebelas, tiga belas,
88             # sepuluh, seratus, dua ratus, ...
89             my %Se_Words = (
90             %Mults, %Teen_Words,
91             );
92              
93             my %Se_Words2 = (
94             %Small_Mults, %Teen_Words,
95             );
96              
97             my $Pos_pat = qr/(?:positif|plus|pos)/;
98             my $Neg_pat = qr/(?:negatif|ngtf|min|minus|mns)/;
99             my $Exp_pat = qr/(?:(?:di)?\s*(?:kali|kl)(?:kan)?\s+(?:sepuluh|splh)
100             \s+(?:pangkat|pkt|pngkt))/x;
101             my $Dec_pat = qr/(?:koma|km|titik|ttk)/;
102             my $Teen_pat = "(?:".join("|", sort keys %Teen_Words).")";
103             my $Mult_pat = "(?:" . join("|", sort keys %Se_Words).")";
104             my $Se_pat = "(?:" . join("|", sort keys %Se).")";
105             my $Se_Mult_pat = "(?:(?:" . join("|", sort keys %Se).")".
106             "(?:" . join("|", sort keys %Se_Words) . "))";
107             my $Se_Mult_pat2 = "(?:(?:" . join("|", sort keys %Se).")".
108             "(?:" . join("|", sort keys %Se_Words2) . "))";
109              
110             # quick pattern for extracting words
111             # neg_pat? (num + mult)+
112             our $Pat = join(
113             "",
114             '(?:(?:', "\n",
115             ' (?:', $Neg_pat, '\s*)?', " # opt: negative\n",
116             ' (?:', '(?:', join("|", sort(keys(%Se),keys(%Digits))), ')|', $Parse::Number::ID::Pat, ')', " # num\n",
117             ' (?:', '\s*', $Dec_pat, '\s*', '(?:', join("|", sort keys %Digits), '\s*)+', ')?', " # opt: decimal\n",
118             ' (?:', '\s*', '(?:', join("|", sort keys %Small_Mults), ')', '){0,3}', " # opt: mult\n",
119             '\s*)+)',
120             );
121             $Pat = qr/$Pat/x;
122              
123             $SPEC{words2nums} = {
124             v => 1.1,
125             summary => 'Convert Indonesian verbage to number',
126             description => <<'_',
127              
128             Parse Indonesian verbage and return number, or undef if failed (unknown verbage
129             or 'syntax error'). In English, this is equivalent to converting "one hundred
130             and twenty three" to 123. Currently can handle real numbers in normal and
131             scientific form in the order of hundreds of trillions.
132              
133             Will produce unexpected result if you feed it stupid verbage.
134              
135             _
136             args => {
137             str => {
138             schema => 'str*',
139             summary => 'The verbage to convert',
140             req => 1,
141             pos => 0,
142             },
143             },
144             args_as => 'array',
145             result_naked => 1,
146             };
147 46     46 1 192 sub words2nums($) { _handle_exp(@_) }
148              
149             $SPEC{words2nums_simple} = {
150             v => 1.1,
151             summary => 'Like words2nums, but can only parse sequence of digits',
152             description => <<'_',
153              
154             In English, this is equivalent to converting "one two three" to 123.
155              
156             Will produce unexpected result if you feed it stupid verbage.
157              
158             _
159             args => {
160             str => {
161             schema => 'str*',
162             summary => 'The verbage to convert',
163             req => 1,
164             pos => 0,
165             },
166             },
167             args_as => 'array',
168             result_naked => 1,
169             };
170 7     7 1 9 sub words2nums_simple($) { _handle_simple(@_) }
171              
172             sub _handle_exp($) {
173 46     46   61 my $words = lc shift;
174 46         37 my ($num1, $num2);
175              
176 46 100       272 if( $words =~ /(.+)\s+$Exp_pat\s+(.+)/ ) {
177             #$log->trace("it's an exponent");
178 4         5 $num1 = _handle_neg($1);
179 4         6 $num2 = _handle_neg($2);
180             #$log->trace("num1 is $num1, num2 is $num2");
181 4 50 33     10 !defined($num1) || !defined($num2) and return undef;
182 4         30 return $num1 * 10 ** $num2;
183             } else {
184             #$log->trace("not an exponent");
185 42         46 $num1 = _handle_neg($words);
186 42 50       52 not defined $num1 and return undef;
187             #$log->trace("num1 = $num1");
188 42         147 return $num1;
189             }
190             }
191              
192             sub _handle_neg($) {
193 50     50   66 my $words = lc shift;
194 50         35 my $num1;
195              
196 50 100       246 if( $words =~ /^\s*$Neg_pat\s+(.+)/ ) {
    100          
197             #$log->trace("it's negative");
198 8         11 $num1 = -_handle_dec($1);
199 8 50       11 not defined $num1 and return undef;
200             #$log->trace("num1 = $num1");
201 8         8 return $num1;
202             } elsif( $words =~ /^\s*$Pos_pat\s+(.+)/ ) {
203             #$log->trace("it's positif");
204 1         4 $num1 = _handle_dec($1);
205 1 50       3 not defined $num1 and return undef;
206             #$log->trace("num1 = $num1");
207 1         2 return $num1;
208             } else {
209             #$log->trace("it's not negative");
210 41         51 $num1 = _handle_dec($words);
211 41 50       51 not defined $num1 and return undef;
212             #$log->trace("num1 = $num1");
213 41         46 return $num1;
214             }
215             }
216              
217             sub _handle_dec($) {
218 50     50   53 my $words = lc shift;
219 50         29 my ($num1, $num2);
220              
221 50 100       595 if( $words =~ /(.+)\s+$Dec_pat\s+(.+)/ ) {
222             #$log->trace("it has decimals (\$1=$1, \$2=$2)");
223 8         8 $num1 = _handle_int($1);
224 8         10 $num2 = _handle_simple($2);
225             #$log->trace("num1 is $num1, num2 is $num2");
226 8 50 33     19 !defined($num1) || !defined($num2) and return undef;
227 8         24 return $num1 + ("0.".$num2);
228             } else {
229             #$log->trace("it's an integer");
230 42         48 $num1 = _handle_int($words);
231 42 50       56 not defined $num1 and return undef;
232             #$log->trace("num1 is $num1");
233 42         53 return $num1;
234             }
235             }
236              
237              
238             # handle words before decimal (e.g, 'seratus dua puluh tiga', ...)
239             sub _handle_int($) {
240 50     50   72 my @words = &_split_it( lc shift );
241 50         42 my ($num, $mult);
242 50         34 my $seen_digits = 0;
243 50         38 my ($w, $a, $subtot, $tot);
244 50         41 my @nums = ();
245              
246 50 50       99 $words[0] eq 'ERR' and return undef;
247             #$log->trace("the words are @words");
248              
249 50         51 for $w (@words) {
250 137 100       356 if( defined $Digits{$w} ) { # digits (satuan)
    100          
    100          
251             #$log->trace("saw a digit: $w");
252 0         0 $seen_digits and do { push @nums, ((10 * (pop @nums)) + $Digits{$w}) }
253 65 50 33     108 or do { push @nums, $Digits{$w}; $seen_digits = 1 }
  65         58  
  65         56  
254             }
255              
256             elsif ( looks_like_number $w ) { # digits (satuan) as number
257             #$log->trace("saw a number: $w");
258 14 50       18 return undef if $seen_digits; # 1 2 is considered an error
259 14         9 push @nums, $w;
260 14         12 $seen_digits = 1;
261             }
262              
263             elsif( $w =~ /^$Teen_pat$/ ) { # special case, teens (belasan)
264             #$log->trace("saw a teen: $w");
265 6 50       12 return undef unless $seen_digits; # mistake in writing teens
266 6         5 push @nums, 10 + pop @nums;
267 6         12 $seen_digits = 0;
268             }
269              
270             else{ # must be a multiplier, or unknown
271             #$log->trace( "saw a multiplier: $w");
272 52 50       71 return undef unless defined $Mults{$w}; # unknown word
273 52 50       60 return undef unless @nums; # mistake in writing tens/multiplier
274              
275 52         47 $a = 0; $subtot = 0;
  52         31  
276 78         59 do { $a = pop @nums; $subtot += $a }
  78         211  
277 52   100     26 until ( $a > $Mults{$w} || !@nums );
278              
279 52 100       70 if( $a > $Mults{$w} ) { push @nums, $a; $subtot -= $a }
  19         15  
  19         12  
280 52         51 push @nums, $Mults{$w}*$subtot;
281 52         56 $seen_digits = 0;
282             }
283             }
284              
285             # calculate total
286 50         43 $tot = 0;
287 50         65 while( @nums ){ $tot += shift @nums }
  72         102  
288 50         64 $tot;
289             }
290              
291              
292             # handle words after decimal (simple with no 'belas', 'puluh', 'ratus', ...)
293             sub _handle_simple($) {
294             #$log->tracef("-> _handle_simple(%s)", \@_);
295 15     15   27 my @words = &_split_it( lc shift );
296             #$log->tracef("words = %s", \@words);
297 15         13 my ($num, $w);
298              
299 15 50       22 $words[0] eq 'ERR' and return undef;
300              
301 15         10 $num = "";
302 15         16 for $w (@words) {
303 38 100       62 if (looks_like_number $w) {
304 4         5 $num .= $w;
305             } else {
306 34 50       42 not defined $Digits{$w} and return undef;
307 34         32 $num .= $Digits{$w};
308             }
309             }
310              
311 15         39 $num;
312             }
313              
314              
315             # split string into array of words. also splits 'sepuluh' -> (se, puluh),
316             # 'tigabelas' -> (tiga, belas), etc.
317             sub _split_it($) {
318 65     65   66 my $words = lc shift;
319 65         57 my @words = ();
320 65         40 my $w;
321              
322 65         164 for $w (split /\s+/, $words) {
323             ##$log->trace("saw $w");
324 154 100 66     1067 if ($w =~ /^([-+]?[0-9.,]+(?:[Ee][+-]?\d+)?)(\D?.*)$/) {
    100 33        
    50          
    50          
325 18         28 my ($n0, $w2) = ($1, $2);
326             #print "n0=$n0, w2=$w2\n";
327 18         28 my $n = parse_number_id(text => $n0);
328 18 50       286 unless (defined $n) {
329 0         0 unshift @words, 'ERR';
330 0         0 last;
331             }
332 18         16 push @words, $n;
333 18 100       29 push @words, $w2 if length($w2);
334             }
335             elsif( $w =~ /^($Se_pat)($Mult_pat)$/ and defined $Words{$1} ) {
336             #$log->trace("i should split $w");
337 18         37 push @words, $1, $2 }
338             elsif( $w =~ /^(.+)\s+($Mult_pat)$/ and defined $Words{$1} ) {
339             #$log->trace("i should split $w");
340 0         0 push @words, $1, $2 }
341             elsif( defined $Words{$w} ) {
342 118         175 push @words, $w }
343             else {
344             #$log->trace("i don't recognize $w");
345 0         0 unshift @words, 'ERR';
346 0         0 last;
347             }
348             }
349              
350             #use Data::Dump; dd \@words;
351 65         130 @words;
352             }
353              
354             1;
355             # ABSTRACT: Convert Indonesian verbage to number
356              
357             __END__