JavaScriptで0.1はなぜ0.1と表示されるのか?
JavaScriptで0.1はなぜ0.1と表示されるのか?
この記事では、正しい結論を導きだせていない可能性が高いですので、ご注意ください。
もちろん、この記事を読んでみて「ここが違う」という指摘は大歓迎です。ご意見がありましたら、ぜひお知らせください。
chromeのコンソールで、「0.1」「0.1+0.2」の計算結果を表示すると、それぞれ「0.1」「0.30000000000000004」と表示されます。
console.log(0.1);
//0.1
console.log(0.1+0.2);
//0.30000000000000004
この記事では、JavaScriptの仕様書を見ながら、下記の疑問について調べてみました。
- なぜ、console.logで表示する結果が、この真の数字と異なるのか?
- なぜ、「0.1」は「0.1」、「0.1+0.2」は「0.30000000000000004」と表示されるのか?
この挙動の鍵はNumber.toString
メソッドにあるらしいので、その中身を重点的に見ていきます。
JavaScriptで「0.1」「0.1+0.2」は、どう扱われるか?
まず、これらの値がJavaScriptで、実際に「どのような値」として管理されているかを確認します。
確認するには、Number.toFixed
メソッドで、引数に十分大きな値を指定しましょう。
実際に、実行してみると、下記のようになります。
console.log((0.1).toFixed(56));
//0.10000000000000000555111512312578270211815834045410156250
console.log((0.1+0.2).toFixed(56));
//0.30000000000000004440892098500626161694526672363281250000
これが、JavaScriptで管理される「数値」の実態です。
一方で、単に、console.log
で値を表示すると、内部的に、Number.toString
メソッドが呼び出され、冒頭のように、適当に端数処理された値が返されます。
なぜ、このような挙動を示すのか、Number.toString
メソッドの挙動を調べていきます。
※ちょっと探してみたのですが、仕様書上、console.log
呼び出し時に、内部的にNumber.toString
が呼び出される、という記載を見つけることができませんでした。この記事では、これ以上深入りはしませんが、もし、仕様書のどこに書かれているかご存じの方がいらしたら、ぜひ、教えてください。
例1. (123456).toString()
まずは、手軽な例として(123456).toString()
を実行したときの挙動を見ていきます。
仕様書の該当箇所は、「ECMAScript 2021 Language Specification」6.1.6.1.20 Number::toStringです。
まず、処理対象の数「x」として、「123456」が渡され、処理が行われます。
The abstract operation Number::toString takes argument x (a Number). It converts x to String format.
次に、処理手順の「1.」~「4.」です。
ここでは、「NaN」「+0」、「-0」、負の数、「Infinity」についての処理が記載されています。
- If x is NaN, return the String "NaN".
- If x is +0𝔽 or -0𝔽, return the String "0".
- If x < +0𝔽, return the string-concatenation of "-" and ! Number::toString(-x).
- If x is +∞𝔽, return the String "Infinity".
今回の「x」(=「123456」)は、どれにも該当しないので、「5.」の処理に進みます。
条件を満たす「n」「k」「s」の値を計算する
処理手順「5.」では、「n」「k」「s」の3つの「整数」値を計算します。
- Otherwise, let n, k, and s be integers such that k ≥ 1, 10k-1 ≤ s < 10k, s × 10n - k is ℝ(x), and k is as small as possible. Note that k is the number of digits in the decimal representation of s, that s is not divisible by 10, and that the least significant digit of s is not necessarily uniquely determined by these criteria.
以下、詳細に見ていきます。
「s」と「k」を決める
「s」は整数値で、かつ、「s × 10n - k」が「ℝ(x)」となるような数です。
「ℝ(x)」が何かは、後で考えるとして、とりあえず、今のところは「ℝ(x)」は「x」と同じ値(=「123456」)と考えて、先に進みましょう。
「s」は整数で、かつ、元の数「x」に10を何回か乗じてできる数なので、「s」の候補は「123456」「1234560」・・・といった数のうちの、いずれかになります。
そして、それぞれの「s」に対して、「k」の値を計算すると、次のようになります。
s | sとkの関係 | k |
---|---|---|
123456 | 106-1 ≦ 123456 < 106 | 6 |
1234560 | 107-1 ≦ 1234560 < 107 | 7 |
12345600 | 108-1 ≦ 12345600 < 108 | 8 |
先ほどの仕様書を読むと、**「k」は「1以上」でかつ「できるだけ小さい数」**にしないといけません。
この中で「k」が一番小さくなるのは、**「s」が「123456」、「k」が「6」**です。これが、目的の「s」「k」の値となります。
この **「k」は「元数値の桁数」**を表しています。
「Number型」と「普通の数値」
※たぶん、この項で書いている解釈は間違えています(結果は合っていると思いますが、過程が間違えているはず)。正しい解釈をご存じの方がいらしたら、ぜひ教えてください。
次に「n」を決めていきたいのですが、その前に、一つ、概念を整理しておきましょう。
仕様書を見ると、「x」と「n」「k」「s」は、「s × 10n - k is ℝ(x)」という関係を満たす、と書かれています。
この文中に出てくる「ℝ(x)」は、「ECMAScript 2021 Language Specification」5.2.5 Mathematical Operationsで定義されています。
A conversion from a Number or BigInt x to a mathematical value is denoted as "the mathematical value of x", or ℝ(x).
「mathmatical value」というのは、プログラミングとは関係ない、普通の算数で使う「数値」(以下、「普通の数値」と表記します)を表しています。
そして「ℝ(x)」は、Number型「x」に対応する「普通の数値」を表すのです。
たとえば、Number型「123456」に対応する「普通の数値」は「123456」です。ですから、「ℝ(123456)」は、普通の数値「123456」という数値を表すことになります。
仕様書上「Number型」の世界と、「普通の数値」の世界は、明確に区分されています。
たとえば、先ほどの「s × 10n - k is ℝ(x)」は、「is」の左側も右側も「普通の数値」で書かれています。
ですから、この文章は、「Number型」の世界ではなく「普通の数値」の世界で考えたときに、「s × 10n - k = ℝ(x)」という意味に解釈することになります。
「n」を決める
では、実際に、先ほどの関係から「n」を決めてみましょう。
先ほど出てきた関係式に、「s」「k」「x」をあてはめると、次のようになります。
「123456 × 10n - 6 = 123456」
ですから、「n」は「6」であることがわかります。
最終的に、元の数と、算出した「n」「k」「s」を表にまとめると次のようになります。
元の数 | n | k | s |
---|---|---|---|
123456 | 6 | 6 | 123456 |
これで、「処理手順5」は終わりです。
「n」「k」「s」の値に応じて、文字列変換を行う。
次に、「処理手順6」に進みます。
- If k ≤ n ≤ 21, return the string-concatenation of:
- the code units of the k digits of the decimal representation of s (in order, with no leading zeroes)
- n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
今回は、このケースに該当するので、この手順に従って表示する値を決定します。
- まず「s」の数値を「k」桁分出力(=「123456」)し
- 次に「n-k」桁分「0」を出力(=今回は何もなし)します
結果、「123456」という文字列が出力されることになります。
例2. (12.3456).toString()
次に、(12.3456).ToString()
を実行したときの挙動を見ていきます。
「処理手順1」~「処理手順4」までは、先ほどと同じように無視することができます。
そこで、「処理手順5」の部分を見ていきます。
条件を満たす「n」「k」「s」の値を計算する
「s」と「k」を決める
「s」と「k」に関しては、結果的には、さきほどとまったく同じになりますが、一応、手順を追ってみましょう。
「s」は整数値で、かつ、「s × 10n - k」の形に書けないといけません。
ですから、「s」は「12.3456」に「10を何回か掛けて整数値にしたもの」(=「123456」「1234560」・・・)のうちのどれかになります。
それぞれの「s」に対して、「k」の値は、次のように計算できます。
s | sとkの関係 | k |
---|---|---|
123456 | 106-1 ≦ 123456 < 106 | 6 |
1234560 | 107-1 ≦ 1234560 < 107 | 7 |
12345600 | 108-1 ≦ 12345600 < 108 | 8 |
「k」を、「1以上」でかつ「できるだけ小さい数」にしようとすると、先ほどと同じく、**「s」が「123456」、「k」が「6」**の組み合わせを採用することになります。
「n」を決める
「s × 10n - k = ℝ(x)」という関係式に、「s」「k」「x」をあてはめると、次のようになります。
「123456 × 10n - 6 = 12.3456」
ここから、「n」は「2」であることがわかります。
参考のため、先ほどの「123456」の場合と、今回の「12.3456」の場合について、元の数と、算出した「n」「k」「s」を表にまとめてみると、次のようになります。
元の数 | n | k | s |
---|---|---|---|
123456 | 6 | 6 | 123456 |
12.3456 | 2 | 6 | 123456 |
「n」が変わりましたが「k」「s」はまったく同じになりました。
「n」「k」「s」の値に応じて、文字列変換を行う。
次に、「処理手順6」以降に進みましょう。
- If k ≤ n ≤ 21, return the string-concatenation of:
- the code units of the k digits of the decimal representation of s (in order, with no leading zeroes)
- n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
今回は、「k」が「6」、「n」が「2」なので、「処理手順6」の条件を満たしません。
次に進みます。
- If 0 < n ≤ 21, return the string-concatenation of:
- the code units of the most significant n digits of the decimal representation of s
- the code unit 0x002E (FULL STOP)
- the code units of the remaining k - n digits of the decimal representation of s
「n」が「2」なので、この「処理手順7」の条件を満たします。
そこで、次のように文字列を出力します。
- 「s」の数値を「n」桁分(=「12」)
- 「.」を出力し
- 「s」の数値の残り「k-n」桁分(=「3456」)を出力します。
その結果、「12.3456」という文字列が出力されます。
例3. (0.1).toString()
それでは、今までの議論を踏まえて、いよいよ、(0.1).ToString()
を実行したときの挙動を見ていきます。
今回も、「処理手順1」~「処理手順4」までは、先ほどとまったく同じで条件を満たさないので、「処理手順5」の部分を見ていきます。
解釈1(誤った解釈?)
※たぶん、この項で書いている解釈は間違えています。が、正しい解釈がわからないので、とりあえず晒しておきます。正しい解釈をご存じの方がいらしたら、ぜひ教えてください。
「x」と「ℝ(x)」の値を計算する
まず、今回指定した「0.1」という数値は、JavaScriptの引数として渡されている数です。ですから、これは「普通の数値」ではなく、**「Number型の数値」**です。
そして、「Number型」では「0.1」という数を正確に表すことができず、「0.10000000000000000555111512312578270211815834045410156250」として扱われます。
ですから、今回のtoString
メソッドの引数「x」の真の値は「0.10000000000000000555111512312578270211815834045410156250」となります。
また、「s」「n」「k」は、「普通の数値」の世界で「s × 10n - k = ℝ(x)」という関係を満たす必要があります。
ここで、右辺の、「ℝ(x)」は、「x」を「普通の数値」に変換した「0.10000000000000000555111512312578270211815834045410156250」になります。
これから、この「ℝ(x)」に一致するような「s」「k」「n」を探していきます。
「s」と「k」を決める
「s」は整数値で、かつ、「ℝ(x)」が「s × 10n - k」の形に書けないといけません。
ですから、「s」は「0.10000000000000000555111512312578270211815834045410156250」に10を何回か掛けて整数値にしたものになります。
先ほどまでの議論を考えると、「s」は、そのような整数値のうちで最小のものになるはずです。
ですから、「s」は「1000000000000000055511151231257827021181583404541015625」になります。
その場合、「k」は、次のようになります。
s | sとkの関係 | k |
---|---|---|
100000000000000005551115123125782702118 1583404541015625 |
1055-1 ≦ 123456 < 1055 | 55 |
つまり、**「s」が「1000000000000000055511151231257827021181583404541015625」、「k」が「55」**になります。
「n」を決める
「s × 10n - k = ℝ(x)」に、「s」「k」「ℝ(x)」をあてはめると、次のようになります。
「1000000000000000055511151231257827021181583404541015625 × 10n - 55 = 0.1000000000000000055511151231257827021181583404541015625」
通常の数の世界での等式ですから、端数処理は一切行いません。
これを計算すると、「n」は「0」になります。
元の数 | n | k | s |
---|---|---|---|
0.100000000000000005551115123 1257827021181583404541015625 |
0 | 55 | 100000000000000005551115123 1257827021181583404541015625 |
「n」「k」「s」の値が決まりましたので、次の処理手順に移りましょう。
「n」「k」「s」の値に応じて、文字列変換を行う。
- If k ≤ n ≤ 21, return the string-concatenation of:
- the code units of the k digits of the decimal representation of s (in order, with no leading zeroes)
- n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
今回は、「k」が「55」、「n」が「0」なので、「処理手順6」の条件を満たしません。
次に進みます。
- If 0 < n ≤ 21, return the string-concatenation of:
- the code units of the most significant n digits of the decimal representation of s
- the code unit 0x002E (FULL STOP)
- the code units of the remaining k - n digits of the decimal representation of s
「n」が「0」なので、この「処理手順7」の条件を満たしません。
次に進みます。
- If -6 < n ≤ 0, return the string-concatenation of:
- the code unit 0x0030 (DIGIT ZERO)
- the code unit 0x002E (FULL STOP)
- -n occurrences of the code unit 0x0030 (DIGIT ZERO)
- the code units of the k digits of the decimal representation of s
「n」が「0」なので、この「処理手順8」の条件を満たします。
そこで、次のように文字列を出力します。
- 「0」を出力
- 「.」を出力
- 「0」を「-n」桁分出力(今回は「n」は「0」なので、何も出力しない)
- 「s」の数値を「k」桁分(=「1000000000000000055511151231257827021181583404541015625」)出力。
その結果、「0.1000000000000000055511151231257827021181583404541015625」という文字列が出力される、、、はずです。
ところが、実際の出力結果は「0.1」なので、これと一致しません。
仕様書の、どこを読み間違えているんでしょう?
解釈2(一部旧仕様書に沿って)考えてみる
「処理手順5」の古い仕様書を確かめる
古いJavaScriptの仕様書の、「処理手順5」に相当する記述を見てみると、微妙に違う表現がされているように見えます。
「Standard ECMA-262 5.1 Edition」9.8.1 ToString Applied to the Number Type
- Otherwise, let n, k, and s be integers such that k ≥ 1, 10k−1 ≤ s < 10k, the Number value for s × 10n−k is m, and k is as small as possible. Note that k is the number of digits in the decimal representation of s, that s is not divisible by 10, and that the least significant digit of s is not necessarily uniquely determined by these criteria.
そこで、旧仕様書に沿って、「処理手順5」の挙動を考えてみようと思います。
以下、「ECMAScript 2021 Language Specification」を「新仕様書」、「Standard ECMA-262 5.1 Edition」を「旧仕様書」と表記していきます。
旧仕様書と新仕様書では大きく違う点が2つあります。
1つ目は、変数名です。旧仕様書では「元の値」は「m」、新仕様書では「元の値」は「x」で表されています。
2つ目は、「s」「n」「k」の計算方法です。引用した文章中の、強調している部分に注目してほしいのですが、ここでは「Number value for s × 10n−k is m」という表現がされています。
「Number value for x」という表現は、**「普通の数値」の世界の数「x」に対応する「Number型の数」**を意味しています。
→ 「Standard ECMA-262 5.1 Edition」8.5 The Number Type
そして、先ほどの文章は、「Number value for s × 10n−k」と「m」(これは、両方ともNumber型の数)が等しいと言っているわけですから、「s」「n」「k」と、元の数「m」が「Number型の数として等しい」という意味に解釈できます。
Number型同士で数値を比較する場合には、少し数値がずれていても一致すると判定されることがあります。
たとえば「0.1000000000000000055511151231257827021181583404541015625」は、少なくとも、0.099999999999999999≦m≦0.100000000000000012を満たす「m」とは等しいと判定されます。
console.log(0.1000000000000000055511151231257827021181583404541015625 === 0.099999999999999999)
// true
console.log(0.1000000000000000055511151231257827021181583404541015625 === 0.100000000000000012)
// true
ですから、この範囲に収まるように「s」「n」「k」を選べば良い、ということになります。
(補足)「解釈1」と「解釈2」の違い
先ほどの「解釈1」(=新仕様書)では「s × 10n−k = ℝ(x)」は「普通の数値」の世界で考えていました。
一方で「解釈2」(=旧仕様書)では「s × 10n−k = x」は「Number型」の世界で考えています。
この点が両者で、大きく異なるように感じます。
が、実際には、新旧仕様書で、ここの計算ロジックが変わるわけがないと思うので、何か、私が読み間違いをしている気がします。
「s」と「k」を決める
「s」は整数値で、かつ、「m」が「s × 10n - k」の形に書けないといけません。
「k」は元の数「m」の桁数を表すのですから、「m」として、できるだけ桁数が小さくなるものを選ぶ必要があります。
先ほど見たとおり、「m」は、「0.099999999999999999 ≦ m ≦ 0.100000000000000012」 の範囲で自由に選んでいいわけです。そこで、この中から、桁数が一番短くなりそうな「0.1」を選びましょう。
その場合、**「s」は「1」(=「0.1」に10を1回掛けたもの)、「k」は「1」**になります。
s | sとkの関係 | k |
---|---|---|
1 | 100 ≦ 1 < 101 | 1 |
「n」を決める
「s × 10n - k = m」(※JavaScriptのNumber型としての等式)に、「s」「k」「m」をあてはめると、次のようになります。
「1 × 10n - 1 = 0.1000000000000000055511151231257827021181583404541015625」
「n」に「0」を入れると、左辺は「1 × 100 - 1」=「0.1」となり、「Number型の世界」では、左辺と右辺は一致します。
console.log(0.1 === 0.1000000000000000055511151231257827021181583404541015625)
// true
ということで、算出した「n」「k」「s」を表にまとめると、次のようになります。
元の数 | n | k | s |
---|---|---|---|
0.1000000000000000055511151231257827021181583404541015625 | 0 | 1 | 1 |
「n」「k」「s」の値に応じて、文字列変換を行う。
ここからは、新仕様書に戻って、「処理手順6」以降に進みましょう。
- If k ≤ n ≤ 21, return the string-concatenation of:
- the code units of the k digits of the decimal representation of s (in order, with no leading zeroes)
- n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
今回は、「k」が「1」、「n」が「0」なので、「処理手順6」の条件を満たしません。
次に進みます。
- If 0 < n ≤ 21, return the string-concatenation of:
- the code units of the most significant n digits of the decimal representation of s
- the code unit 0x002E (FULL STOP)
- the code units of the remaining k - n digits of the decimal representation of s
「n」が「0」なので、この「処理手順7」の条件を満たしません。
次に進みます。
- If -6 < n ≤ 0, return the string-concatenation of:
- the code unit 0x0030 (DIGIT ZERO)
- the code unit 0x002E (FULL STOP)
- -n occurrences of the code unit 0x0030 (DIGIT ZERO)
- the code units of the k digits of the decimal representation of s
「n」が「0」なので、この「処理手順8」の条件を満たします。
そこで、次のように文字列を出力します。
- 「0」を出力
- 「.」を出力
- 「0」を「-n」桁分出力(今回は「n」は「0」なので、何も出力しない)
- 「s」の数値を「k」桁分(=「1」)出力。
その結果、「0.1」という文字列が出力されることになります。
これで、実際の挙動と同じ動きになりました。
例4. (0.1+0.2).toString()
最後に、(0.1+0.2).ToString()
を実行したときの挙動を見ていきます。
今回も、「処理手順1」~「処理手順4」までは、先ほどとまったく同じで条件を満たさないので無視できます。
そこで、「処理手順5」の部分を見ていきます。
条件を満たす「n」「k」「s」の値を(一部旧仕様書に沿って)計算する
「s」と「k」を決める
先ほどの「例3」と同じように、旧仕様書に沿って、「s」「k」の値を算定してみます。
まず、元の値「m」は、「0.3000000000000000444089209850062616169452667236328125000」です。
console.log((0.1+0.2).toFixed(55));
// 0.3000000000000000444089209850062616169452667236328125000
また、少なくとも「0.3000000000000000167 ≦ m ≦ 0.3000000000000000721」の範囲の数は、Number型の数値として一致します。
console.log(0.30000000000000004440892098500626161694526672363281250 === 0.3000000000000000167);
// true
console.log(0.30000000000000004440892098500626161694526672363281250 === 0.3000000000000000721);
// true
「k」は元の数「m」の桁数を表すのですから、「m」として、できるだけ桁数が小さくなるものを選ぶ必要があります。
この範囲内で、桁数が一番小さくなる数は「0.30000000000000002」「0.30000000000000003」・・・「0.30000000000000007」の6通りあります。
(新仕様書、旧仕様書のどちらでも)仕様書上は、この中から、任意の数値を選べるようです。そこで、今回は「0.30000000000000004」を、元の数「m」として先に進みましょう。
さて、「s」は整数値で、かつ、「m」が「s × 10n - k」の形に書けないといけないのですから、「s」は「30000000000000004」になります。
そして、「k」は「17」になります。
s | sとkの関係 | k |
---|---|---|
30000000000000004 | 1017-1 ≦ 123456 < 1017 | 17 |
「n」を決める
まず、「s × 10n - k = m」の左辺を計算します。
「s × 10n - k」に「s」「k」をあてはめましょう。
「30000000000000004 × 10n - 1 = 0.30000000000000004440892098500626161694526672363281250」
なお、等号は、「Number型」の世界での等号であることに注意してください。
そして、この数式を成り立たせようとすると、「n」は「0」になることがわかります。
実際、「n」に「0」を入れて、左辺を計算すると、「30000000000000004 × 100 - 17」=「0.30000000000000004」となります。
なお、この左辺の計算は、「普通の数値」の世界で行うことになります。
旧仕様書では、左辺は「the Number value for s × 10n−k」という表現をされているので、「s × 10n−k」の計算は「普通の数値」の世界で行い、その後、それに対応する「the Number value」(=Number型の値)に変換をすることになります。
そして、JavaScriptのNumber型として、左辺と右辺を比較すると、両者は一致していることがわかります。
console.log(0.30000000000000004 === 0.30000000000000004440892098500626161694526672363281250)
// true
ということで、算出した「n」「k」「s」を表にまとめると、次のようになります。
元の数 | n | k | s |
---|---|---|---|
0.30000000000000004440892098500626161694526672363281250 | 0 | 17 | 30000000000000004 |
「n」「k」「s」の値に応じて、文字列変換を行う。
ここからは、新仕様書に戻って、「処理手順6」以降に進みましょう。
- If k ≤ n ≤ 21, return the string-concatenation of:
- the code units of the k digits of the decimal representation of s (in order, with no leading zeroes)
- n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
今回は、「k」が「17」、「n」が「0」なので、「処理手順6」の条件を満たしません。
次に進みます。
- If 0 < n ≤ 21, return the string-concatenation of:
- the code units of the most significant n digits of the decimal representation of s
- the code unit 0x002E (FULL STOP)
- the code units of the remaining k - n digits of the decimal representation of s
「n」が「0」なので、この「処理手順7」の条件を満たしません。
次に進みます。
- If -6 < n ≤ 0, return the string-concatenation of:
- the code unit 0x0030 (DIGIT ZERO)
- the code unit 0x002E (FULL STOP)
- -n occurrences of the code unit 0x0030 (DIGIT ZERO)
- the code units of the k digits of the decimal representation of s
「n」が「0」なので、この「処理手順8」の条件を満たします。
そこで、次のように文字列を出力します。
- 「0」を出力
- 「.」を出力
- 「0」を「-n」桁分出力(今回は「n」は「0」なので、何も出力しない)
- 「s」の数値を「k」桁分(=「30000000000000004」)出力。
その結果、「0.30000000000000004」という文字列が出力されることになります。
これで、実際の挙動と同じ動きになりました。
まとめ
仕様書の読み方が、どこか間違えているのだろうと思います。
どう読むのが正しいのか教えてください。