File Coverage

blib/lib/Lingua/EN/Numbers.pm
Criterion Covered Total %
statement 95 97 97.9
branch 55 70 78.5
condition 25 39 64.1
subroutine 13 13 100.0
pod 0 2 0.0
total 188 221 85.0


line stmt bran cond sub pod time code
1              
2             package Lingua::EN::Numbers;
3              
4             require Exporter;
5             @ISA = qw(Exporter);
6              
7 4     4   40185 use 5.006;
  4         14  
8 4     4   21 use strict;
  4         6  
  4         95  
9 4     4   19 use warnings;
  4         11  
  4         254  
10 4 50   4   7384 BEGIN { *DEBUG = sub () {0} unless defined &DEBUG } # setup a DEBUG constant
11              
12             our $VERSION = '2.03';
13             our @EXPORT = ();
14             our @EXPORT_OK = qw( num2en num2en_ordinal );
15             our (%D, %Card2ord, %Mult);
16              
17             @D{0 .. 20, 30,40,50,60,70,80,90} = qw|
18             zero
19             one two three four five six seven eight nine ten
20             eleven twelve thirteen fourteen fifteen
21             sixteen seventeen eighteen nineteen
22             twenty thirty forty fifty sixty seventy eighty ninety
23             |;
24              
25              
26             @Card2ord{ qw| one two three five eight nine twelve |}
27             = qw| first second third fifth eighth ninth twelfth |;
28              
29              
30             {
31             my $c = 0;
32             for ( '', qw<
33             thousand million billion trillion quadrillion quintillion sextillion
34             septillion octillion nonillion
35             > ) {
36             $Mult{$c} = $_;
37             $c++;
38             }
39             }
40              
41             #==========================================================================
42              
43             sub num2en_ordinal {
44             # Cardinals are [one two three...]
45             # Ordinals are [first second third...]
46            
47 6 50 33 6 0 37 return undef unless defined $_[0] and length $_[0];
48 6         11 my($x) = $_[0];
49            
50 6         9 $x = num2en($x);
51 6 50       15 return $x unless $x;
52 6 50       36 $x =~ s/(\w+)$//s or return $x . "th";
53 6         13 my $last = $1;
54              
55             $last =
56 6   33     42 $Card2ord{$last} || ( $last =~ s/y$/ieth/ && $last ) || ( $last !~ /th$/ ? $last . "th" : $last );
57              
58 6         19 return "$x$last";
59             }
60              
61             #==========================================================================
62              
63             sub num2en {
64 86     86 0 1423 my $x = $_[0];
65 86 100 100     474 return undef unless defined $x and length $x;
66              
67 84 50       197 return 'not-a-number' if $x eq 'NaN';
68 84 50       196 return 'positive infinity' if $x =~ m/^\+inf(?:inity)?$/si;
69 84 50       172 return 'negative infinity' if $x =~ m/^\-inf(?:inity)?$/si;
70 84 50       183 return 'infinity' if $x =~ m/^inf(?:inity)?$/si;
71              
72 84 100       305 return $D{$x} if exists $D{$x}; # the most common cases
73              
74             # Make sure it's not in scientific notation:
75 54 100       67 { my $e = _e2en($x); return $e if defined $e; }
  54         117  
  54         151  
76            
77 49         61 my $orig = $x;
78              
79 49         84 $x =~ s/,//g; # nix any commas
80              
81 49         60 my $sign;
82 49 100       162 $sign = $1 if $x =~ s/^([-+])//s;
83            
84 49         98 my($int, $fract);
85 49 100       195 if( $x =~ m<^[0-9]+$> ) { $int = $x }
  33 100       49  
    100          
86 9         32 elsif( $x =~ m<^([0-9]+)\.([0-9]+)$> ) { $int = $1; $fract = $2 }
  9         19  
87 3         7 elsif( $x =~ m<^\.([0-9]+)$> ) { $fract = $1 }
88             else {
89 4         5 DEBUG and print "Not a number: \"orig\"\n";
90 4         23 return undef;
91             }
92            
93 45         52 DEBUG and printf " Working on Sign[%s] Int2en[%s] Fract[%s] < \"%s\"\n",
94             map defined($_) ? $_ : "nil", $sign, $int, $fract, $orig;
95            
96 45   66     122 return join ' ', grep defined($_) && length($_),
97             _sign2en($sign),
98             _int2en($int),
99             _fract2en($fract),
100             ;
101             }
102              
103             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
104              
105             sub _sign2en {
106 45 100 66 45   177 return undef unless defined $_[0] and length $_[0];
107 13 100       44 return 'negative' if $_[0] eq '-';
108 4 50       15 return 'positive' if $_[0] eq '+';
109 0         0 return "WHAT_IS_$_[0]";
110             }
111              
112             sub _fract2en { # "1234" => "point one two three four"
113 45 100 66 45   440 return undef unless defined $_[0] and length $_[0];
114 12         20 my $x = $_[0];
115 12         180 return join ' ', 'point', map $D{$_}, split '', $x;
116             }
117              
118             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
119             # The real work:
120              
121             sub _int2en {
122 74 50 66 74   549 return undef unless defined $_[0] and length $_[0]
      66        
123             and $_[0] =~ m/^[0-9]+$/s;
124              
125 71         102 my($x) = $_[0];
126              
127 71 100       265 return $D{$x} if defined $D{$x}; # most common/irreg cases
128            
129 43 100       155 if( $x =~ m/^(.)(.)$/ ) {
    100          
130 23         128 return $D{$1 . '0'} . '-' . $D{$2};
131             # like forty - two
132             # note that neither bit can be zero at this point
133            
134             } elsif( $x =~ m/^(.)(..)$/ ) {
135 11         40 my($h, $rest) = ("$D{$1} hundred", $2);
136 11 100       28 return $h if $rest eq '00';
137 10         52 return "$h and " . _int2en(0 + $rest);
138             } else {
139 9         21 return _bigint2en($x);
140             }
141             }
142              
143             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
144              
145             sub _bigint2en {
146 9 50 33 9   80 return undef unless defined $_[0] and length $_[0]
      33        
147             and $_[0] =~ m/^[0-9]+$/s;
148              
149 9         15 my($x) = $_[0];
150              
151 9         12 my @chunks; # each: [ string, exponent ]
152            
153             {
154 9         13 my $groupnum = 0;
  9         34  
155 9         12 my $num;
156 9         46 while( $x =~ s<([0-9]{1,3})$><>s ) { # pull at most three digits from the end
157 23         51 $num = $1 + 0;
158 23 100       66 unshift @chunks, [ $num, $groupnum ] if $num;
159 23         80 ++$groupnum;
160             }
161 9 50       38 return $D{'0'} unless @chunks; # rare but possible
162             }
163            
164 9         10 my $and;
165            
166 9 100 100     49 $and = 'and' if $chunks[-1][1] == 0 and $chunks[-1][0] < 100;
167             # The special 'and' that shows up in like "one thousand and eight"
168             # and "two billion and fifteen", but not "one thousand [*and] five hundred"
169             # or "one million, [*and] nine"
170              
171 9         31 _chunks2en( \@chunks );
172              
173             # bugfix: neilb: deals with case where we have at least millions, thousands,
174             # but 00N for the last chunk.
175             # $chunks[-2] .= " and" if $and and @chunks > 1;
176 9 100 66     44 if ($and and @chunks > 1) {
177 6         15 $chunks[-2] .= " and $chunks[-1]";
178 6         9 pop(@chunks);
179             }
180              
181 9 100 100     37 return "$chunks[0] $chunks[1]" if @chunks == 2 and !$and;
182             # Avoid having a comma if just two units
183 8         34 return join ", ", @chunks;
184             }
185              
186              
187             sub _chunks2en {
188 9     9   16 my $chunks = $_[0];
189 9 50       24 return unless @$chunks;
190 9         12 my @out;
191 9         18 foreach my $c (@$chunks) {
192 19 50       71 push @out, $c = _groupify( _int2en( $c->[0] ), $c->[1] ) if $c->[0];
193             }
194 9         25 @$chunks = @out;
195 9         18 return;
196             }
197              
198             sub _groupify {
199             # turn ("seventeen", 3) => "seventeen billion"
200 19     19   27 my($basic, $multnum) = @_;
201 19 100       63 return $basic unless $multnum; # the first group is unitless
202 12         14 DEBUG > 2 and print " Groupifying $basic x $multnum mults\n";
203 12 50       72 return "$basic $Mult{$multnum}" if $Mult{$multnum};
204             # Otherwise it must be huuuuuge, so fake it with scientific notation
205 0         0 return "$basic " . "times ten to the " . num2en_ordinal($multnum * 3);
206             }
207              
208             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
209             #
210             # Because I can never remember this:
211             #
212             # 3.1E8
213             # ^^^ is called the "mantissa"
214             # ^ is called the "exponent"
215             # (the implicit "10" is the "base" a/k/a "radix")
216              
217             sub _e2en {
218 54     54   75 my $x = $_[0];
219              
220 54         72 my($m, $e);
221 54 100       217 if( $x =~
222             m<
223             ^(
224             [-+]? # leading sign
225             (?:
226             [0-9,]+ | [0-9,]*\.[0-9]+ # number
227             )
228             )
229             [eE]
230             ([-+]?[0-9]+) # mantissa, has to be an integer
231             $
232             >x
233             ) {
234 5         13 ($m, $e) = ($1, $2);
235 5         5 DEBUG and print " Scientific notation: [$x] => $m E $e\n";
236 5         10 $e += 0;
237 5         12 return num2en($m) . ' times ten to the ' . num2en_ordinal($e);
238             } else {
239 49         67 DEBUG and print " Okay, $x isn't in exponential notation\n";
240 49         102 return undef;
241             }
242             }
243              
244             #==========================================================================
245             1;
246              
247             __END__