2015-01-31 16:02:36 +00:00
|
|
|
|
<?php
|
2024-10-26 11:03:04 +00:00
|
|
|
|
/* Copyright (C) 2015 Laurent Destailleur <eldy@users.sourceforge.net>
|
|
|
|
|
|
* Copyright (C) 2015 Víctor Ortiz Pérez <victor@accett.com.mx>
|
2025-03-15 15:34:35 +00:00
|
|
|
|
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
|
2024-10-26 11:03:04 +00:00
|
|
|
|
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
|
2015-01-31 16:02:36 +00:00
|
|
|
|
*
|
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
|
*
|
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
2019-09-23 19:55:30 +00:00
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
* or see https://www.gnu.org/
|
2015-01-31 16:02:36 +00:00
|
|
|
|
*/
|
2022-11-12 22:16:06 +00:00
|
|
|
|
|
2015-01-31 16:02:36 +00:00
|
|
|
|
/**
|
2019-12-11 22:04:27 +00:00
|
|
|
|
* \file htdocs/core/lib/functionsnumtoword.lib.php
|
2015-01-31 16:02:36 +00:00
|
|
|
|
* \brief A set of functions for Dolibarr
|
|
|
|
|
|
* This file contains all frequently used functions.
|
|
|
|
|
|
*/
|
2018-08-14 10:23:10 +00:00
|
|
|
|
|
2017-02-17 15:39:36 +00:00
|
|
|
|
/**
|
2020-08-05 23:56:42 +00:00
|
|
|
|
* Function to return a number into a text.
|
2020-02-08 16:14:04 +00:00
|
|
|
|
* May use module NUMBERWORDS if found.
|
2018-08-14 10:23:10 +00:00
|
|
|
|
*
|
2020-08-05 23:56:42 +00:00
|
|
|
|
* @param float $num Number to convert (must be a numeric value, like reported by price2num())
|
2019-02-13 23:03:10 +00:00
|
|
|
|
* @param Translate $langs Language
|
2020-12-10 11:01:22 +00:00
|
|
|
|
* @param string $currency ''=number to translate | 'XX'=currency code to use into text
|
|
|
|
|
|
* @param boolean $centimes false=no cents/centimes | true=there is cents/centimes
|
2019-02-13 23:03:10 +00:00
|
|
|
|
* @return string|false Text of the number
|
2017-02-17 15:39:36 +00:00
|
|
|
|
*/
|
2020-12-10 11:01:22 +00:00
|
|
|
|
function dol_convertToWord($num, $langs, $currency = '', $centimes = false)
|
2017-02-17 15:39:36 +00:00
|
|
|
|
{
|
2020-08-06 00:48:16 +00:00
|
|
|
|
//$num = str_replace(array(',', ' '), '', trim($num)); This should be useless since $num MUST be a php numeric value
|
|
|
|
|
|
if (!$num) {
|
|
|
|
|
|
return false;
|
2017-02-17 15:39:36 +00:00
|
|
|
|
}
|
2020-02-04 13:18:37 +00:00
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
// Dedicated converter for French. The generic algorithm below is built on the English
|
|
|
|
|
|
// number structure and cannot express French spelling rules (et, hyphens, agreement of
|
|
|
|
|
|
// cent/vingt, elision of "un" before cent/mille). See dolConvertToWordFrench().
|
|
|
|
|
|
$langcode = $langs->getDefaultLang(0);
|
|
|
|
|
|
if (preg_match('/^fr/i', (string) $langcode)) {
|
|
|
|
|
|
return dolConvertToWordFrench($num, $langs, $currency, $centimes);
|
2017-02-17 15:39:36 +00:00
|
|
|
|
}
|
2020-02-04 13:18:37 +00:00
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
$numbackup = $num;
|
|
|
|
|
|
|
2023-10-13 13:00:51 +00:00
|
|
|
|
if (isModEnabled('numberwords')) {
|
2025-03-15 15:34:35 +00:00
|
|
|
|
$concatWords = $langs->getLabelFromNumber((string) $num, $currency);
|
2020-01-30 14:58:21 +00:00
|
|
|
|
return $concatWords;
|
2020-02-04 13:18:37 +00:00
|
|
|
|
} else {
|
2024-03-18 22:41:24 +00:00
|
|
|
|
$TNum = explode('.', (string) $num);
|
2020-08-06 00:59:46 +00:00
|
|
|
|
|
2025-01-27 15:47:39 +00:00
|
|
|
|
$num = abs((int) $TNum[0]);
|
2020-08-06 00:48:16 +00:00
|
|
|
|
$words = array();
|
|
|
|
|
|
$list1 = array(
|
|
|
|
|
|
'',
|
|
|
|
|
|
$langs->transnoentitiesnoconv('one'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('two'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('three'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('four'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('five'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('six'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('seven'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('eight'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('nine'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('ten'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('eleven'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('twelve'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('thirteen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('fourteen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('fifteen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('sixteen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('seventeen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('eighteen'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('nineteen')
|
|
|
|
|
|
);
|
|
|
|
|
|
$list2 = array(
|
|
|
|
|
|
'',
|
|
|
|
|
|
$langs->transnoentitiesnoconv('ten'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('twenty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('thirty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('forty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('fifty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('sixty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('seventy'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('eighty'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('ninety'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('hundred')
|
|
|
|
|
|
);
|
|
|
|
|
|
$list3 = array(
|
|
|
|
|
|
'',
|
|
|
|
|
|
$langs->transnoentitiesnoconv('thousand'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('million'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('billion'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('trillion'),
|
|
|
|
|
|
$langs->transnoentitiesnoconv('quadrillion')
|
2020-01-30 14:58:21 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
2024-03-19 17:53:45 +00:00
|
|
|
|
$num_length = strlen((string) $num);
|
2020-08-06 00:48:16 +00:00
|
|
|
|
$levels = (int) (($num_length + 2) / 3);
|
|
|
|
|
|
$max_length = $levels * 3;
|
|
|
|
|
|
$num = substr('00'.$num, -$max_length);
|
|
|
|
|
|
$num_levels = str_split($num, 3);
|
|
|
|
|
|
$nboflevels = count($num_levels);
|
|
|
|
|
|
for ($i = 0; $i < $nboflevels; $i++) {
|
|
|
|
|
|
$levels--;
|
2024-08-15 14:57:02 +00:00
|
|
|
|
$hundreds = (int) ((int) $num_levels[$i] / 100);
|
2020-08-06 00:48:16 +00:00
|
|
|
|
$hundreds = ($hundreds ? ' '.$list1[$hundreds].' '.$langs->transnoentities('hundred').($hundreds == 1 ? '' : 's').' ' : '');
|
2024-08-15 14:57:02 +00:00
|
|
|
|
$tens = (int) ((int) $num_levels[$i] % 100);
|
2020-08-06 00:48:16 +00:00
|
|
|
|
$singles = '';
|
|
|
|
|
|
if ($tens < 20) {
|
|
|
|
|
|
$tens = ($tens ? ' '.$list1[$tens].' ' : '');
|
|
|
|
|
|
} else {
|
2026-02-27 14:22:47 +00:00
|
|
|
|
$tsd = (int) ($tens / 10); // tens digit (2-9)
|
|
|
|
|
|
$usd = (int) ($tens % 10); // units digit (0-9)
|
|
|
|
|
|
|
|
|
|
|
|
// French-style systems: 70-79 = sixty + (10-19), 90-99 = eighty + (10-19)
|
|
|
|
|
|
// Detection: if the translation of "seventy"/"ninety" contains the word for "ten"
|
|
|
|
|
|
$tenWord = trim($list1[10]); // e.g. "dix"
|
|
|
|
|
|
if ($usd > 0 && ($tsd == 7 || $tsd == 9) && $tenWord !== '' && strpos(trim($list2[$tsd]), $tenWord) !== false) {
|
|
|
|
|
|
// Use the base ten word (60 for 70s, 80 for 90s) + teen word (11-19)
|
|
|
|
|
|
$baseWord = trim($list2[$tsd - 1]);
|
|
|
|
|
|
// Remove trailing 's' (e.g. quatre-vingts → quatre-vingt when used in composition)
|
|
|
|
|
|
if (substr($baseWord, -1) === 's') {
|
|
|
|
|
|
$baseWord = substr($baseWord, 0, -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
$tens = ' '.$baseWord.' ';
|
|
|
|
|
|
$singles = ' '.$list1[10 + $usd].' ';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$tens = ' '.$list2[$tsd].' ';
|
|
|
|
|
|
$singles = ' '.$list1[$usd].' ';
|
|
|
|
|
|
}
|
2020-08-06 00:48:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
$words[] = $hundreds.$tens.$singles.(($levels && (int) ($num_levels[$i])) ? ' '.$list3[$levels].' ' : '');
|
|
|
|
|
|
} //end for loop
|
|
|
|
|
|
$commas = count($words);
|
|
|
|
|
|
if ($commas > 1) {
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$commas -= 1;
|
2020-08-06 00:48:16 +00:00
|
|
|
|
}
|
2020-01-30 14:58:21 +00:00
|
|
|
|
$concatWords = implode(' ', $words);
|
|
|
|
|
|
// Delete multi whitespaces
|
|
|
|
|
|
$concatWords = trim(preg_replace('/[ ]+/', ' ', $concatWords));
|
|
|
|
|
|
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if (!empty($currency)) {
|
2020-01-30 14:58:21 +00:00
|
|
|
|
$concatWords .= ' '.$currency;
|
|
|
|
|
|
}
|
2018-08-14 10:23:10 +00:00
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
// If we need to write cents, spell the 2-digit cents value (e.g. 0.07 => 7 cents, not 70)
|
|
|
|
|
|
$tmptab = explode('.', number_format((float) abs($numbackup), 2, '.', ''));
|
|
|
|
|
|
$cents = isset($tmptab[1]) ? (int) $tmptab[1] : 0;
|
2020-08-05 23:56:42 +00:00
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
if ($cents > 0) {
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if (!empty($currency)) {
|
|
|
|
|
|
$concatWords .= ' '.$langs->transnoentities('and');
|
|
|
|
|
|
}
|
2020-08-05 23:56:42 +00:00
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
$concatWords .= ' '.dol_convertToWord((float) $cents, $langs, '', false);
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if (!empty($currency)) {
|
|
|
|
|
|
$concatWords .= ' '.$langs->transnoentities('centimes');
|
|
|
|
|
|
}
|
2020-01-30 14:58:21 +00:00
|
|
|
|
}
|
2020-08-06 00:48:16 +00:00
|
|
|
|
return $concatWords;
|
2017-02-17 15:39:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2018-08-14 10:23:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
FIX: French amount in words (cent/vingt agreement, et, hyphens, mille/million, centimes) (#38945)
Replace the English-structured algorithm (only translated word by word) with a
dedicated French converter for fr_* languages. It handles the French rules the
generic algorithm cannot express:
- elision of "un" before cent/mille (cent, mille, not "un cent"/"un mille")
- agreement of cent/quatre-vingt (deux cents, trois cent soixante, quatre-vingts,
quatre-vingt mille)
- "et" for 21/31/41/51/61/71 (vingt et un, soixante et onze)
- hyphenation, configurable via CONVERT_TO_WORD_FR (PRE1990 default / REFORME1990)
- plural of million/milliard
Also fix the cents extraction for all languages (0.07 gave "seventy" instead of
"seven") and render the decimal part as "et X centime(s)".
Add a detailed PHPUnit test (100+ French values) in test/phpunit.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-06-22 12:34:52 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Convert a non-negative integer into French words (standard French: soixante-dix,
|
|
|
|
|
|
* quatre-vingts, quatre-vingt-dix). Handles "et", hyphens and the agreement of
|
|
|
|
|
|
* "cent" and "quatre-vingt(s)".
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $n Integer to convert (sign is ignored)
|
|
|
|
|
|
* @param bool $reform false = traditional spelling (hyphen only between tens and units),
|
|
|
|
|
|
* true = 1990 rectified spelling (hyphens everywhere)
|
|
|
|
|
|
* @return string Number written in French words
|
|
|
|
|
|
*/
|
|
|
|
|
|
function dolConvertIntToFrenchWords($n, $reform = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
$n = (int) $n;
|
|
|
|
|
|
if ($n < 0) {
|
|
|
|
|
|
$n = -$n;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($n === 0) {
|
|
|
|
|
|
return 'zéro';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$sep = $reform ? '-' : ' ';
|
|
|
|
|
|
$etjoin = $reform ? '-et-' : ' et ';
|
|
|
|
|
|
|
|
|
|
|
|
$units = array(
|
|
|
|
|
|
'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf',
|
|
|
|
|
|
'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize',
|
|
|
|
|
|
'dix-sept', 'dix-huit', 'dix-neuf'
|
|
|
|
|
|
);
|
|
|
|
|
|
$tensmap = array(2 => 'vingt', 3 => 'trente', 4 => 'quarante', 5 => 'cinquante', 6 => 'soixante');
|
|
|
|
|
|
$scales = array('', 'mille', 'million', 'milliard', 'billion', 'billiard', 'trillion');
|
|
|
|
|
|
|
|
|
|
|
|
// Spell a number from 1 to 99
|
|
|
|
|
|
$below100 = function (int $m) use ($units, $tensmap, $etjoin): string {
|
|
|
|
|
|
if ($m < 20) {
|
|
|
|
|
|
return $units[$m] ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
$t = intdiv($m, 10);
|
|
|
|
|
|
$u = $m % 10;
|
|
|
|
|
|
if ($t == 7 || $t == 9) {
|
|
|
|
|
|
// 70-79 = soixante + (10..19), 90-99 = quatre-vingt + (10..19)
|
|
|
|
|
|
$base = ($t == 7) ? 'soixante' : 'quatre-vingt';
|
|
|
|
|
|
if ($t == 7 && $u == 1) {
|
|
|
|
|
|
return $base.$etjoin.$units[10 + $u]; // soixante et onze (but quatre-vingt-onze takes no "et")
|
|
|
|
|
|
}
|
|
|
|
|
|
return $base.'-'.$units[10 + $u];
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($t == 8) {
|
|
|
|
|
|
// 80 = quatre-vingts, 81..89 = quatre-vingt-x (no "s", no "et")
|
|
|
|
|
|
return ($u == 0) ? 'quatre-vingts' : 'quatre-vingt-'.$units[$u];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 20..69
|
|
|
|
|
|
$w = $tensmap[$t] ?? '';
|
|
|
|
|
|
if ($u == 0) {
|
|
|
|
|
|
return $w;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($u == 1) {
|
|
|
|
|
|
return $w.$etjoin.$units[1]; // vingt et un .. soixante et un
|
|
|
|
|
|
}
|
|
|
|
|
|
return $w.'-'.$units[$u];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Split into groups of 3 digits, lowest group first (index 0 = units, 1 = thousands, 2 = millions...)
|
|
|
|
|
|
$groups = array();
|
|
|
|
|
|
$tmp = $n;
|
|
|
|
|
|
while ($tmp > 0) {
|
|
|
|
|
|
$groups[] = $tmp % 1000;
|
|
|
|
|
|
$tmp = intdiv($tmp, 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
$ng = count($groups);
|
|
|
|
|
|
|
|
|
|
|
|
$pieces = array(); // each entry: array('text' => string, 'noun' => bool) ; noun = million/milliard scale
|
|
|
|
|
|
for ($g = $ng - 1; $g >= 0; $g--) {
|
|
|
|
|
|
$val = $groups[$g];
|
|
|
|
|
|
if ($val == 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$h = intdiv($val, 100);
|
|
|
|
|
|
$r = $val % 100;
|
|
|
|
|
|
|
|
|
|
|
|
// "cent" agrees (becomes "cents") only when multiplied and not followed by another numeral:
|
|
|
|
|
|
// plural when terminal (units group) or directly before a noun scale (million+),
|
|
|
|
|
|
// but invariable before "mille".
|
|
|
|
|
|
$centplural = ($h >= 2 && $r == 0 && ($g === 0 || $g >= 2));
|
|
|
|
|
|
|
|
|
|
|
|
$text = '';
|
|
|
|
|
|
if ($h >= 1) {
|
|
|
|
|
|
$text = ($h == 1) ? 'cent' : $units[$h].$sep.'cent';
|
|
|
|
|
|
if ($centplural) {
|
|
|
|
|
|
$text .= 's';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($r > 0) {
|
|
|
|
|
|
$rtext = $below100($r);
|
|
|
|
|
|
// "quatre-vingts" keeps its "s" only when terminal or before a noun scale, not before "mille".
|
|
|
|
|
|
if ($r == 80 && $g === 1) {
|
|
|
|
|
|
$rtext = 'quatre-vingt';
|
|
|
|
|
|
}
|
|
|
|
|
|
$text = ($text === '') ? $rtext : $text.$sep.$rtext;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($g == 1) {
|
|
|
|
|
|
// thousands: "mille" is invariable and takes no leading "un"
|
|
|
|
|
|
$text = ($val == 1) ? 'mille' : $text.$sep.'mille';
|
|
|
|
|
|
$pieces[] = array('text' => $text, 'noun' => false);
|
|
|
|
|
|
} elseif ($g >= 2) {
|
|
|
|
|
|
$scaleword = isset($scales[$g]) ? $scales[$g] : '';
|
|
|
|
|
|
if ($scaleword !== '') {
|
|
|
|
|
|
if ($val >= 2) {
|
|
|
|
|
|
$scaleword .= 's'; // millions, milliards...
|
|
|
|
|
|
}
|
|
|
|
|
|
$text .= ' '.$scaleword; // a noun scale is always separated by a space
|
|
|
|
|
|
}
|
|
|
|
|
|
$pieces[] = array('text' => $text, 'noun' => true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$pieces[] = array('text' => $text, 'noun' => false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$result = '';
|
|
|
|
|
|
foreach ($pieces as $i => $piece) {
|
|
|
|
|
|
if ($i === 0) {
|
|
|
|
|
|
$result = $piece['text'];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// after a noun scale (million+) keep a space, otherwise use the chosen separator
|
|
|
|
|
|
$result .= ($pieces[$i - 1]['noun'] ? ' ' : $sep).$piece['text'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Convert a number into French words. Used by dol_convertToWord() for French languages.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param float $num Number to convert
|
|
|
|
|
|
* @param Translate $langs Language object (used for the "and"/"centime"/"centimes" labels)
|
|
|
|
|
|
* @param string $currency '' = no currency word, otherwise a currency string appended after the integer part
|
|
|
|
|
|
* @param bool $centimes true = render the decimal part as "et X centime(s)"
|
|
|
|
|
|
* @return string Number written in French words
|
|
|
|
|
|
*/
|
|
|
|
|
|
function dolConvertToWordFrench($num, $langs, $currency = '', $centimes = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Spelling convention, set with constant CONVERT_TO_WORD_FR (Home - Setup - Other setup):
|
|
|
|
|
|
// - default/'PRE1990' = traditional spelling (hyphen only between tens and units)
|
|
|
|
|
|
// - 'REFORME1990' = 1990 rectified spelling (hyphens between all the numeral words)
|
|
|
|
|
|
$reform = (getDolGlobalString('CONVERT_TO_WORD_FR') === 'REFORME1990');
|
|
|
|
|
|
|
|
|
|
|
|
$numstr = number_format((float) $num, 2, '.', '');
|
|
|
|
|
|
$negative = false;
|
|
|
|
|
|
if (strpos($numstr, '-') === 0) {
|
|
|
|
|
|
$negative = true;
|
|
|
|
|
|
$numstr = substr($numstr, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
$tmptab = explode('.', $numstr);
|
|
|
|
|
|
$intpart = (int) $tmptab[0];
|
|
|
|
|
|
$cents = isset($tmptab[1]) ? (int) $tmptab[1] : 0;
|
|
|
|
|
|
|
|
|
|
|
|
$result = ($intpart === 0) ? '' : dolConvertIntToFrenchWords($intpart, $reform);
|
|
|
|
|
|
|
|
|
|
|
|
if (!empty($currency)) {
|
|
|
|
|
|
if ($result === '') {
|
|
|
|
|
|
$result = 'zéro';
|
|
|
|
|
|
}
|
|
|
|
|
|
$result .= ' '.$currency;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($cents > 0) {
|
|
|
|
|
|
if ($centimes) {
|
|
|
|
|
|
$label = $langs->transnoentitiesnoconv($cents === 1 ? 'centime' : 'centimes');
|
|
|
|
|
|
$and = $langs->transnoentitiesnoconv('and');
|
|
|
|
|
|
$centtext = dolConvertIntToFrenchWords($cents, $reform).' '.$label;
|
|
|
|
|
|
if ($result === '') {
|
|
|
|
|
|
$result = $centtext;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$result .= ' '.$and.' '.$centtext;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Not an amount: spell the decimal part after "virgule"
|
|
|
|
|
|
$result = ($result === '' ? 'zéro' : $result).' virgule '.dolConvertIntToFrenchWords($cents, $reform);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($result === '') {
|
|
|
|
|
|
$result = 'zéro';
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($negative) {
|
|
|
|
|
|
$result = 'moins '.$result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-01-31 16:02:36 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Function to return number or amount in text.
|
2018-08-14 10:23:10 +00:00
|
|
|
|
*
|
2019-03-11 00:01:15 +00:00
|
|
|
|
* @param float $numero Number to convert
|
|
|
|
|
|
* @param Translate $langs Language
|
|
|
|
|
|
* @param string $numorcurrency 'number' or 'amount'
|
2023-07-25 21:29:34 +00:00
|
|
|
|
* @return string|int Text of the number or -1 in case TOO LONG (more than 1000000000000.99)
|
2024-12-30 00:26:53 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @deprecated Use dol_convertToWord instead
|
2015-01-31 16:02:36 +00:00
|
|
|
|
*/
|
2019-01-27 14:20:16 +00:00
|
|
|
|
function dolNumberToWord($numero, $langs, $numorcurrency = 'number')
|
2015-01-31 16:02:36 +00:00
|
|
|
|
{
|
2020-08-06 00:58:07 +00:00
|
|
|
|
// If the number is negative convert to positive and return -1 if it is too long
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($numero < 0) {
|
|
|
|
|
|
$numero *= -1;
|
|
|
|
|
|
}
|
2020-08-06 00:58:07 +00:00
|
|
|
|
if ($numero >= 1000000000001) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return -1;
|
2020-08-06 00:58:07 +00:00
|
|
|
|
}
|
2017-02-24 13:41:12 +00:00
|
|
|
|
|
2015-03-15 23:47:51 +00:00
|
|
|
|
// Get 2 decimals to cents, another functions round or truncate
|
2019-02-24 19:06:55 +00:00
|
|
|
|
$strnumber = number_format($numero, 10);
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$len = strlen($strnumber);
|
2024-11-04 11:32:13 +00:00
|
|
|
|
$parte_decimal = '00'; // For static analysis, strnumber should contain '.'
|
2021-02-23 21:03:23 +00:00
|
|
|
|
for ($i = 0; $i < $len; $i++) {
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($strnumber[$i] == '.') {
|
|
|
|
|
|
$parte_decimal = $strnumber[$i + 1].$strnumber[$i + 2];
|
2015-03-15 23:47:51 +00:00
|
|
|
|
break;
|
2015-01-31 16:02:36 +00:00
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
2017-02-24 13:41:12 +00:00
|
|
|
|
|
2024-11-04 11:32:13 +00:00
|
|
|
|
/* Dolibarr 3.6.2 doesn't have $langs->default, why ask $lang like a parameter in case it exists? */
|
2022-11-14 09:09:10 +00:00
|
|
|
|
if (((is_object($langs) && $langs->getDefaultLang(0) == 'es_MX') || (!is_object($langs) && $langs == 'es_MX')) && $numorcurrency == 'currency') {
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($numero >= 1 && $numero < 2) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return ("UN PESO ".$parte_decimal." / 100 M.N.");
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} elseif ($numero >= 0 && $numero < 1) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return ("CERO PESOS ".$parte_decimal." / 100 M.N.");
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} elseif ($numero >= 1000000 && $numero < 1000001) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return ("UN MILLÓN DE PESOS ".$parte_decimal." / 100 M.N.");
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} elseif ($numero >= 1000000000000 && $numero < 1000000000001) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return ("UN BILLÓN DE PESOS ".$parte_decimal." / 100 M.N.");
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} else {
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$entexto = "";
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$number = $numero;
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($number >= 1000000000) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$CdMMillon = (int) ($numero / 100000000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $CdMMillon * 100000000000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$DdMMillon = (int) ($numero / 10000000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $DdMMillon * 10000000000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$UdMMillon = (int) ($numero / 1000000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $UdMMillon * 1000000000;
|
2019-02-24 19:06:55 +00:00
|
|
|
|
$entexto .= hundreds2text($CdMMillon, $DdMMillon, $UdMMillon);
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= " MIL ";
|
2024-11-04 11:32:13 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
$CdMMillon = 0;
|
|
|
|
|
|
$DdMMillon = 0;
|
|
|
|
|
|
$UdMMillon = 0;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($number >= 1000000) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$CdMILLON = (int) ($numero / 100000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $CdMILLON * 100000000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$DdMILLON = (int) ($numero / 10000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $DdMILLON * 10000000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$udMILLON = (int) ($numero / 1000000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $udMILLON * 1000000;
|
2019-02-24 19:06:55 +00:00
|
|
|
|
$entexto .= hundreds2text($CdMILLON, $DdMILLON, $udMILLON);
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if (!$CdMMillon && !$DdMMillon && !$UdMMillon && !$CdMILLON && !$DdMILLON && $udMILLON == 1) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= " MILLÓN ";
|
2021-02-23 21:03:23 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
$entexto .= " MILLONES ";
|
|
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
2024-11-04 11:32:13 +00:00
|
|
|
|
|
2015-03-15 23:47:51 +00:00
|
|
|
|
if ($number >= 1000) {
|
|
|
|
|
|
$cdm = (int) ($numero / 100000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $cdm * 100000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$ddm = (int) ($numero / 10000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $ddm * 10000;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$udm = (int) ($numero / 1000);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $udm * 1000;
|
2019-02-24 19:06:55 +00:00
|
|
|
|
$entexto .= hundreds2text($cdm, $ddm, $udm);
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($cdm || $ddm || $udm) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= " MIL ";
|
2021-02-23 21:03:23 +00:00
|
|
|
|
}
|
2024-11-04 11:32:13 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
$ddm = 0;
|
|
|
|
|
|
$cdm = 0;
|
|
|
|
|
|
$udm = 0;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
$c = (int) ($numero / 100);
|
2024-08-07 00:53:45 +00:00
|
|
|
|
$numero -= $c * 100;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$d = (int) ($numero / 10);
|
|
|
|
|
|
$u = (int) $numero - $d * 10;
|
2019-02-24 19:06:55 +00:00
|
|
|
|
$entexto .= hundreds2text($c, $d, $u);
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if (!$cdm && !$ddm && !$udm && !$c && !$d && !$u && $number > 1000000) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= " DE";
|
2021-02-23 21:03:23 +00:00
|
|
|
|
}
|
2020-08-06 01:55:47 +00:00
|
|
|
|
$entexto .= " PESOS ".$parte_decimal." / 100 M.N.";
|
2015-01-31 16:02:36 +00:00
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return $entexto;
|
|
|
|
|
|
}
|
2023-07-12 14:32:50 +00:00
|
|
|
|
return -1;
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
2015-01-31 16:02:36 +00:00
|
|
|
|
|
2016-04-09 13:07:33 +00:00
|
|
|
|
/**
|
2016-04-09 17:50:13 +00:00
|
|
|
|
* hundreds2text
|
2018-08-14 10:23:10 +00:00
|
|
|
|
*
|
2016-04-09 17:50:13 +00:00
|
|
|
|
* @param integer $hundreds Hundreds
|
|
|
|
|
|
* @param integer $tens Tens
|
|
|
|
|
|
* @param integer $units Units
|
2018-08-14 10:23:10 +00:00
|
|
|
|
* @return string
|
2016-04-09 13:07:33 +00:00
|
|
|
|
*/
|
2016-04-09 17:50:13 +00:00
|
|
|
|
function hundreds2text($hundreds, $tens, $units)
|
|
|
|
|
|
{
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($hundreds == 1 && $tens == 0 && $units == 0) {
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return "CIEN";
|
|
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$centenas = array("CIENTO", "DOSCIENTOS", "TRESCIENTOS", "CUATROCIENTOS", "QUINIENTOS", "SEISCIENTOS", "SETECIENTOS", "OCHOCIENTOS", "NOVECIENTOS");
|
|
|
|
|
|
$decenas = array("", "", "TREINTA ", "CUARENTA ", "CINCUENTA ", "SESENTA ", "SETENTA ", "OCHENTA ", "NOVENTA ");
|
|
|
|
|
|
$veintis = array("VEINTE", "VEINTIUN", "VEINTIDÓS", "VEINTITRÉS", "VEINTICUATRO", "VEINTICINCO", "VEINTISÉIS", "VEINTISIETE", "VEINTIOCHO", "VEINTINUEVE");
|
|
|
|
|
|
$diecis = array("DIEZ", "ONCE", "DOCE", "TRECE", "CATORCE", "QUINCE", "DIECISÉIS", "DIECISIETE", "DIECIOCHO", "DIECINUEVE");
|
2020-09-07 08:18:17 +00:00
|
|
|
|
$unidades = array("UN", "DOS", "TRES", "CUATRO", "CINCO", "SEIS", "SIETE", "OCHO", "NUEVE");
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto = "";
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($hundreds != 0) {
|
|
|
|
|
|
$entexto .= $centenas[$hundreds - 1];
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($tens > 2) {
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($hundreds != 0) {
|
|
|
|
|
|
$entexto .= " ";
|
|
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$entexto .= $decenas[$tens - 1];
|
|
|
|
|
|
if ($units != 0) {
|
2015-01-31 16:02:36 +00:00
|
|
|
|
$entexto .= " Y ";
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$entexto .= $unidades[$units - 1];
|
2015-01-31 16:02:36 +00:00
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
return $entexto;
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} elseif ($tens == 2) {
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($hundreds != 0) {
|
|
|
|
|
|
$entexto .= " ";
|
|
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= " ".$veintis[$units];
|
|
|
|
|
|
return $entexto;
|
2020-05-21 13:05:19 +00:00
|
|
|
|
} elseif ($tens == 1) {
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($hundreds != 0) {
|
|
|
|
|
|
$entexto .= " ";
|
|
|
|
|
|
}
|
2015-03-15 23:47:51 +00:00
|
|
|
|
$entexto .= $diecis[$units];
|
|
|
|
|
|
return $entexto;
|
|
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
if ($units != 0) {
|
2021-02-23 21:03:23 +00:00
|
|
|
|
if ($hundreds != 0 || $tens != 0) {
|
|
|
|
|
|
$entexto .= " ";
|
|
|
|
|
|
}
|
2020-02-06 09:49:12 +00:00
|
|
|
|
$entexto .= $unidades[$units - 1];
|
2015-03-15 23:47:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
return $entexto;
|
2015-01-31 16:02:36 +00:00
|
|
|
|
}
|