legacy/outils/fonctions/ecritureNombre.js

/**
 * Vérifie si le nombre est bien écrit (ne gère pas l'écriture scientifique 1e3)
 * L'ancienne syntaxe `verifNombreBienEcrit (nombreStr, checkSpace, checkUselessZero, ignoreVirguleInutile)` fonctionne aussi
 * @param {string} nombreStr L'écriture du nombre à vérifier
 * @param {Object} [options]
 * @param {boolean} [options.checkSpace=true] Passer false pour ignorer les espaces (ils seront tolérés n'importe où)
 * @param {boolean} [options.checkUselessZero=true] Passer false pour ignorer les zéros inutiles
 * @param {boolean} [options.ignoreVirguleInutile=false] Passer true pour tolérer une virgule sans partie décimale
 * @param {boolean} [options.laxStartingSpace=false] Passer true pour tolérer des espaces au début (avant ou après le signe)
 * @returns {{good: boolean, erreur: string, remede: string, nb: number|undefined}} nb fourni si good
 */
export function verifNombreBienEcrit (nombreStr, options) {
  let checkSpace, checkUselessZero, ignoreVirguleInutile, laxStartingSpace, laxEndingSpace
  if (arguments.length > 2) {
    checkSpace = Boolean(arguments[1])
    checkUselessZero = Boolean(arguments[2])
    ignoreVirguleInutile = Boolean(arguments[3])
  } else if (typeof options === 'boolean') {
    checkSpace = Boolean(arguments[1])
    checkUselessZero = false
    ignoreVirguleInutile = false
  } else {
    // options en objet, les valeurs par défaut deviennent true
    ({ checkSpace = true, checkUselessZero = true, ignoreVirguleInutile = false, laxStartingSpace = false, laxEndingSpace = false } = options ?? {})
  }
  if (typeof nombreStr === 'number') {
    console.warn(TypeError('Il faut appeler cette fonction avec une string'))
    nombreStr = String(nombreStr)
  }
  if (typeof nombreStr !== 'string') {
    throw Error(`nombre invalide : ${nombreStr}`)
  }
  if (/\de[+-]?\d+/.test(nombreStr)) {
    // c'est un nombre en écriture scientifique, on ne le gère pas ici
    throw RangeError(`Nombre hors du champ d’analyse : ${nombreStr}`)
  }

  nombreStr = nombreStr
    // on remplace le(s) point(s) par une virgule
    .replace(/\./g, ',')
    // et d'éventuel espaces fins insécables mis par les ZonesStyleMathquill
    // eslint-disable-next-line no-irregular-whitespace
    .replace(/ /g, ' ')

  // on nettoie les espaces qu'on nous demande d'ignorer
  if (!checkSpace) {
    nombreStr = nombreStr.replace(/ /g, '')
  }
  if (laxStartingSpace) {
    nombreStr = nombreStr.replace(/^ *([+-]?) */, '$1')
  }
  if (laxEndingSpace) {
    nombreStr = nombreStr.replace(/ +$/, '')
  }
  // pas d'espace au début
  if (nombreStr.startsWith(' ')) {
    return {
      good: false,
      erreur: 'espaceDebut',
      remede: 'Il ne doit pas y avoir d’espace au début du nombre !'
    }
  }
  // récup du signe, on cherche pas plus loin si y'en a plusieurs
  if (/[+-].*[+-]/.test(nombreStr)) {
    return {
      good: false,
      erreur: 'pbSigne',
      remede: 'Un nombre ne peut pas avoir deux signes !'
    }
  }
  // check si le signe éventuel est bien au début (on ignore les espaces avant traités ensuite)
  const isNegatif = nombreStr.startsWith('-')
  if (isNegatif) {
    nombreStr = nombreStr.substring(1)
  } else if (nombreStr.startsWith('+')) {
    nombreStr = nombreStr.substring(1)
  }
  if (/[+-]/.test(nombreStr)) {
    return {
      good: false,
      erreur: 'pbSigne',
      remede: 'Le signe n’est pas au début du nombre !'
    }
  }
  // nombreStr est désormais sans signe
  if (nombreStr.startsWith(' ')) {
    return {
      good: false,
      erreur: 'espaceDebut',
      remede: 'Il ne doit pas y avoir d’espace entre le signe et le nombre !'
    }
  }
  if (nombreStr.endsWith(' ')) {
    return {
      good: false,
      erreur: 'espaceFin',
      remede: 'Il ne doit pas y avoir d’espace à la fin du nombre !'
    }
  }

  const intrus = nombreStr.replace(/[ 0-9,]/g, '')
  if (intrus) {
    const pl = intrus.length > 1 ? 's' : ''
    return {
      good: false,
      erreur: 'intrus',
      remede: `Ce${pl} caractère${pl} ne peu${pl ? 'ven' : ''}t pas être dans un nombre : ${intrus}`
    }
  }

  // ok, nb sans signe et sans espace aux extrémités
  if (nombreStr.includes('  ')) {
    return {
      good: false,
      erreur: 'espaceDouble',
      remede: 'Tu as mis 2 espaces côte à côte !'
    }
  }
  // on passe à la virgule
  if (nombreStr.indexOf(',') !== nombreStr.lastIndexOf(',')) {
    return {
      good: false,
      erreur: 'virguleMultiple',
      remede: 'Il ne peut y avoir qu’une seule virgule dans un nombre !'
    }
  }
  // position de la virgule
  if (nombreStr.startsWith(',')) {
    return {
      good: false,
      erreur: 'virguleDebut',
      remede: 'Un nombre ne peut pas commencer par une virgule !'
    }
  }

  let [partieEntiere, partieDecimale] = nombreStr.split(',')
  if (partieDecimale == null) partieDecimale = ''

  if (!checkUselessZero) {
    // pour la partie entière faut garder le dernier si c'est le seul
    if (partieEntiere.startsWith('0')) {
      partieEntiere = partieEntiere.replace(/^0+/, '')
      if (!partieEntiere) partieEntiere = '0'
    }
    partieDecimale = partieDecimale.replace(/0+$/, '')
  }
  if ((partieEntiere.startsWith('0') && partieEntiere.length > 1) || partieDecimale.endsWith('0')) {
    return {
      good: false,
      erreur: 'zeroInutile',
      remede: 'Tu dois supprimer tous les zéros inutiles !'
    }
  }
  if (!ignoreVirguleInutile && /,[ 0]*$/.test(nombreStr)) {
    return {
      good: false,
      erreur: 'virguleInutile',
      remede: 'Pas besoin de virgule quand la partie décimale est nulle !'
    }
  }
  if (partieEntiere.endsWith(' ')) {
    return {
      good: false,
      erreur: 'espaceFinEnt',
      remede: 'Il ne doit pas y avoir d’espace à la fin de la partie entière !'
    }
  }
  if (partieDecimale.startsWith(' ')) {
    return {
      good: false,
      erreur: 'espaceDebDec',
      remede: 'Il ne doit pas y avoir d’espace après la virgule !'
    }
  }

  if (checkSpace) {
    // test que les espaces sont bien les séparateurs de milliers
    let comptgrp = 0
    let comptgrpold = 3
    let ing = false
    // on part de la droite pour la partie entière
    for (let i = partieEntiere.length - 1; i > -1; i--) {
      if (partieEntiere[i] === ' ') {
        if (ing) {
          comptgrpold = comptgrp
          ing = false
        }
        comptgrp = 0
      } else {
        comptgrp++
        ing = true
        if ((comptgrpold !== 3) || (comptgrp > 3)) {
          return {
            good: false,
            erreur: 'espaceEnt',
            remede: 'Dans la partie entière, les chiffres doivent être regroupés par 3 en commençant par la droite !'
          }
        }
      }
    }

    comptgrp = 0
    comptgrpold = 3
    ing = false
    for (const char of partieDecimale) {
      if (char === ' ') {
        if (ing) {
          comptgrpold = comptgrp
        }
        ing = false
        comptgrp = 0
      } else {
        comptgrp++
        ing = true
        if ((comptgrpold !== 3) || (comptgrp > 3)) {
          return {
            good: false,
            erreur: 'espaceDec',
            remede: 'Dans la partie décimale, les chiffres doivent être regroupés par 3 en commençant par la gauche !'
          }
        }
      }
    }
  }

  // recolle tout
  const nbStrCompact = (isNegatif ? '-' : '') + nombreStr
    .replace(',', '.')
    .replace(/ /g, '')
  return {
    good: true,
    nb: parseFloat(nbStrCompact),
    erreur: '',
    remede: ''
  }
}

/**
 * Formate un nombre en string, avec séparateur de milliers
 * @param {number} nb
 * @returns {string}
 */
export function formateNombre (nb) {
  if (!Number.isFinite(nb)) {
    console.error(Error('nombre invalide'), nb)
    return ''
  }
  let signe = ''
  let entier = false
  let nbChaine = String(nb)
  // on gére le cas où nb serait hors scope (1e42 ou 1e-42)
  if (nbChaine.includes('e')) {
    if (nbChaine.includes('e-')) {
      console.error(Error(`nombre trop petit ${nbChaine}`))
    } else {
      console.error(Error(`nombre trop grand ${nbChaine}`))
    }
    return nbChaine
  }
  let pEntiereFormate = ''
  let decimaleFormate = ''
  if (nb < 0) {
    // si le nombre est négatif on récupère le signe"-"
    nbChaine = nbChaine.substring(1)
    signe = '-'
  }
  let [pEntiere, decimale] = nbChaine.split('.')
  if (decimale == null) {
    // le nombre est un entier
    decimale = ''
    entier = true
  }
  if (pEntiere.length > 3) {
    // formatage de la partie entière
    const q = Math.floor(pEntiere.length / 3)
    const r = pEntiere.length % 3

    if (r === 0) {
      pEntiereFormate += pEntiere.substring(0, 3)
      for (let i = 1; i < q; i++) {
        pEntiereFormate += ' ' + pEntiere.substring(i * 3, (i + 1) * 3)
      }
    } else {
      pEntiereFormate += pEntiere.substring(0, r)
      const chaineTemp = pEntiere.substring(r)
      for (let i = 0; i < q; i++) {
        pEntiereFormate += ' ' + chaineTemp.substring(i * 3, (i + 1) * 3)
      }
    }
  } else {
    pEntiereFormate = pEntiere
  }
  // fin du formatage de la partie entière

  if (!entier) {
    // formatage des décimales
    if (decimale.length > 3) {
      decimaleFormate += decimale.substring(0, 3)
      const q1 = Math.floor(decimale.length / 3)
      const r1 = decimale.length % 3

      if (r1 === 0) {
        for (let i = 1; i < q1; i++) {
          decimaleFormate += ' ' + decimale.substring(i * 3, (i + 1) * 3)
        }
      } else {
        const chaineTemp1 = decimale.substring(3 * q1)
        for (let i = 1; i < q1; i++) {
          decimaleFormate += ' ' + decimale.substring(i * 3, (i + 1) * 3)
        }
        decimaleFormate += ' ' + chaineTemp1
      }
    } else decimaleFormate = decimale
  }
  // fin du formatage des décimales

  if (entier) {
    return signe + pEntiereFormate
  }
  return signe + pEntiereFormate + ',' + decimaleFormate
}