2021年6月5日

数値

最新の JavaScript では、2つの数値型があります:

  1. JavaScript での通常の数値は “倍精度” として知られる64ビットフォーマットIEEE-754 で格納されます。ほとんどの場合で使用する数値はこちらであり、本チャプターでもこちらについて説明します。

  2. BigInt は任意の長さの整数を表現するためのものです。通常の数値では 253 を超えることや、 -253 よりも小さい値を扱うことができないため、場合によって必要になります。BigInt は限られた特別な領域で使用されます。BigInt は BigInt で取り上げます。

したがって、このチャプターでは、通常の数値について説明します。では、知識を広げていきましょう。

数値を書く多くの方法

10億を書くことを想像してください。明白な方法は次の通りです:

let billion = 1000000000;

区切りとしてアンダースコア _ を使用することも可能です:

let billion = 1_000_000_000;

ここで、アンダースコア _ は “糖衣構文” の役割をはたし、数値をより読みやすくします。JavaScriptエンジンは単に数字の間にある _ を無視するので、上の 10億とまったく同じです。

ただし、実際には間違えやすさから、通常は何度もゼロを書くのを避けます。また我々は怠け者です。10億を "1bn" としたり、73億を "7.3bn" と書いたりします。大きな数値を表現する場合には大抵これが当てはまります。

JavaScriptでは、数値に文字 "e" を追加し、ゼロの数を指定することで数値を短くします。:

let billion = 1e9;  // 10 億, 文字通り: 1 と 9 個のゼロ

alert( 7.3e9 );  // 73 億 (7,300,000,000)

つまり、"e"1 と与えられたゼロの数をかけ合わせます。

1e3 = 1 * 1000
1.23e6 = 1.23 * 1000000

今度は非常に小さい数値を書いてみましょう。1マイクロ秒(100万分の1秒):

let ms = 0.000001;

先程書いたように、"e" が役立ちます。明示的にゼロを書くのを避けたい場合、このように書くことができます:

let ms = 1e-6; // 1 から左にゼロを6つ

0.000001 の中のゼロの数は6つです。なので 1e-6 になります。

つまり、"e" の後の負値は 1 と与えられたゼロの数で数値を割ったものになります。

// -3 divides by 1 with 3 zeroes
1e-3 = 1 / 1000 (=0.001)

// -6 divides by 1 with 6 zeroes
1.23e-6 = 1.23 / 1000000 (=0.00000123)

16進数、2進数、8進数(Hex, binary and octal numbers)

16進数 の数値は色、エンコード文字やその他多くのものを表現する文字としてJavaScriptで広く使われています。もちろん、それらを短く書く方法があります: 0x とそれに続いて数値を書きます。

例えば:

alert( 0xff ); // 255
alert( 0xFF ); // 255 (同じです。文字の大小は関係ありません)

2進数と8進数はあまり使われませんが、0b, 0o のプレフィックスでサポートされています:

let a = 0b11111111; // 255 の2進数表記
let b = 0o377; // 255 の8進数表記

alert( a == b ); // true, 両方とも同じ数値(255)

このようなサポートを持つ数字体系は3つしかありません。他の数値体系では、parseInt 関数を使う必要があります(このチャプターで後ほど学びます)。

toString(base)

メソッド num.toString(base) は与えられた base の記数法で num の文字列表現を返します。

例:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

base2 から 36 までの値を取ります。デフォルトは 10 です。

一般的なユースケースは次の通りです:

  • base=16 は 16進数の色や文字エンコードなどに利用され、数字は 0..9 または A..F になります。

  • base=2 は主にビット単位の操作をデバッグするためのものです。数字は 01 です。

  • base=36 は最大値です。数字は 0..9 または A..Z です。全てのアルファベットが数値を表現するために使われます。36 が役立つケースは、例えばURLを短くするために、長い数値の識別子を何か短いものに変える必要がある場合です。基数 36 を使うことでシンプルに表現できます。

    alert( 123456..toString(36) ); // 2n9c
メソッドを呼ぶための2つのドット

123456..toString(36) の2つのドットはタイプミスではないことに注意してください。上の例の toString のように、数値に対して直接メソッド呼び出しをしたいとき、その後に2つのドット .. を置く必要があります。

もし1つのドットを置いた場合 123456.toString(36)、エラーになるでしょう。なぜならJavaScript構文は最初のドットの後を小数部分と考えるためです。そして、もう1つドットを置くと、JavaScriptは小数部分が空であることを知り、メソッドと判断します。

また、このように書くこともできます (123456).toString(36).

丸め

数値処理で最も使う操作の1つが丸めです。

丸めにはいくつかの組み込みの関数があります:

Math.floor
切り捨て: 3.13 になり, -1.1-2 になります。
Math.ceil
切り上げ: 3.14 になり, -1.1-1 になります。
Math.round
四捨五入して最も近い整数にする: 3.13 になり, 3.64 になります。 -1.1-1 です。
Math.trunc (Internet Explorer では未サポート)
丸めを使わずに、小数点以下を削除: 3.13 になり, -1.1-1 です。

これらの違いのサマリをテーブルにします:

Math.floor Math.ceil Math.round Math.trunc
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1

これらの関数は、数値の小数点の扱い方の全ての可能性をカバーしています。しかし、小数の後の数値を n-th(n桁) に丸めたいときはどうすればよいでしょうか。

例えば、1.2345 という数値があり、1.23 のみを取り出すような、2桁に丸めたい場合です。

それをするために2つの方法があります:

  1. 乗除算

    例えば、小数第2位で数値を丸めるために、 数値を 100 で乗算し、丸め関数を呼び出した後、それを除算します。

    let num = 1.23456;
    
    alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  2. メソッド toFixed(n) は点の後の数字を “n” 桁に丸め、結果の文字列表現を返します。

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"

    これは、Math.round と似たように、最も近い値に丸めます:

    let num = 12.36;
    alert( num.toFixed(1) ); // "12.4"

    toFixed の結果は文字列であることに注意してください。もし小数部分が指定桁より短い場合、末尾にゼロが挿入されます。:

    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000", 正確に5桁になるよう 0 が追加されます

    単項プラス、または Number() 呼び出しを使うことで、数値に変換することができます: +num.toFixed(5)

精密でない計算

内部的には、数値は 64bit フォーマットIEEE-754で表現されるため、正確に 64bit で数値を格納できます。: そのうち 52bit が数字の格納のために使われ、11bit が 小数点の位置で(整数値のときゼロです。)、 1bit は符号です。

もし、数値が長すぎる場合、64bitの記憶域をオーバーフローし、無限大になる可能性があります:

alert( 1e500 ); // Infinity

少しはっきりしないかもしれませんが、よく起こるのは精度の低下です。

この(偽となる)テストを考えてみましょう:

alert( 0.1 + 0.2 == 0.3 ); // false

上記の通り、もし 0.10.2 の合計が 0.3 かどうかをチェックした場合、false になります。

奇妙です! 0.3 でないのなら、何なのでしょう?

alert( 0.1 + 0.2 ); // 0.30000000000000004

なんと! ここには誤った比較よりも多くの驚きがあります。もしもe-ショッピングのサイトを作っており、訪問者が $0.10$0.20 の商品をカートに入れたと想像してください。注文の総額は $0.30000000000000004 になります。誰もがそれに驚くでしょう。

しかし、なぜこのようなことが起こるのでしょうか?

数値はバイナリ形式で、1と0の並びでメモリ上に格納されます。しかし10進数でシンプルに見える 0.10.2 のような小数は、バイナリ形式では終わることのない小数です。

言い換えると、0.1 とは何でしょう?それは 1 を 10 で割った 1/10 です。10進数では、このような数値は簡単に表現できます。 それと 1/3 を比較してみてください。これは無限の小数 0.33333(3) になります。

従って、10 の累乗による除算は 10進数では上手く動作することが保証されますが、3 による除算は保証されていません。同じ理由で、2進数では、2 の累乗による除算は動作することが保証されていますが、 1/10 は無限の2進数の小数になります。

2進数を使って、 正確な 0.1 または 正確な 0.2 を格納する方法はありません。ちょうど、10進数で 1/3 を小数で正確に表現できないように。

数値形式 IEEE-754 は、可能な限り近い数値に丸めてこれを解決します。 これらの丸めルールでは、通常 “小さな精度損失” は見ることができないので、数値は 0.3 と表示されます。 しかし、損失は依然として存在することに注意してください。

これは次のようにして見ることができます:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

そして、2つの数値の合計をしたとき、それらの “精度損失” は加算されます。

そういうわけで、 0.1 + 0.2 は正確な 0.3 ではありません。

JavaScript だけではありません

同じ問題は多くの他のプログラミング言語で存在します。

PHP, Java, C, Perl, Ruby は全く同じ結果を返します。なぜならそれらは同じ数値形式に基づいているためです。

この問題を回避できるのでしょうか?もちろん、幾つかの方法があります。最も信頼できる方法は、toFixed(n) メソッドにより結果を丸めることです:

let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30

toFixed は常に文字列を返すことに注意してください。それは小数点の後2桁となることを保証します。実際、e-ショッピングを持っていて $0.30 を表示するときに便利です。それ以外のケースでは、数値に変換するために単項プラスを使うことができます。

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3

一時的に数値を 100 (またはより大きな数値)で乗算し、整数に変換し、計算を行ってから除算することもできます。その後、整数で計算しようとすると、エラーはいくらか減少しますが、依然として除算ではまだ異常です。:

alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001

そのため、乗算/除算によるアプローチはエラーは減らせますが、完全に除去することはできません。

もしもショップを扱っている場合、最も抜本的な解決策はセントで全ての価格を格納し、全く小数を使わないことです。しかし、30% のディスカウントを適用するとどうなるでしょうか?実際には、完全に回避することはほぼ不可能なので、上記の解決法はこの落とし穴を回避するのに役立ちます。

興味深いこと

これを試してみてください:

// Hello! 自己増加する数値です!
alert( 9999999999999999 ); // shows 10000000000000000

これも同じ問題によるものです: 精度の損失です。数値は64bitであり、そのうちの52bitが数値を格納するのに使えますが、十分ではありません。従って、最下位の数字は消えます。

JavaScriptはこのようなイベントではエラーをトリガーしません。数値を目的のフォーマットに合わせるよう最善を尽くしますが、このフォーマットは残念ながら十分大きいものではありません。

2つのゼロ

数値の内部表現のもう一つの面白い結果は、2つのゼロ、すなわち 0-0 の存在です。

記号は1ビットで表現されるため、すべての数字は、0を含めて正または負になります。

ほとんどの場合、区別は気になりません。なぜなら演算子はそれらを同じものとして扱うためです。

テスト: isFinite と isNaN

2つの特別な数値を覚えていますか?

  • Infinite (と -Infinite) は何よりも大きい(小さい)特別な数値です。
  • NaN はエラーを表現します。

これらは number 型に属しますが、 “通常の” 数値ではないため、それらをチェックするための特別な関数があります:

  • isNaN(value) はその引数を数値に変換し、NaN であるかをテストします:

    alert( isNaN(NaN) ); // true
    alert( isNaN("str") ); // true

    しかし、この関数は必要なのでしょうか?単に比較 === NaN は使えないのでしょうか?答えは No です。NaN はそれ自身を含めて何とも等しくなく、ユニークです:

    alert( NaN === NaN ); // false
  • isFinite(value) は引数を数値に変換し、それが NaN/Infinity/-Infinity ではなく通常の数値であれば、true を返します。:

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false, 特別な値: NaN なので
    alert( isFinite(Infinity) ); // false, 特別な値: Infinity なので

時々、isFinite は文字列値が通常の数値かどうかをチェックするのに使われます:

let num = +prompt("Enter a number", '');

// Infinity を入力しなければ true です -- Infinity は数値ではありません
alert( isFinite(num) );

空、またはスペースのみの文字列は、isFinite を含めた全ての数値関数で 0 として扱われる点に注意してください。

Object.is を使った比較

値を === のように比較する特別な組み込みメソッド Object.is があります。これは2つの特殊なケースではより信頼できます:

  1. NaN でうまく動作します: Object.is(NaN, NaN) === true、これは良いことです。
  2. 0-0 は異なります: Object.is(0, -0) === false、重要ではありませんが、技術的にはそれらは異なる値です。

上記以外の全てのケースでは、Object.is(a, b)a === b と同じです。

この比較の方法は JavaScriptの仕様でしばしば使われます。内部のアルゴリズムが2つの値が正確に同じかを比較する必要があるとき、Object.is (内部的にはSameValueと呼ばれます)を使います。

parseInt と parseFloat

プラス + または Number() を使った数値変換は厳密です。もし値が厳密な数値でない場合は失敗します:

alert( +"100px" ); // NaN

唯一の例外は文字列の最初、または最後のスペースです。それらは無視されます。

しかし、実際にはCSS で "100px""12pt" のような単位を持つ値を持つことが頻繁にあります。また、多くの国では、通貨記号が金額の後にあるので、"19€" と言った値があり、その中から数値を抽出したいと考えています。

そのために parseIntparseFloat があります。

それらはできるだけ文字列から数値を “読み込み” ます。エラーが起きると、収集された数値が返されます。関数 parseInt は整数を返し、一方で、parseFloat は浮動小数を返します:

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, 整数部のみ返却されます
alert( parseFloat('12.3.4') ); // 12.3, 2つ目の点で読むのをやめます

parseInt/parseFloatNaN を返す状況があります。それは数値が読み込めなかったときに発生します:

alert( parseInt('a123') ); // NaN, 最初の文字で処理が止まります
parseInt(str, radix) の2つ目の引数

parseInt() 関数は任意の2つ目のパラメータを持ちます。それは数値システムの基数を指定するので、parseInt もまた16進数、2進数などの文字列をパースすることが出来ます:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, 0x がなくても動作します

alert( parseInt('2n9c', 36) ); // 123456

他の数学的関数

JavaScript は数学関数と定数の小さなライブラリを含む組み込みのMath オブジェクトを持っています。

いくつか例です:

Math.random()

0 から 1まで (1は含みません)のランダムな数値を返します

alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (any random numbers)
Math.max(a, b, c...) / Math.min(a, b, c...)

任意の数の引数から最大/最小を返します

alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2) ); // 1
Math.pow(n, power)

n を与えられた数だけ累乗します

alert( Math.pow(2, 10) ); // 2 in power 10 = 1024

docs for the Math にある三角法など、Math オブジェクトにはより多くの関数や定数があります。

サマリ

大きな数値を書く場合:

  • 数値へ加えるゼロの数と一緒に "e" を付け加えます。このように: 123e6123 と 6つのゼロです。
  • "e" の後の負の値は、1と与えられたゼロの数で数値を除算します。

異なる数値体系の場合:

  • 16進数(0x)、8進数(0o)や2進数(0b) で直接数値を書くことが出来ます。
  • parseInt(str, base)2 ≤ base ≤ 36 の間の任意の数値システムの整数を解析します。
  • num.toString(base) は数値を、与えられた base を基数とした数値システムの文字列に変換します。

12pt100px のような値を数値に変換する場合:

  • “ソフト” 変換として parseInt/parseFloat を使います。それは文字列から数値を読み込み、エラーが起きる前までに読めた値を返します。

分数の場合:

  • Math.floor, Math.ceil, Math.trunc, Math.round または num.toFixed(precision) を使って丸めます。
  • 分数を扱う際に精度の低下があることを覚えておいてください。

より多くの算術関数:

  • 必要なとき、Math を見てください。このライブラリはとても小さいですが、基本で必要なものはカバーしています。

タスク

重要性: 5

Sum numbers from the visitor

訪問者に2つの数値を入力させ、合計を表示するスクリプトを作りなさい。

デモを実行

P.S. 型の落とし穴があります

let a = +prompt("The first number?", "");
let b = +prompt("The second number?", "");

alert( a + b );

prompt の前の単項プラス + に注意してください。それはすぐに値を数値に変換します。

そうしない場合、ab が文字列となり、合計はそれらの連結になります。つまり: "1" + "2" = "12".

重要性: 4

ドキュメントによると、Math.roundtoFixed は両方とも最も近い数値に丸めます: 0..4 は下がり、5..9 は上がります。

例えば:

alert( 1.35.toFixed(1) ); // 1.4

下の似た例では、なぜ 6.356.4 ではなく 6.3 に丸められるのでしょうか?

alert( 6.35.toFixed(1) ); // 6.3

正しい方法で 6.35 を丸める方法はどうなるでしょう?

内部的には、小数部分 6.35 は無限のバイナリです。このような場合はいつものように、精度が低下されたものが保存されています。

見てみましょう:

alert( 6.35.toFixed(20) ); // 6.34999999999999964473

精度の低下は数値のインクリメント/デクリメント両方を引き起こす可能性があります。この特定のケースで、数値は少し小さくなり、そういう訳で丸め下げされます。

また、1.35 は何でしょう?

alert( 1.35.toFixed(20) ); // 1.35000000000000008882

ここでは、精度の低下は数値を少し大きくしました。なので丸め上げられます。

正しい方法で丸めたい場合、6.35 で問題を解決するにはどうすればいいですか?

そのためには、丸める前に整数に近づける必要があります:

alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

63.5 はまったく精度の低下がないことに注意してください。それは、小数部分 0.5 は実際には 1/2 だからです。2のべき乗で除算された分数は、バイナリシステムで正確に表現されます。今度はそれを丸めることができます:

alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(丸められた) -> 6.4
重要性: 5

訪問者が有効な数値を入力するまで、数値を尋ねる関数 readNumber を作成してください。

結果の値は数値として返す必要があります。

また、訪問者は空行の入力または “CANCEL” の選択により処理を停止することができます。このケースでは、関数は null を返します。

デモを実行

テストと一緒にサンドボックスを開く

function readNumber() {
  let num;

  do {
    num = prompt("Enter a number please?", 0);
  } while ( !isFinite(num) );

  if (num === null || num === '') return null;

  return +num;
}

alert(`Read: ${readNumber()}`);

null/空行を処理する必要がある分、この解答はすこし入り組んでいます。

実際には、入力が「通常の数字」になるまで入力を受け入れます。null (キャンセル)と空行、両方とも数値形式では 0 なので、その条件にフィットしています。

停止した後、null と空行特別に扱う必要がります(null を返す)、なぜならそれらの数値への変換は 0 になるためです。

サンドボックスでテストと一緒に解答を開く

重要性: 4

このループは無限です。終わりません。なぜでしょう?

let i = 0;
while (i != 10) {
  i += 0.2;
}

なぜなら、i は決して 10 と等しくならないからです。

i本当 の値を見るために実行してみましょう。:

let i = 0;
while (i < 11) {
  i += 0.2;
  if (i > 9.8 && i < 10.2) alert( i );
}

ちょうど 10 になるものはありません。

0.2 のような少数を足すときの精度の低下によりこのようなことが起こります。

結論: 小数部分を扱うとき、等価チェックを避けましょう。

重要性: 2

組み込み関数 Math.random()0 から 1 (1 は含まない)のランダムな値を作成します。

min から max (max は含まない)のランダムな小数値を生成する関数 random(min, max) を書いてください。

動作例:

alert( random(1, 5) ); // 1.2345623452
alert( random(1, 5) ); // 3.7894332423
alert( random(1, 5) ); // 4.3435234525

ベースとして、previous task の解法が使えます。

区間 0…1 のすべての値を min から max までの値に “マッピング” する必要があります。

2つのステージでそれができます:

  1. max - min と 0…1 からのランダム値を掛けると、取りうる値の範囲は 0…1 から 0…max – min まで増加します。
  2. ここで、min を足すと、取りうる範囲は min から max になります。

関数:

function random(min, max) {
  return min + Math.random() * (max - min);
}

alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
重要性: 2

取りうる値として minmax を含む、min から max のランダムな 整数 値を生成する関数 randomInteger(min, max) を作りなさい。

範囲 min..max からの任意の値は、同じ確率で現れなければいけません。

動作例:

alert( random(1, 5) ); // 1
alert( random(1, 5) ); // 3
alert( random(1, 5) ); // 5

シンプルだが間違った解法

最もシンプルで、しかし間違った解法は、min から max の値を生成してから丸める方法です:

function randomInteger(min, max) {
  let rand = min + Math.random() * (max - min);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

この関数は動きますが、間違っています。境界値 minmax を得る確率は他の値の半分になります。

上の例を複数回実行してみると、2 が他より多く現れることに簡単に気付くでしょう。

これは、Math.round()1..3 のランダムな数値を取った後に、次のように丸めるためです:

1   から 1.4999999999 までの値は 1 になります
1.5 から 2.4999999999 までの値は 2 になります
2.5 から 2.9999999999 までの値は 3 になります

これで、12 の半分しか取得できないことが明白に分かりますね。3についても同様です。

正しい解法

このタスクを正しく解く方法はたくさんあります。一例は、区間の端を調整することです。同じ区間となるように、値を 0.5 から 3.5 の間で生成します。そうすると境界値の発生確率が上がります:

function randomInteger(min, max) {
  // rand は (min-0.5) から (max+0.5) になりました
  let rand = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

もう一つの例は、min から max+1 のランダムな数値に対して Math.floor を使う方法です。

function randomInteger(min, max) {
  // ここでは rand は min から (max+1) です
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert( randomInteger(1, 3) );

ここでは、区間は次のように割り当てられます:

1 から 1.9999999999 の値は 1 になります
2 から 2.9999999999 の値は 2 になります
3 から 3.9999999999 の値は 3 になります

全ての区間は同じ長さを持つため、最終的な分布が等しくなります。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。