/** @module lib/outils/conversion/nombreEnMots */
// cf https://chiffre-en-lettre.fr/
const dizaineNames = [
'',
'',
'vingt',
'trente',
'quarante',
'cinquante',
'soixante',
'soixante',
'quatre-vingt',
'quatre-vingt'
]
const uniteNames20 = [
'',
'un',
'deux',
'trois',
'quatre',
'cinq',
'six',
'sept',
'huit',
'neuf',
'dix',
'onze',
'douze',
'treize',
'quatorze',
'quinze',
'seize',
'dix-sept',
'dix-huit',
'dix-neuf'
]
const uniteNames = [
// À priori on sert avant milliard|million|mille|cent => 0 et 1 donnent des chaînes vides,
// il faudra gérer 'zéro' et 'un' dans le code
'',
'',
'deux',
'trois',
'quatre',
'cinq',
'six',
'sept',
'huit',
'neuf',
'dix'
]
const fractionNames = [
'',
'dixième',
'centième',
'millième',
'dix-millième',
'cent-millième',
'millionième',
'dix-millionième',
'cent-millionième',
'milliardième',
'dix-milliardième',
'cent-milliardième'
]
function de1a99EnMot (nb, { ordinal } = {}) {
const dizaines = Math.trunc(nb / 10)
let unites = nb % 10
const liaison = (unites === 1 && ![0, 1, 8, 9].includes(dizaines)) ? 'et' : ''
if ([1, 7, 9].includes(dizaines)) unites += 10
let mot = [
dizaineNames[dizaines],
liaison,
uniteNames20[unites]
].filter(s => s).join('-')
if (!ordinal && /.+(vingt|cent)$/.test(mot)) mot += 's'
return mot
}
function de1a1000EnMot (nb, { ordinal } = {}) {
if (nb < 100) return de1a99EnMot(nb, { ordinal })
if (nb === 100) return 'cent'
if (nb === 1000) return 'mille'
// y’a au moins une centaine et qq
const centaines = Math.trunc(nb / 100)
const reste = nb % 100
let resultat = uniteNames[centaines]
if (resultat) resultat += '-'
resultat += 'cent'
if (reste) {
resultat += '-' + de1a99EnMot(reste, { ordinal })
} else if (!ordinal && centaines > 1) {
resultat += 's'
}
return resultat
}
/**
* Retourne nb écrit en un mot
* @param {number} nb Entier compris entre 0 et 999 999 999 999
* @param {Object} [options]
* @param {boolean} [options.ordinal] passer true pour un ordinal (vingt ou cent reste invariable s’il termine le mot, aurait eu un 's' sinon)
* @return {string}
* @throws {TypeError} si nb n’est pas un entier
* @throws {RangeError} si nb n’est pas entre 0 et 999 999 999 999
*/
export function entierEnMot (nb, { ordinal } = {}) {
if (!Number.isInteger(nb)) throw TypeError('entierEnMot ne peut convertir que des entiers')
if (nb < 0 || nb >= 1e13) throw RangeError(`entierEnMot ne peut convertir que des entiers entre 0 et 999 999 999 999 (${nb} fourni)`)
if (nb === 0) return 'zéro'
if (nb < 100) return de1a99EnMot(nb, { ordinal })
if (nb < 1000) return de1a1000EnMot(nb, { ordinal })
// nb en string de 12 caractères
const snb = String(nb).padStart(12, '0')
// on découpe en 4 morceaux, on veut des numbers
const re = /\d{3}/y // y pour sticky, le lastIndex de la regex avance à chaque exec, cf https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
const chunks = []
while (re.lastIndex < 12) chunks.push(Number(re.exec(snb)))
const [milliards, millions, milliers, unites] = chunks
const pluriel = n => n > 1 ? 's' : ''
const portions = []
if (milliards) {
portions.push(de1a1000EnMot(milliards))
portions.push('milliard' + pluriel(milliards))
}
if (millions) {
portions.push(de1a1000EnMot(millions))
portions.push('million' + pluriel(millions))
}
if (milliers) {
if (milliers > 1) portions.push(de1a1000EnMot(milliers, { ordinal: true }))
portions.push('mille')
}
if (unites) portions.push(de1a1000EnMot(unites, { ordinal }))
return portions.join('-')
}
/**
* Retourne les mots correspondant au nombre (orthographe recommandée depuis 1990)
* @param {number} nb Un nombre entre -1e13 et 1e13 (exclus)
* @param {Object} [options]
* @param {boolean} [options.ajouterUnites=false] Passer true pour que le mot "unités" soit ajouté avant la partie décimale (3,14 => trois unités et quatorze centièmes)
* @param {number} [options.maxDecimales=11] Préciser un nb max de décimales (nb sera alors arrondi), entre 0 et 11
* @param [options.garderZerosNonSignificatifs] Passer true pour conserver les zéros non significatifs (avec maxDecimales à 3 le nb 3,14 renverra "trois et cent-quarante millièmes" plutôt que "trois et quatorze centièmes")
* @return {string}
*/
export function nombreEnMots (nb, { ajouterUnites, maxDecimales = 11, garderZerosNonSignificatifs } = {}) {
if (!Number.isFinite(nb)) throw TypeError('nombre invalide')
if (Math.abs(nb) >= 1e13) throw RangeError('nombreEnMots ne peut convertir que des nombres inférieurs à 10^13')
if (!Number.isInteger(maxDecimales) || maxDecimales > 11 || maxDecimales < 0) {
console.error(Error(`maxDecimales invalide (${maxDecimales}) => ramené à 11`))
maxDecimales = 11
}
if (maxDecimales === 0) return entierEnMot(Math.round(nb))
let prefix = ''
if (nb < 0) {
prefix = 'moins '
nb = Math.abs(nb)
}
// nb est positif
let unite = ''
if (ajouterUnites) {
unite = ' unité'
if (nb >= 2) unite += 's'
}
let strDecim
if (Number.isInteger(nb)) {
if (maxDecimales && garderZerosNonSignificatifs) {
// y’a pas de décimales mais on en veut
strDecim = '0'.repeat(maxDecimales)
} else {
if (nb === 0 && ajouterUnites) return 'zéro unité'
if (nb === 1 && ajouterUnites) return prefix + 'une unité'
return prefix + entierEnMot(nb) + unite
}
}
// y’a des décimales, partie entière
let partieEntiere
if (nb < 1) {
partieEntiere = unite ? 'zéro' : ''
} else if (nb < 2) {
partieEntiere = unite ? 'une' : 'un'
} else {
partieEntiere = entierEnMot(Math.trunc(nb))
}
const liaison = partieEntiere ? ' et ' : ''
// attention, il ne faut pas appliquer .toFixed(maxDecimales) au nombre d’origine
// car si le résultat dépasse 16 caractères (en 64 bits) ça donne n’importe quoi
// On ne peut pas non plus caster en string, avec String() ou .toString(), car ça peut donner du 1e-7 par ex
// Faut utiliser toFixed sur la partie décimale seulement,
// sauf que nb - Math.trunc(nb) est pas fiable non plus (avec 298765432191.1001 ça donne 0.10009765625)
if (!strDecim) {
strDecim = nb < 1
? nb.toFixed(maxDecimales).substr(2)
: String(nb).replace(/\d+\./, '').padEnd(maxDecimales, '0')
// dans le 2e cas ça peut être trop…
if (strDecim.length > maxDecimales) {
const diviseur = Math.pow(10, strDecim.length - maxDecimales)
strDecim = String(Math.round(Number(strDecim) / diviseur))
}
}
if (!garderZerosNonSignificatifs) strDecim = strDecim.replace(/0+$/, '')
const nbDecim = Number(strDecim)
const partieDecimale = entierEnMot(nbDecim)
const uniteDecimale = fractionNames[strDecim.length]
return prefix + partieEntiere + unite + liaison + partieDecimale + ' ' + uniteDecimale + (nbDecim > 1 ? 's' : '')
} // nombreEnMot