web-dev-qa-db-ja.com

JavaScriptで任意の数値の小数点以下の桁数を取得する信頼できる方法はありますか?

丸め関数を探しているわけではないことに注意してください。小数点以下の桁数を任意の数の簡略化された10進数表現で返す関数を探しています。つまり、次のようになります。

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

私の最初の本能は、文字列表現を使用することでした:'.'、次に'e-'で、次のように計算します(例は詳細です):

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;
  }
}

上記の私の例では、この関数は機能します。ただし、可能なすべての値をテストするまで満足しなかったため、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

これは最初は妥当なように見えましたが、その後、ダブルテイクで5e-324 * 105e-323!そして、それは私を襲った:私は非常に小さい数の量子化の影響を扱っている。保存前に数値が量子化されるだけでなく、さらに、2進数で格納されている一部の数値は、10進数表現が不当に長いため、10進数表現が切り捨てられています。これは、文字列表現を使用して真の10進数の精度を得ることができないことを意味するため、私には不幸です。

StackOverflowコミュニティに来ました。数の真の小数点以下の精度を得る信頼できる方法を知っている人はいますか?

この関数の目的は、誰かが尋ねれば、浮動小数点を単純化された分数に変換する別の関数で使用することです(つまり、比較的素な整数の分子とゼロ以外の自然分母を返します)。この外側の関数で欠けている唯一の部分は、浮動小数点数の小数点以下の桁数を決定するための信頼できる方法です。そのため、適切な10の累乗を掛けることができます。うまくいけば、考えすぎています。

31
Milosz

歴史的注記:以下のコメントスレッドは、最初と2番目の実装を参照している場合があります。バグのある実装でリードすると混乱が生じたため、2017年9月に注文を入れ替えました。

_"0.1e-100"_を101にマップするものが必要な場合は、次のようなことを試すことができます

_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
}
_

仕様によると、組み込みの数値から文字列への変換に基づくソリューションは、指数を超えて21桁までしか正確ではありません。

9.8.1数値型に適用されるToString

  1. それ以外の場合は、n、k、およびsを整数として、k≥1、10k-1≤s <10k、s×10n-kの数値はm、kは可能な限り小さくします。 kはsの10進数表現の桁数であり、sは10で割り切れないことに注意してください。また、sの最下位桁は必ずしもこれらの基準によって一意に決定されるわけではありません。
  2. K≤n≤21の場合、sの10進数表現のk桁(先頭に0がない順)と、それに続くn-k個の文字 ‘0’で構成される文字列を返します。
  3. 0 <n≤21の場合、sの10進数表現の最上位n桁で構成される文字列を返し、その後に小数点「。」が続き、sの10進数表現の残りのk-n桁が続きます。
  4. −6 <n≤0の場合、文字「0」、それに続く小数点「。」、それに続く文字「0」の−n回、その後に続く10進数表現のk桁で構成される文字列を返します。 s。

歴史的注記:以下の実装には問題があります。コメントスレッドのコンテキストとしてここに残します。

_Number.prototype.toFixed_ の定義に基づいて、次のように機能するようですが、IEEE-754のdouble値の表現により、特定の数値は誤った結果を生成します。たとえば、decimalPlaces(0.123)は_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 (!)_
18
Mike Samuel

まあ、私は浮動小数点数に10のべき乗を掛けると整数になるという事実に基づいたソリューションを使用します。

たとえば、3.14 * 10 ^ 2を掛けると、314(整数)になります。指数は、浮動小数点数の小数を表します。

ですから、10の累乗を増やして徐々に浮動小数点を乗算すると、最終的に解に到達すると思いました。

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
Edwin Dalorzo

これは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
gion_13

2017年アップデート

これは、エドウィンの回答に基づいた簡略版です。これにはテストスイートがあり、NaN、無限大、指数表記、0.0029や0.0408などの連続する分数の問題のある表現を含む数値を含む、コーナーケースの正しい小数を返します。これは、金融アプリケーションの大部分をカバーします。ここで、_0.0408_は、小数点以下が4(6ではない)の方が、3.14e-21が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));_

トレードオフは、メソッドが最大10の保証された小数に制限されることです。それはより多くの小数を正しく返すかもしれませんが、それに依存しないでください。 1e-10より小さい数値はゼロと見なされ、関数は0を返します。この特定の値は、11.6894のコーナーケースを正しく解決するために選択されたもので、10の累乗を乗算する単純な方法は失敗します(4ではなく5を返します)。 )。

しかし、これは 5番目 であり、0.0029、0.0408、0.1584、4.3573の後、私が発見したコーナーケースです。それぞれの後で、精度を小数点以下1桁に下げる必要がありました。この関数が10進数の誤った数を返す可能性がある10未満の10進数を持つ他の数値があるかどうかはわかりません。安全のために、 任意精度ライブラリ を探します。

文字列に変換して_._で分割することは、最大7桁までの解決策にすぎないことに注意してください。 String(0.0000007) === "7e-7"。それとももっと少ないですか?浮動小数点表現は直感的ではありません。

3
Dan Dascalescu

保存する前に数値が量子化されるだけではありません。さらに、2進数で格納されている一部の数値は、10進表記が不当に長いため、10進表記が切り捨てられています。

JavaScriptは IEEE-754 倍精度(64ビット)形式を使用して数値を表します。私が理解しているように、これにより53ビットの精度、つまり15桁から16桁の10進数が得られます。

したがって、桁数が多い場合は、概算になります。 this thread で言及されているライブラリを含む、より多くの精度で大きな数を処理するためのライブラリがいくつかあります。

1
nnnnnn

これは私のために働く

const decimalPlaces = value.substring(value.indexOf('.') + 1).length;

このメソッドは、値が標準の数値であることを想定しています。

0
nick