dolibarr 24.0.0-beta
functionsnumtoword.lib.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2015 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2015 Víctor Ortiz Pérez <victor@accett.com.mx>
4 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
5 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 * or see https://www.gnu.org/
20 */
21
38function dol_convertToWord($num, $langs, $currency = '', $centimes = false)
39{
40 //$num = str_replace(array(',', ' '), '', trim($num)); This should be useless since $num MUST be a php numeric value
41 if (!$num) {
42 return false;
43 }
44
45 // Dedicated converter for French. The generic algorithm below is built on the English
46 // number structure and cannot express French spelling rules (et, hyphens, agreement of
47 // cent/vingt, elision of "un" before cent/mille). See dolConvertToWordFrench().
48 $langcode = $langs->getDefaultLang(0);
49 if (preg_match('/^fr/i', (string) $langcode)) {
50 return dolConvertToWordFrench($num, $langs, $currency, $centimes);
51 }
52
53 $numbackup = $num;
54
55 if (isModEnabled('numberwords')) {
56 $concatWords = $langs->getLabelFromNumber((string) $num, $currency);
57 return $concatWords;
58 } else {
59 $TNum = explode('.', (string) $num);
60
61 $num = abs((int) $TNum[0]);
62 $words = array();
63 $list1 = array(
64 '',
65 $langs->transnoentitiesnoconv('one'),
66 $langs->transnoentitiesnoconv('two'),
67 $langs->transnoentitiesnoconv('three'),
68 $langs->transnoentitiesnoconv('four'),
69 $langs->transnoentitiesnoconv('five'),
70 $langs->transnoentitiesnoconv('six'),
71 $langs->transnoentitiesnoconv('seven'),
72 $langs->transnoentitiesnoconv('eight'),
73 $langs->transnoentitiesnoconv('nine'),
74 $langs->transnoentitiesnoconv('ten'),
75 $langs->transnoentitiesnoconv('eleven'),
76 $langs->transnoentitiesnoconv('twelve'),
77 $langs->transnoentitiesnoconv('thirteen'),
78 $langs->transnoentitiesnoconv('fourteen'),
79 $langs->transnoentitiesnoconv('fifteen'),
80 $langs->transnoentitiesnoconv('sixteen'),
81 $langs->transnoentitiesnoconv('seventeen'),
82 $langs->transnoentitiesnoconv('eighteen'),
83 $langs->transnoentitiesnoconv('nineteen')
84 );
85 $list2 = array(
86 '',
87 $langs->transnoentitiesnoconv('ten'),
88 $langs->transnoentitiesnoconv('twenty'),
89 $langs->transnoentitiesnoconv('thirty'),
90 $langs->transnoentitiesnoconv('forty'),
91 $langs->transnoentitiesnoconv('fifty'),
92 $langs->transnoentitiesnoconv('sixty'),
93 $langs->transnoentitiesnoconv('seventy'),
94 $langs->transnoentitiesnoconv('eighty'),
95 $langs->transnoentitiesnoconv('ninety'),
96 $langs->transnoentitiesnoconv('hundred')
97 );
98 $list3 = array(
99 '',
100 $langs->transnoentitiesnoconv('thousand'),
101 $langs->transnoentitiesnoconv('million'),
102 $langs->transnoentitiesnoconv('billion'),
103 $langs->transnoentitiesnoconv('trillion'),
104 $langs->transnoentitiesnoconv('quadrillion')
105 );
106
107 $num_length = strlen((string) $num);
108 $levels = (int) (($num_length + 2) / 3);
109 $max_length = $levels * 3;
110 $num = substr('00'.$num, -$max_length);
111 $num_levels = str_split($num, 3);
112 $nboflevels = count($num_levels);
113 for ($i = 0; $i < $nboflevels; $i++) {
114 $levels--;
115 $hundreds = (int) ((int) $num_levels[$i] / 100);
116 $hundreds = ($hundreds ? ' '.$list1[$hundreds].' '.$langs->transnoentities('hundred').($hundreds == 1 ? '' : 's').' ' : '');
117 $tens = (int) ((int) $num_levels[$i] % 100);
118 $singles = '';
119 if ($tens < 20) {
120 $tens = ($tens ? ' '.$list1[$tens].' ' : '');
121 } else {
122 $tsd = (int) ($tens / 10); // tens digit (2-9)
123 $usd = (int) ($tens % 10); // units digit (0-9)
124
125 // French-style systems: 70-79 = sixty + (10-19), 90-99 = eighty + (10-19)
126 // Detection: if the translation of "seventy"/"ninety" contains the word for "ten"
127 $tenWord = trim($list1[10]); // e.g. "dix"
128 if ($usd > 0 && ($tsd == 7 || $tsd == 9) && $tenWord !== '' && strpos(trim($list2[$tsd]), $tenWord) !== false) {
129 // Use the base ten word (60 for 70s, 80 for 90s) + teen word (11-19)
130 $baseWord = trim($list2[$tsd - 1]);
131 // Remove trailing 's' (e.g. quatre-vingts → quatre-vingt when used in composition)
132 if (substr($baseWord, -1) === 's') {
133 $baseWord = substr($baseWord, 0, -1);
134 }
135 $tens = ' '.$baseWord.' ';
136 $singles = ' '.$list1[10 + $usd].' ';
137 } else {
138 $tens = ' '.$list2[$tsd].' ';
139 $singles = ' '.$list1[$usd].' ';
140 }
141 }
142 $words[] = $hundreds.$tens.$singles.(($levels && (int) ($num_levels[$i])) ? ' '.$list3[$levels].' ' : '');
143 } //end for loop
144 $commas = count($words);
145 if ($commas > 1) {
146 $commas -= 1;
147 }
148 $concatWords = implode(' ', $words);
149 // Delete multi whitespaces
150 $concatWords = trim(preg_replace('/[ ]+/', ' ', $concatWords));
151
152 if (!empty($currency)) {
153 $concatWords .= ' '.$currency;
154 }
155
156 // If we need to write cents, spell the 2-digit cents value (e.g. 0.07 => 7 cents, not 70)
157 $tmptab = explode('.', number_format((float) abs($numbackup), 2, '.', ''));
158 $cents = isset($tmptab[1]) ? (int) $tmptab[1] : 0;
159
160 if ($cents > 0) {
161 if (!empty($currency)) {
162 $concatWords .= ' '.$langs->transnoentities('and');
163 }
164
165 $concatWords .= ' '.dol_convertToWord((float) $cents, $langs, '', false);
166 if (!empty($currency)) {
167 $concatWords .= ' '.$langs->transnoentities('centimes');
168 }
169 }
170 return $concatWords;
171 }
172}
173
174
185function dolConvertIntToFrenchWords($n, $reform = false)
186{
187 $n = (int) $n;
188 if ($n < 0) {
189 $n = -$n;
190 }
191 if ($n === 0) {
192 return 'zéro';
193 }
194
195 $sep = $reform ? '-' : ' ';
196 $etjoin = $reform ? '-et-' : ' et ';
197
198 $units = array(
199 'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf',
200 'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize',
201 'dix-sept', 'dix-huit', 'dix-neuf'
202 );
203 $tensmap = array(2 => 'vingt', 3 => 'trente', 4 => 'quarante', 5 => 'cinquante', 6 => 'soixante');
204 $scales = array('', 'mille', 'million', 'milliard', 'billion', 'billiard', 'trillion');
205
206 // Spell a number from 1 to 99
207 $below100 = function (int $m) use ($units, $tensmap, $etjoin): string {
208 if ($m < 20) {
209 return $units[$m] ?? '';
210 }
211 $t = intdiv($m, 10);
212 $u = $m % 10;
213 if ($t == 7 || $t == 9) {
214 // 70-79 = soixante + (10..19), 90-99 = quatre-vingt + (10..19)
215 $base = ($t == 7) ? 'soixante' : 'quatre-vingt';
216 if ($t == 7 && $u == 1) {
217 return $base.$etjoin.$units[10 + $u]; // soixante et onze (but quatre-vingt-onze takes no "et")
218 }
219 return $base.'-'.$units[10 + $u];
220 }
221 if ($t == 8) {
222 // 80 = quatre-vingts, 81..89 = quatre-vingt-x (no "s", no "et")
223 return ($u == 0) ? 'quatre-vingts' : 'quatre-vingt-'.$units[$u];
224 }
225 // 20..69
226 $w = $tensmap[$t] ?? '';
227 if ($u == 0) {
228 return $w;
229 }
230 if ($u == 1) {
231 return $w.$etjoin.$units[1]; // vingt et un .. soixante et un
232 }
233 return $w.'-'.$units[$u];
234 };
235
236 // Split into groups of 3 digits, lowest group first (index 0 = units, 1 = thousands, 2 = millions...)
237 $groups = array();
238 $tmp = $n;
239 while ($tmp > 0) {
240 $groups[] = $tmp % 1000;
241 $tmp = intdiv($tmp, 1000);
242 }
243 $ng = count($groups);
244
245 $pieces = array(); // each entry: array('text' => string, 'noun' => bool) ; noun = million/milliard scale
246 for ($g = $ng - 1; $g >= 0; $g--) {
247 $val = $groups[$g];
248 if ($val == 0) {
249 continue;
250 }
251 $h = intdiv($val, 100);
252 $r = $val % 100;
253
254 // "cent" agrees (becomes "cents") only when multiplied and not followed by another numeral:
255 // plural when terminal (units group) or directly before a noun scale (million+),
256 // but invariable before "mille".
257 $centplural = ($h >= 2 && $r == 0 && ($g === 0 || $g >= 2));
258
259 $text = '';
260 if ($h >= 1) {
261 $text = ($h == 1) ? 'cent' : $units[$h].$sep.'cent';
262 if ($centplural) {
263 $text .= 's';
264 }
265 }
266 if ($r > 0) {
267 $rtext = $below100($r);
268 // "quatre-vingts" keeps its "s" only when terminal or before a noun scale, not before "mille".
269 if ($r == 80 && $g === 1) {
270 $rtext = 'quatre-vingt';
271 }
272 $text = ($text === '') ? $rtext : $text.$sep.$rtext;
273 }
274
275 if ($g == 1) {
276 // thousands: "mille" is invariable and takes no leading "un"
277 $text = ($val == 1) ? 'mille' : $text.$sep.'mille';
278 $pieces[] = array('text' => $text, 'noun' => false);
279 } elseif ($g >= 2) {
280 $scaleword = isset($scales[$g]) ? $scales[$g] : '';
281 if ($scaleword !== '') {
282 if ($val >= 2) {
283 $scaleword .= 's'; // millions, milliards...
284 }
285 $text .= ' '.$scaleword; // a noun scale is always separated by a space
286 }
287 $pieces[] = array('text' => $text, 'noun' => true);
288 } else {
289 $pieces[] = array('text' => $text, 'noun' => false);
290 }
291 }
292
293 $result = '';
294 foreach ($pieces as $i => $piece) {
295 if ($i === 0) {
296 $result = $piece['text'];
297 } else {
298 // after a noun scale (million+) keep a space, otherwise use the chosen separator
299 $result .= ($pieces[$i - 1]['noun'] ? ' ' : $sep).$piece['text'];
300 }
301 }
302
303 return $result;
304}
305
306
316function dolConvertToWordFrench($num, $langs, $currency = '', $centimes = false)
317{
318 // Spelling convention, set with constant CONVERT_TO_WORD_FR (Home - Setup - Other setup):
319 // - default/'PRE1990' = traditional spelling (hyphen only between tens and units)
320 // - 'REFORME1990' = 1990 rectified spelling (hyphens between all the numeral words)
321 $reform = (getDolGlobalString('CONVERT_TO_WORD_FR') === 'REFORME1990');
322
323 $numstr = number_format((float) $num, 2, '.', '');
324 $negative = false;
325 if (strpos($numstr, '-') === 0) {
326 $negative = true;
327 $numstr = substr($numstr, 1);
328 }
329 $tmptab = explode('.', $numstr);
330 $intpart = (int) $tmptab[0];
331 $cents = isset($tmptab[1]) ? (int) $tmptab[1] : 0;
332
333 $result = ($intpart === 0) ? '' : dolConvertIntToFrenchWords($intpart, $reform);
334
335 if (!empty($currency)) {
336 if ($result === '') {
337 $result = 'zéro';
338 }
339 $result .= ' '.$currency;
340 }
341
342 if ($cents > 0) {
343 if ($centimes) {
344 $label = $langs->transnoentitiesnoconv($cents === 1 ? 'centime' : 'centimes');
345 $and = $langs->transnoentitiesnoconv('and');
346 $centtext = dolConvertIntToFrenchWords($cents, $reform).' '.$label;
347 if ($result === '') {
348 $result = $centtext;
349 } else {
350 $result .= ' '.$and.' '.$centtext;
351 }
352 } else {
353 // Not an amount: spell the decimal part after "virgule"
354 $result = ($result === '' ? 'zéro' : $result).' virgule '.dolConvertIntToFrenchWords($cents, $reform);
355 }
356 }
357
358 if ($result === '') {
359 $result = 'zéro';
360 }
361 if ($negative) {
362 $result = 'moins '.$result;
363 }
364
365 return $result;
366}
367
368
379function dolNumberToWord($numero, $langs, $numorcurrency = 'number')
380{
381 // If the number is negative convert to positive and return -1 if it is too long
382 if ($numero < 0) {
383 $numero *= -1;
384 }
385 if ($numero >= 1000000000001) {
386 return -1;
387 }
388
389 // Get 2 decimals to cents, another functions round or truncate
390 $strnumber = number_format($numero, 10);
391 $len = strlen($strnumber);
392 $parte_decimal = '00'; // For static analysis, strnumber should contain '.'
393 for ($i = 0; $i < $len; $i++) {
394 if ($strnumber[$i] == '.') {
395 $parte_decimal = $strnumber[$i + 1].$strnumber[$i + 2];
396 break;
397 }
398 }
399
400 /* Dolibarr 3.6.2 doesn't have $langs->default, why ask $lang like a parameter in case it exists? */
401 if (((is_object($langs) && $langs->getDefaultLang(0) == 'es_MX') || (!is_object($langs) && $langs == 'es_MX')) && $numorcurrency == 'currency') {
402 if ($numero >= 1 && $numero < 2) {
403 return ("UN PESO ".$parte_decimal." / 100 M.N.");
404 } elseif ($numero >= 0 && $numero < 1) {
405 return ("CERO PESOS ".$parte_decimal." / 100 M.N.");
406 } elseif ($numero >= 1000000 && $numero < 1000001) {
407 return ("UN MILL&OacuteN DE PESOS ".$parte_decimal." / 100 M.N.");
408 } elseif ($numero >= 1000000000000 && $numero < 1000000000001) {
409 return ("UN BILL&OacuteN DE PESOS ".$parte_decimal." / 100 M.N.");
410 } else {
411 $entexto = "";
412 $number = $numero;
413 if ($number >= 1000000000) {
414 $CdMMillon = (int) ($numero / 100000000000);
415 $numero -= $CdMMillon * 100000000000;
416 $DdMMillon = (int) ($numero / 10000000000);
417 $numero -= $DdMMillon * 10000000000;
418 $UdMMillon = (int) ($numero / 1000000000);
419 $numero -= $UdMMillon * 1000000000;
420 $entexto .= hundreds2text($CdMMillon, $DdMMillon, $UdMMillon);
421 $entexto .= " MIL ";
422 } else {
423 $CdMMillon = 0;
424 $DdMMillon = 0;
425 $UdMMillon = 0;
426 }
427 if ($number >= 1000000) {
428 $CdMILLON = (int) ($numero / 100000000);
429 $numero -= $CdMILLON * 100000000;
430 $DdMILLON = (int) ($numero / 10000000);
431 $numero -= $DdMILLON * 10000000;
432 $udMILLON = (int) ($numero / 1000000);
433 $numero -= $udMILLON * 1000000;
434 $entexto .= hundreds2text($CdMILLON, $DdMILLON, $udMILLON);
435 if (!$CdMMillon && !$DdMMillon && !$UdMMillon && !$CdMILLON && !$DdMILLON && $udMILLON == 1) {
436 $entexto .= " MILL&OacuteN ";
437 } else {
438 $entexto .= " MILLONES ";
439 }
440 }
441
442 if ($number >= 1000) {
443 $cdm = (int) ($numero / 100000);
444 $numero -= $cdm * 100000;
445 $ddm = (int) ($numero / 10000);
446 $numero -= $ddm * 10000;
447 $udm = (int) ($numero / 1000);
448 $numero -= $udm * 1000;
449 $entexto .= hundreds2text($cdm, $ddm, $udm);
450 if ($cdm || $ddm || $udm) {
451 $entexto .= " MIL ";
452 }
453 } else {
454 $ddm = 0;
455 $cdm = 0;
456 $udm = 0;
457 }
458 $c = (int) ($numero / 100);
459 $numero -= $c * 100;
460 $d = (int) ($numero / 10);
461 $u = (int) $numero - $d * 10;
462 $entexto .= hundreds2text($c, $d, $u);
463 if (!$cdm && !$ddm && !$udm && !$c && !$d && !$u && $number > 1000000) {
464 $entexto .= " DE";
465 }
466 $entexto .= " PESOS ".$parte_decimal." / 100 M.N.";
467 }
468 return $entexto;
469 }
470 return -1;
471}
472
481function hundreds2text($hundreds, $tens, $units)
482{
483 if ($hundreds == 1 && $tens == 0 && $units == 0) {
484 return "CIEN";
485 }
486 $centenas = array("CIENTO", "DOSCIENTOS", "TRESCIENTOS", "CUATROCIENTOS", "QUINIENTOS", "SEISCIENTOS", "SETECIENTOS", "OCHOCIENTOS", "NOVECIENTOS");
487 $decenas = array("", "", "TREINTA ", "CUARENTA ", "CINCUENTA ", "SESENTA ", "SETENTA ", "OCHENTA ", "NOVENTA ");
488 $veintis = array("VEINTE", "VEINTIUN", "VEINTID&OacuteS", "VEINTITR&EacuteS", "VEINTICUATRO", "VEINTICINCO", "VEINTIS&EacuteIS", "VEINTISIETE", "VEINTIOCHO", "VEINTINUEVE");
489 $diecis = array("DIEZ", "ONCE", "DOCE", "TRECE", "CATORCE", "QUINCE", "DIECIS&EacuteIS", "DIECISIETE", "DIECIOCHO", "DIECINUEVE");
490 $unidades = array("UN", "DOS", "TRES", "CUATRO", "CINCO", "SEIS", "SIETE", "OCHO", "NUEVE");
491 $entexto = "";
492 if ($hundreds != 0) {
493 $entexto .= $centenas[$hundreds - 1];
494 }
495 if ($tens > 2) {
496 if ($hundreds != 0) {
497 $entexto .= " ";
498 }
499 $entexto .= $decenas[$tens - 1];
500 if ($units != 0) {
501 $entexto .= " Y ";
502 $entexto .= $unidades[$units - 1];
503 }
504 return $entexto;
505 } elseif ($tens == 2) {
506 if ($hundreds != 0) {
507 $entexto .= " ";
508 }
509 $entexto .= " ".$veintis[$units];
510 return $entexto;
511 } elseif ($tens == 1) {
512 if ($hundreds != 0) {
513 $entexto .= " ";
514 }
515 $entexto .= $diecis[$units];
516 return $entexto;
517 }
518 if ($units != 0) {
519 if ($hundreds != 0 || $tens != 0) {
520 $entexto .= " ";
521 }
522 $entexto .= $unidades[$units - 1];
523 }
524 return $entexto;
525}
$c
Definition line.php:334
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
dolConvertToWordFrench($num, $langs, $currency='', $centimes=false)
Convert a number into French words.
dol_convertToWord($num, $langs, $currency='', $centimes=false)
Function to return a number into a text.
hundreds2text($hundreds, $tens, $units)
hundreds2text
dolConvertIntToFrenchWords($n, $reform=false)
Convert a non-negative integer into French words (standard French: soixante-dix, quatre-vingts,...
dolNumberToWord($numero, $langs, $numorcurrency='number')
Function to return number or amount in text.