File Coverage

blib/lib/Lingua/FR/Numbers.pm
Criterion Covered Total %
statement 112 118 94.9
branch 58 62 93.5
condition 31 33 93.9
subroutine 10 12 83.3
pod 6 6 100.0
total 217 231 93.9


line stmt bran cond sub pod time code
1             package Lingua::FR::Numbers;
2 6     6   71752 use strict;
  6         8  
  6         144  
3              
4 6     6   3110 use utf8; # some accents here and there
  6         63  
  6         26  
5 6     6   163 use Carp qw(carp);
  6         10  
  6         262  
6 6     6   22 use Exporter;
  6         8  
  6         178  
7 6     6   20 use vars qw( $VERSION @ISA @EXPORT_OK );
  6         6  
  6         297  
8 6         6574 use vars qw(
9             $MODE
10             %NUMBER_NAMES
11             %ORDINALS
12             $OUTPUT_DECIMAL_DELIMITER
13             $SIGN_NAMES
14 6     6   20 );
  6         5  
15              
16             $VERSION = '1.161910';
17             @ISA = qw(Exporter);
18             @EXPORT_OK = qw( &number_to_fr &ordinate_to_fr );
19             $SIGN_NAMES = ('moins');
20             $OUTPUT_DECIMAL_DELIMITER = ('virgule');
21             %NUMBER_NAMES = (
22             0 => 'zéro',
23             1 => 'un',
24             2 => 'deux',
25             3 => 'trois',
26             4 => 'quatre',
27             5 => 'cinq',
28             6 => 'six',
29             7 => 'sept',
30             8 => 'huit',
31             9 => 'neuf',
32             10 => 'dix',
33             11 => 'onze',
34             12 => 'douze',
35             13 => 'treize',
36             14 => 'quatorze',
37             15 => 'quinze',
38             16 => 'seize',
39             17 => 'dix-sept',
40             18 => 'dix-huit',
41             19 => 'dix-neuf',
42             20 => 'vingt',
43             30 => 'trente',
44             40 => 'quarante',
45             50 => 'cinquante',
46             60 => 'soixante',
47             70 => 'soixante',
48             80 => 'quatre-vingt',
49             90 => 'quatre-vingt',
50             100 => 'cent',
51             1e3 => 'mille',
52             1e6 => 'million',
53             1e9 => 'milliard',
54             1e12 => 'billion', # un million de millions
55             1e18 => 'trillion', # un million de billions
56             1e24 => 'quatrillion', # un million de trillions
57             1e30 => 'quintillion', # un million de quatrillions
58             1e36 => 'sextillion', # un million de quintillions,
59             # the sextillion is the biggest legal unit
60             );
61             %ORDINALS = (
62             1 => 'premier',
63             5 => 'cinqu',
64             9 => 'neuv',
65             );
66              
67             sub number_to_fr {
68 209     209 1 5315 my $number = shift;
69 209         213 my @fr_string = ();
70              
71             # Test if $number is really a number, or return undef, from perldoc
72             # -q numbers
73 209         440 $number =~ s/_//g; # Allow for '_' separating figures
74 209 100       912 if ( $number !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) {
75 2         229 carp("Invalid number format: '$number'");
76 2         11 return;
77             }
78              
79 207 100       340 if ( $number > ( 1e75 - 1 ) ) {
80 1         90 carp("Number '$number' too big to be represented as string");
81 1         5 return;
82             }
83              
84 206 100       314 return $NUMBER_NAMES{0} if $number == 0;
85              
86             # Add the 'minus' string if the number is negative.
87 204 100       289 push @fr_string, $SIGN_NAMES if abs $number != $number;
88 204         147 $number = abs $number;
89              
90             # We deal with decimal numbers by calling number2fr twice, once for
91             # the integer part, and once for the decimal part.
92 204 100       288 if ( $number != int $number ) {
93              
94             # XXX Ugly Hack.
95 9         37 ( my $decimal ) = $number =~ /\.(\d+)/;
96              
97 9         20 push @fr_string, number_to_fr( int $number ), $OUTPUT_DECIMAL_DELIMITER;
98              
99             # Decimal numbers are correctly interpreted
100             # https://github.com/sebthebert/Lingua-FR-Numbers/commit/89b717da8950d183488c6d93c7d5e638628ef13f
101 9 100       27 if ( $decimal =~ s/^(0+([1-9][0-9]*))$/$2/ ) {
102 5         6 my $decimal_power = 10**length $1;
103 5 50       7 last unless $decimal_power;
104 5         5 my $fr_decimal;
105 5         6 $fr_decimal = number_to_fr($decimal) . ' ';
106 5         9 $fr_decimal .= ordinate_to_fr($decimal_power);
107 5 100       15 $fr_decimal .= 's' if $decimal > 1;
108 5         6 push @fr_string, $fr_decimal;
109             }
110             else {
111 4         8 push @fr_string, number_to_fr($decimal);
112             }
113              
114 9         36 return join ( ' ', @fr_string );
115             }
116              
117             # First, we split the number by 1000 blocks
118             # i.e:
119             # $block[0] => 0 .. 999 => centaines
120             # $block[1] => 1000 .. 999_999 => milliers
121             # $block[2] => 1e6 .. 999_999_999 => millions
122             # $block[3] => 1e9 .. 1e12-1 => milliards
123 195         135 my @blocks;
124 195         271 while ($number) {
125 440         381 push @blocks, $number % 1000;
126 440         644 $number = int $number / 1000;
127             }
128 195         160 @blocks = reverse @blocks;
129              
130             # We then go through each block, starting from the greatest
131             # (..., billions, millions, thousands)
132 195         343 foreach ( 0 .. $#blocks ) {
133              
134             # No need to spell numbers like 'zero million'
135 440 100       634 next if $blocks[$_] == 0;
136              
137 256         188 my $number = $blocks[$_];
138              
139             # Determine the 'size' of the block
140 256         330 my $power = 10**( ( $#blocks - $_ ) * 3 );
141 256         293 my $hundred = int( $blocks[$_] / 100 );
142 256         220 my $teens = int( $blocks[$_] % 100 / 10 );
143 256         189 my $units = $blocks[$_] % 10;
144              
145             # Process hundred numbers 'inside' the block
146             # (ie. 235 in 235000 when dealing with thousands.)
147              
148             # Hundreds
149 256 100       321 if ($hundred) {
150 86         117 my $fr_hundred;
151              
152             # We don't say 'un cent'
153 86 100       147 $fr_hundred = $NUMBER_NAMES{$hundred} . ' '
154             unless $hundred == 1;
155              
156 86         82 $fr_hundred .= $NUMBER_NAMES{100};
157              
158             # Cent prend un 's' quand il est multiplié par un autre
159             # nombre et qu'il termine l'adjectif numéral.
160 86 100 100     250 $fr_hundred .= 's'
      100        
      100        
161             if ( $hundred > 1 && !$teens && !$units && $_ == $#blocks );
162              
163 86         102 push @fr_string, $fr_hundred;
164             }
165              
166             # Process number below 100
167 256         196 my $fr_decimal;
168              
169             # No tens
170 256 100 100     1000 $fr_decimal = $NUMBER_NAMES{$units}
      100        
      100        
171             if ( $units && !$teens )
172             && # On ne dit pas 'un mille' (A bit awkward to put here)
173             !( $number == 1 && ( $power == 1000 ) );
174              
175             # Cas spécial pour les 80
176             # On dit 'quatre-vingts' mais 'quatre-vingt-deux'
177 256 100 100     883 if ( $teens == 8 ) {
    100          
    100          
178             $fr_decimal = $units
179             ? $NUMBER_NAMES{ $teens * 10 } . '-' . $NUMBER_NAMES{$units}
180 18 100       63 : $NUMBER_NAMES{ $teens * 10 } . 's';
181             }
182              
183             # Cas spécial pour les nombres en 70 et 90
184             elsif ( $teens == 7 || $teens == 9 ) {
185 50         43 $units += 10;
186 50 100 100     110 if ( $teens == 7 && $units == 11 ) {
187             $fr_decimal =
188 4         15 $NUMBER_NAMES{ $teens * 10 } . ' et ' . $NUMBER_NAMES{$units};
189             }
190             else {
191             $fr_decimal =
192 46         95 $NUMBER_NAMES{ $teens * 10 } . '-' . $NUMBER_NAMES{$units};
193             }
194              
195             }
196              
197             # Un nombre s'écrit avec un trait d'union sauf s'il est associé
198             # à 'cent' ou à 'mille'; ou s'il est relié par 'et'.
199             # Nombres écrits avec des 'et': 21, 31, 51, 61, 71
200             elsif ($teens) {
201 79 100 66     208 if ( $teens == 1 ) {
    100          
    100          
202 44         85 $fr_decimal = $NUMBER_NAMES{ $teens * 10 + $units };
203             }
204             elsif ( $units == 1 || $units == 11 ) {
205             $fr_decimal =
206 5         19 $NUMBER_NAMES{ $teens * 10 } . ' et ' . $NUMBER_NAMES{$units};
207             }
208             elsif ( $units == 0 ) {
209 3         7 $fr_decimal = $NUMBER_NAMES{ $teens * 10 };
210             }
211             else {
212             $fr_decimal =
213 27         75 $NUMBER_NAMES{ $teens * 10 } . '-' . $NUMBER_NAMES{$units};
214             }
215             }
216              
217 256 100       383 push @fr_string, $fr_decimal if $fr_decimal;
218              
219             # Processing thousands, millions, billions, ...
220 256 100       351 if ( $power >= 1e3 ) {
221 122         69 my $fr_power;
222              
223 122 100       188 if ( exists $NUMBER_NAMES{$power} ) {
224 108         98 $fr_power = $NUMBER_NAMES{$power};
225              
226             # Billion, milliard, etc. prennent un 's' au pluriel
227 108 100 100     292 $fr_power .= 's' if $number > 1 && $power >= 1e6;
228              
229 108         112 push @fr_string, $fr_power;
230             }
231              
232             # If the power we're looking dealing with doesn't exists
233             # (ie. 1e15, 1e21) we multiply by the lowest power we have,
234             # starting at 1e6.
235             else {
236 14         11 my $sub_power;
237 14         38 my $pow_diff = 1;
238             do {
239 22         16 $pow_diff *= 1_000_000;
240 22         64 $sub_power = $power / $pow_diff;
241 14         12 } until exists $NUMBER_NAMES{$sub_power};
242              
243             # If the power_diff doesn't exists (for really big
244             # numbers), we do the same dance.
245 14 50       23 unless ( exists $NUMBER_NAMES{$pow_diff} ) {
246              
247             }
248 14         11 $fr_power = $NUMBER_NAMES{$pow_diff};
249 14 100       28 $fr_power .= 's' if $number > 1;
250 14         33 $fr_power .= " de $NUMBER_NAMES{$sub_power}s";
251              
252             # XXX Ugly hack - some architecture output "million de billion" instead of "trillion"
253 14         16 $fr_power =~ s/million(s)? de billions?/trillion$1/g;
254              
255 14         23 push @fr_string, $fr_power;
256             }
257             }
258              
259 256         319 next;
260             }
261              
262 195         671 return join ( ' ', @fr_string );
263             }
264              
265             sub ordinate_to_fr {
266 25     25 1 3312 my $number = shift;
267              
268 25 100       53 unless ( $number > 0 ) {
269 3         422 carp('Ordinates must be strictly positive');
270 3         18 return;
271             }
272 22 100       46 return $ORDINALS{1} if $number == 1;
273              
274 21         28 my $ordinal = number_to_fr($number);
275 21         27 my $last_digit = $number % 10;
276              
277 21 100 66     66 if ( $last_digit != 1 && exists $ORDINALS{$last_digit} ) {
278 5         8 my $replace = number_to_fr($last_digit);
279 5         69 $ordinal =~ s/$replace$/$ORDINALS{$last_digit}/;
280             }
281              
282 21         71 $ordinal =~ s/e?$/ième/;
283 21         26 $ordinal =~ s/vingtsième/vingtième/; # Bug #1772
284 21         50 $ordinal;
285             }
286              
287             #
288             # OO Methods
289             #
290             sub new {
291 76     76 1 13542 my $class = shift;
292 76         65 my $number = shift;
293 76         140 bless \$number, $class;
294             }
295              
296             sub parse {
297 0     0 1 0 my $self = shift;
298 0 0       0 if ( $_[0] ) { $$self = shift }
  0         0  
299 0         0 $self;
300             }
301              
302             sub get_string {
303 76     76 1 5580 my $self = shift;
304 76         126 number_to_fr($$self);
305             }
306              
307             sub get_ordinate {
308 0     0 1   my $self = shift;
309 0           ordinate_to_fr($$self);
310             }
311              
312             1;
313              
314             __END__