Est-il un moyen fiable en JavaScript pour obtenir le nombre de décimales d'un nombre arbitraire?

Il est important de noter que je ne cherche pas de fonction d'arrondi. Je cherche une fonction qui renvoie le nombre de décimales dans la représentation décimale simplifiée d'un nombre arbitraire. Autrement dit, nous avons ce qui suit:

decimalPlaces(5555.0);     //=> 0
decimalPlaces(5555);       //=> 0
decimalPlaces(555.5);      //=> 1
decimalPlaces(555.50);     //=> 1
decimalPlaces(0.0000005);  //=> 7
decimalPlaces(5e-7);       //=> 7
decimalPlaces(0.00000055); //=> 8
decimalPlaces(5.5e-7);     //=> 8

Mon premier instinct était d'utiliser les représentations de chaîne: split on '.', puis on 'e-', et faire le calcul, comme ça (l'exemple est verbeux):

function decimalPlaces(number) {
  var parts = number.toString().split('.', 2),
    integerPart = parts[0],
    decimalPart = parts[1],
    exponentPart;

  if (integerPart.charAt(0) === '-') {
    integerPart = integerPart.substring(1);
  }

  if (decimalPart !== undefined) {
    parts = decimalPart.split('e-', 2);
    decimalPart = parts[0];
  }
  else {
    parts = integerPart.split('e-', 2);
    integerPart = parts[0];
  }
  exponentPart = parts[1];

  if (exponentPart !== undefined) {
    return integerPart.length +
      (decimalPart !== undefined ? decimalPart.length : 0) - 1 +
      parseInt(exponentPart);
  }
  else {
    return decimalPart !== undefined ? decimalPart.length : 0;
  }
}

Pour mes exemples ci-dessus, cette fonction fonctionne. Cependant, je ne suis pas satisfait jusqu'à ce que j'ai testé toutes les valeurs possibles, donc j'ai éclaté Number.MIN_VALUE.

Number.MIN_VALUE;                      //=> 5e-324
decimalPlaces(Number.MIN_VALUE);       //=> 324

Number.MIN_VALUE * 100;                //=> 4.94e-322
decimalPlaces(Number.MIN_VALUE * 100); //=> 324

Cela semblait raisonnable au début, mais ensuite sur une double prise, j'ai réalisé que 5e-324 * 10 devrait être 5e-323! Et puis ça m'a frappé: je fais face aux effets de la quantification de très petits nombres. Non seulement les nombres sont quantifiés avant le stockage; de plus, certains nombres stockés en binaire ont des représentations décimales déraisonnablement longues, de sorte que leurs représentations décimales sont tronquées. C'est dommage pour moi, car il signifie que je ne peux pas obtenir leur vraie précision décimale en utilisant leurs représentations de chaîne.

Je viens donc à vous, communauté StackOverflow. Quelqu'un parmi vous connaît-il un moyen fiable d'obtenir la véritable précision post-décimale d'un nombre?

Le but de cette fonction, si quelqu'un demande, est d'être utilisé dans une autre fonction qui convertit un float en une fraction simplifiée (c'est-à-dire qu'il renvoie le numérateur entier relativement coprime et le dénominateur naturel non nul). Le seul manquant pièce de cette fonction externe est un moyen fiable de déterminer le nombre de décimales dans le flotteur, donc je peux le multiplier par une puissance de 10. J'espère que je vais avoir à y penser il.

26
demandé sur Jonathan Leffler 2012-03-02 23:53:13

6 réponses

Note historique: le fil de commentaire ci-dessous peut faire référence aux première et deuxième implémentations. J'ai échangé la commande en septembre 2017 puisque mener avec une implémentation boguée a causé de la confusion.

Si vous voulez quelque chose qui mappe "0.1e-100" à 101, alors vous pouvez essayer quelque chose comme

function decimalPlaces(n) {
  // Make sure it is a number and use the builtin number -> string.
  var s = "" + (+n);
  // Pull out the fraction and the exponent.
  var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s);
  // NaN or Infinity or integer.
  // We arbitrarily decide that Infinity is integral.
  if (!match) { return 0; }
  // Count the number of digits in the fraction and subtract the
  // exponent to simulate moving the decimal point left by exponent places.
  // 1.234e+2 has 1 fraction digit and '234'.length -  2 == 1
  // 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
  return Math.max(
      0,  // lower limit.
      (match[1] == '0' ? 0 : (match[1] || '').length)  // fraction length
      - (match[2] || 0));  // exponent
}

Selon la spécification, toute solution basée sur la conversion de nombre->chaîne intégrée ne peut être précise qu'à 21 endroits au-delà de l'exposant.

9.8.1 ToString appliqué au nombre Type

  1. sinon, soit n, k et s entiers tels que k ≥ 1, 10k - 1 ≤ s
  2. Si K ≤ N ≤ 21, renvoie la chaîne composée des K chiffres de la représentation décimale de s (dans l'ordre, sans zéros en tête), suivi de n-k occurrences du caractère '0'.
  3. Si 0
  4. Si -6

Note historique: la mise en œuvre ci-dessous est problématique. Je le laisse ici comme contexte pour le fil de commentaire.

Basé sur la définition de Number.prototype.toFixed, Il semble que ce qui suit devrait fonctionner, mais en raison de la représentation IEEE-754 des valeurs doubles, certains nombres produiront de faux résultats. Par exemple, decimalPlaces(0.123) renvoie 20.

function decimalPlaces(number) {
  // toFixed produces a fixed representation accurate to 20 decimal places
  // without an exponent.
  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length;
}

// The OP's examples:
console.log(decimalPlaces(5555.0));  // 0
console.log(decimalPlaces(5555));  // 0
console.log(decimalPlaces(555.5));  // 1
console.log(decimalPlaces(555.50));  // 1
console.log(decimalPlaces(0.0000005));  // 7
console.log(decimalPlaces(5e-7));  // 7
console.log(decimalPlaces(0.00000055));  // 8
console.log(decimalPlaces(5e-8));  // 8
console.log(decimalPlaces(0.123));  // 20 (!)
15
répondu Mike Samuel 2017-09-15 14:27:14

Eh bien, j'utilise une solution basée sur le fait que si vous multipliez un nombre à virgule flottante par la bonne puissance de 10, vous obtenez un entier.

Par exemple, si vous multipliez 3.14 * 10 ^ 2, vous obtenez 314 (un entier). L'exposant représente alors le nombre de décimales du nombre à virgule flottante a.

Donc, j'ai pensé que si je multiplie progressivement un virgule flottante en augmentant des puissances de 10, vous finirez par arriver à la solution.

let decimalPlaces = function () {
   function isInt(n) {
      return typeof n === 'number' && 
             parseFloat(n) == parseInt(n, 10) && !isNaN(n);
   }
   return function (n) {
      const a = Math.abs(n);
      let c = a, count = 1;
      while (!isInt(c) && isFinite(c)) {
         c = a * Math.pow(10, count++);
      }
      return count - 1;
   };
}();

for (const x of [
  0.0028, 0.0029, 0.0408,
  0, 1.0, 1.00, 0.123, 1e-3,
  3.14, 2.e-3, 2.e-14, -3.14e-21,
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));
12
répondu Edwin Dalorzo 2017-06-29 06:53:51

Cela fonctionne pour les nombres inférieurs à e-17:

function decimalPlaces(n){
    var a;
    return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'$1').length)>=1?a:0;
}
3
répondu gion_13 2013-04-01 05:57:51

Mise à jour 2017

Voici une version simplifiée basée sur la réponse D'Edwin. Il a une suite de tests et renvoie le nombre correct de décimales pour les cas de coin, y compris Nan, Infinity, notations d'exposant, et les nombres avec des représentations problématiques de leurs fractions successives, telles que 0.0029 ou 0.0408. Cela couvre la grande majorité des applications financières, où 0.0408 ayant 4 décimales (pas 6) est plus important que 3.14 e-21 ayant 23.

function decimalPlaces(n) {
  function hasFraction(n) {
    return Math.abs(Math.round(n) - n) > 1e-10;
  }

  let count = 0;
  // multiply by increasing powers of 10 until the fractional part is ~ 0
  while (hasFraction(n * (10 ** count)) && isFinite(10 ** count))
    count++;
  return count;
}

for (const x of [
  0.0028, 0.0029, 0.0408, 0.1584, 4.3573, // corner cases against Edwin's answer
  11.6894,
  0, 1.0, 1.00, 0.123, 1e-3, -1e2, -1e-2, -0.1,
  NaN, 1E500, Infinity, Math.PI, 1/3,
  3.14, 2.e-3, 2.e-14,
  1e-9,  // 9
  1e-10,  // should be 10, but is below the precision limit
  -3.14e-13,  // 15
  3.e-13,  // 13
  3.e-14,  // should be 14, but is below the precision limit
  123.12345678901234567890,  // 14, the precision limit
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));

Le le compromis est que la méthode est limitée à un maximum de 10 décimales garanties. Il peut renvoyer plus de décimales correctement, mais ne comptez pas sur cela. Les nombres inférieurs à 1e-10 peuvent être considérés comme zéro, et la fonction retournera 0. Cette valeur particulière a été choisie pour résoudre correctement le cas de coin 11.6894, pour lequel la méthode simple de multiplier par des puissances de 10 échoue (elle renvoie 5 au lieu de 4).

Cependant, c'est le 5ème cas de coin que j'ai découvert, après 0.0029, 0.0408, 0.1584 et 4.3573. Après chaque, j'ai dû réduire la précision d'une décimale. Je ne sais pas s'il y a d'autres nombres avec moins de 10 décimales pour lesquels cette fonction peut renvoyer un nombre incorrect de décimales. Pour être du bon côté, recherchez une bibliothèque de précision arbitraire .

Notez que la conversion en chaîne et le fractionnement par . n'est qu'une solution pour un maximum de 7 décimales. String(0.0000007) === "7e-7". Ou peut-être même moins? La représentation en virgule flottante n'est pas intuitive.

2
répondu Dan Dascalescu 2017-11-16 21:43:26

non seulement les nombres sont quantifiés avant le stockage; de plus, certains nombres stockés en binaire ont des représentations décimales déraisonnablement longues, de sorte que leurs représentations décimales sont tronquées.

JavaScript représente des nombres en utilisant IEEE-754 format double précision (64 bits). Si je comprends bien, cela vous donne une précision de 53 bits, ou quinze à seize chiffres décimaux.

Donc, pour tout nombre avec plus de chiffres, vous obtenez juste une approximation. Il y a quelques bibliothèques autour de gérer de grands nombres avec plus de précision, y compris ceux mentionnés dans Ce thread.

1
répondu nnnnnn 2017-05-23 10:30:08

Basé sur la réponse gion_13, je suis venu avec ceci:

function decimalPlaces(n){
let result= /^-?[0-9]+\.([0-9]+)$/.exec(n);
return result === null ? 0 : result[1].length;
}

for (const x of [
  0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, -3.14e-21,
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));

Il corrige le retour 1 quand il n'y a pas de décimales. Pour autant que je sache, cela fonctionne sans erreurs.

-1
répondu Hoffmann 2017-06-29 02:32:27