File Coverage

blib/lib/Lingua/EN/Numbers.pm
Criterion Covered Total %
statement 99 101 98.0
branch 55 70 78.5
condition 25 39 64.1
subroutine 14 14 100.0
pod 0 2 0.0
total 193 226 85.4


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