[EffectiveJavaScript輪読会]JavaScriptの浮動小数点を理解しよう

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第3回目です。

前回のおさらい

前回は、「どのJavaScriptをつかっているのかを意識しよう」をお届けしました。

[EffectiveJavaScript輪読会]どのJavaScriptをつかっているのかを意識しよう
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第2回目です。前回のおさらい前回は、「ノンプロ研Ef...

今回は、「JavaScriptの浮動小数点を理解しよう」 をお届けします。

テキスト第1章「JavaScriptに慣れ親もう」の項目2に対応しています。※担当回です。

今日のアジェンダ

  • 10進数と2進数
  • 2進数と集積回路
  • ビット演算
  • 浮動小数点の限界と循環小数
  • 実務を意識するなら1『数値リテラルと近似値』
  • 実務を意識するなら2『2つのオブジェクトとメソッド』

10進数と2進数

整数の2503(にせんごひゃくさん)を、V8から実装された、べき乗演算子で表現してみましょう。

function myFunction1_2_01() {

const THOUSANDS = 10 ** 3;
const HUNDREDS = 10 ** 2;
const TENTH = 10 ** 1;
const FIRST = 10 ** 0;

const t = 2 * THOUSANDS;
const h = 5 * HUNDREDS;
const te = 0 * TENTH;
const f = 3 * FIRST;

console.log(t + h + te + f); //2503

}

お気付きの方もいるかもしれませんが、10進数 はそれぞれの桁に10のべき乗をすることで表現できます。

同様に、2進数 も表現してみましょう。

2進数の1100(いちいちぜろぜろ)は、それぞれの桁に2のべき乗をすることで表現できます。

function myFunction1_2_02() {

const FORTH = 2 ** 3;
const THIRD = 2 ** 2;
const SECOND = 2 ** 1;
const FIRST = 2 ** 0;

const a = 1 * FORTH;
const b = 1 * THIRD;
const c = 0 * SECOND;
const d = 0 * FIRST;

console.log(a + b + c + d); //12

}

べき乗とは、同じ数字を複数回にわたって掛ける ことですが、掛けられる数字を「基数」、掛ける回数を「指数」と呼びます。

10進数と2進数の変換

さて、整数を10進法で2503(にせんごひゃくさん)と表したり、整数の12(じゅうに)を2進数で1100(いちいちぜろぜろ)と表記してきました。

この、2進数と10進数の表記変換は、JavaScriptのメソッドで行えます。

10進数→2進数

10進数から2進数への変換は、Number.toString()メソッド です。

toString()メソッドの引数に基数を渡すと、変換後の文字列を返します。

function myFunction1_2_03() {

console.log((12).toString(2)); //'1100'

}

2進数→10進数

2進数から10進数への変換は、parseInt()メソッド です。

parseInt()メソッドは、第1引数に数値、第2引数に基数を渡すと、第1引数を第2引数で解釈した数値 を返します。

これは、Number.parseInt()メソッドでも、トップレベル関数のparseInt()メソッドでも、同様の結果を返します。

function myFunction1_2_03() {

console.log(Number.parseInt(1100,2)); //12
console.log(parseInt(1100,2)); //12

}

2進数と集積回路

2進数で使う数字は、0と1の2種類です。そして、2進数の1桁を ビット と呼びます。

2進数の1桁は0か1なので、1ビットなら2通り(0か1か)表現できます。
– 2ビット(2進数で2桁)なら4通り(00か01か10か11か)
– 3ビット(2進数で3桁)なら8通り(000か001か010か・・・省略)
– 4ビット(2進数で4桁)なら16通り(0000か・・・省略)
– 8ビット(2進数で8桁)なら256通り(00000000か・・・省略)

このように、ビット数が大きいほど、よりたくさんの情報を処理できます。

「32ビット」と言われたら、どれくらいの大きさか想像がつきませんが、「2の32乗」 となると理解できます。

書籍で登場する「53ビット」は、9007兆1992億5474万992です。

function myFunction1_2_04() {

console.log(2 ** 32); //4294967296(42億9496万7296)
console.log(2 ** 64); //18,446744073709552000(1844京6744兆737億955万2000)

let int = -2;
console.log(int ** 53); //-9007199254740992(‐9007兆1992億5474万992)

int = 2;
console.log(int ** 53); //9007199254740992(9007兆1992億5474万992)

}

ちなみに、コンピューターでビットが使われる理由は、電気信号のON(電気が流れている)とOFF(流れていない)が2進数と相性が良かったからです。

集積回路のピン1本で、ビットを表現できるため、ビットを情報処理の最小単位としています。

ビットとデータ型

「4ビット」と言われたら、2の4乗なので、2 x 2 x 2 x 2 = 16です。

数値でいうと、負の方向に16通り(―16, -15, -14…-1)、正の方向に16通り(1, 2, 3…16)が表現できます。(厳密には違いますが、ご了承ください)

この、数値の範囲がどれくらい必要なのか、ということを定義したものが「型」です。

型は宣言されたさい、メモリを確保します。確保するメモリが少なれければ少ないほど処理が早い ことは、みなさんも想像がつくでしょう。

VBAは歴史あるプログラミング言語ですが、以下のような数値型を実装しています。

Byte型は8ビットであり、256通りの文字を表現できることは、2進数で8桁なら256通りを表現できることと、話がつながりますね。

そして、本題であるJavaScriptは、Numberという唯一の数値型がありますが、これは、国際規格の「Double」を採用しています。

JavaScriptの数値型は、倍精度浮動小数点(使用メモリは64ビット) です。

ビット演算

JavaScriptの数値型はDoubleであり、整数や小数の演算を行うときは、倍精度浮動小数点(整数も含まれる)として演算を行います。

0.1 * 1.9 は 0.19ですし、-99 + 100 は 1です。

しかし、V8から実装された「ビット演算」を行うさいは、例外的に32ビットで演算 されます。

この、32ビットかどうかという確認は手間が掛かりますが、とりあえず「本当にビット列に変換しているのか」の裏を取ってみましょう。

整数「8」を2進数で表すと、「1000(いちぜろぜろぜろ)」です。同様に、整数「1」は「1(いち)」です。

この2進数を32ビットで表現する(1や0を32個並べたもの)ために、0で埋めるpadStart()メソッドを使います。

function myFunction1_2_05() {

console.log((8).toString(2).padStart(32, 0)); //00000000000000000000000000001000
console.log((1).toString(2).padStart(32, 0)); //00000000000000000000000000000001

}

ビット演算の論理演算

ビット演算とは、2つのビットで表現された値を、積(AND)・和(OR)・否定(NOT)、などの論理的な方法で演算することです。

積(AND)

対応するビットの両方が1である位置のビットで、1を返します。

和(OR)

対応するビットのどちらか片方が1である位置のビットで、1を返します。

その他にもビット演算はありますが、割愛します。

JavaScriptのビット演算

JavaScriptでは「&」がビット論理積で、「|」がビット論理和です。

ある2つの数値に、ビット演算子をぶつけると、以下のような手順で演算を行います。
1. 両方のオペランド(右辺と左辺)を32ビットの数値エンコードに変換する
2. 演算子をもとに、論理積や論理和を行う
3. 結果を10進数に変換してNumberを返す

コードで確認しましょう。

function myFunction1_2_06() {

//倍精度小数点(整数を含む)による演算
console.log(0.1 * 1.9); //0.19
console.log(-99 + 100); //1

//ビット演算&は論理積
const bitwiseAnd = 8 & 1;
console.log(typeof bitwiseAnd); //number

console.log(bitwiseAnd); //2進数0000が論理積の解
console.log(parseInt(0000, 2)); //0000とは0である
console.log(8 & 1); //0

//ビット演算|は論理和
const bitwiseOr = 8 | 1;
console.log(typeof bitwiseOr); //number

console.log(bitwiseOr); //2進数1001が論理積の解
console.log(parseInt(1001, 2)); //1001とは9である
console.log(8 | 1); //9

}

浮動小数点の限界と循環小数

2進数の小数点

2進数の1011.0011(いちぜろいちいち.ぜろぜろいちいち)を、10進数の11.1875(じゅういってんいちはちななご)に、整数と少数に分けて コードで変換してみましょう。

function myFunction1_2_07() {

//1011(いちぜろいちいち)
const FORTH = 2 ** 3;
const THIRD = 2 ** 2;
const SECOND = 2 ** 1;
const FIRST = 2 ** 0;

const a = 1 * FORTH;
const b = 0 * THIRD;
const c = 1 * SECOND;
const d = 1 * FIRST;

const int = a + b + c + d; //11(じゅういち)

//.0011(.ぜろぜろいちいち)
const U_FIRST = 2 ** -1;
const U_SECOND = 2 ** -2;
const U_THIRD = 2 ** -3;
const U_FORTH = 2 ** -4;

const e = 0 * U_FIRST;
const f = 0 * U_SECOND;
const g = 1 * U_THIRD;
const h = 1 * U_FORTH;

const float = e + f + g + h; //.1875(てんいちはちななご)

console.log(int + float); //11.1875(じゅういってんいちはちななご)

console.log((11.1875).toString(2)); //1011.0011(いちぜろいちいち.ぜろぜろいちいち)

}

2進数で小数点を表現するばあいは、それぞれの桁に2のマイナスべき乗をすることで表現できます。

マイナスべき乗とは、同じ数字を複数回にわたって割る ということです。

2進数の小数点以下を、1桁目から順にみていくと、小数点以下1桁目は0.5、2桁目は0.25、という順になっています。

function myFunction1_2_08() {

const U_FIRST = 2 ** -1;
const U_SECOND = 2 ** -2;
const U_THIRD = 2 ** -3;
const U_FORTH = 2 ** -4;

console.log(U_FIRST); //0.5
console.log(U_SECOND); //0.25
console.log(U_THIRD); //0.125
console.log(U_FORTH); //0.0625

}

つまり、2進数の小数点以下を4桁で表現しようとしたばあい 、0.5、0.25、0.125、0.0625の 4つの小数点の組み合わせ(足し算)でしか表現できない ということです。

循環小数

では、10進数の0.1や、0.3を表現するためには、2進数の小数点以下を何桁用意すればいいのでしょうか。

答えは、何百桁あっても表現できません

これは、10割る3の答えを10進数で表せないことと同じで、「循環小数」と呼ばれます。

かんたんな小数点の演算をJavaScriptが間違う理由は、このためです。

function myFunction1_2_09() {

//10進数の0.625と0.125の間の0.1を2進数に変換すると、小数点以下の桁数が何百あっても足りない。
console.log((0.1).toString(2)); //0.0001100110011001100110011001100110011001100110011001101(丸め)

//循環小数
console.log(10 / 3); //3.3333333333333335

//有名な、JavaScriptが計算を間違う例
console.log(0.1 + 0.2); //0.30000000000000004
console.log(0.1 + 0.2 === 0.3); //false

//後述するtoFixed()メソッドによる裏取り
const x = Number((0.1).toFixed(20)); //0.10000000000000000555
const y = Number((0.2).toFixed(20)); //0.20000000000000001110
const sum = x + y; //0.30000000000000004
console.log(0.1 + 0.2 === sum); //true

}

浮動小数点で得られる結果は、「ある位置で結果を丸めた近似値」 なのです。

実務を意識するなら1『数値リテラルと近似値』

かなり、長くなってしまいました。最後のセクションでは、実務で使えるトピックをお届けします。

数値リテラル

JavaScriptの数値型は、倍精度浮動小数点を採用しています。

JavaScriptの数値リテラル、特に小数点のリテラルでは、整数のゼロと、小数点以下のゼロを省略できます。

また、組み込みNumberメソッドにぶつける数値リテラルは3種類です。

V8から導入された「バイナリリテラル」もご紹介します。

数値の先頭に「0b(ぜろびー)」をつけることで、以降の数値を2進数に評価します。

bはバイナリという意味です。意味をなさないゼロは省略されます。

function myFunction1_2_10() {

//ゼロの省略
console.log(.123);
console.log(123.);

//組み込みNumberメソッドにぶつける数値リテラル
console.log(123.0.toFixed()); //123
console.log(123..toFixed()); //123
console.log((123).toFixed()); //123

console.log(10.0.toString(2)); //1010
console.log(10..toString(2)); //1010
console.log((10).toString(2)); //1010

//バイナリリテラル
console.log(0b00000000000000000000000000001001); //9
console.log(0b1001); //9

}

ES2020では、桁区切りにアンスコが導入されました。いつかGASもES2020が準拠されたら使える表記法ですね。

間違いを無視する

コンピュータの小数点の計算間違いを回避する方法のひとつは、「間違いを無視する」ことです。

0.1ミリの精度を求められる状況で、0.000000001ミリの誤差を気にする必要はありません。

逆にいうと、数学では ある程度の誤差を認めて、近似値を取ることで本来の課題を解決する ことがよくあります。

円周率を3.14とする例がわかりやすいと思います。近年、注目を集めている データサイエンス にも通じる話です。

実務を意識するなら2『2つのオブジェクトとメソッド』

Mathオブジェクトのメソッド

組み込みのMathオブジェクトでは、「小数点以下をどう処理して、整数を返すか」という視点で、メソッドが提供されています。

主に「四捨五入」「切り捨て」「切り上げ」が身近な存在でしょう。

function myFunction1_2_11() {
//小数点以下を○○して整数を返す

//四捨五入する
console.log(Math.round(12.4)); // 12
console.log(Math.round(12.5)); // 13

//切り捨てる
console.log(Math.floor(12.4)); // 12
console.log(Math.floor(12.555)); // 12

//切り上げる
console.log(Math.ceil(12.4)); // 13
console.log(Math.ceil(12.53)); // 13

}

これらのメソッドは、桁数の指定ができませんが、一度、小数点の位置を調整して、演算して戻す という手法が常套句です。

function myFunction1_2_12() {

//小数点第一位を基準とする(四捨五入・切り捨て・切り上げ)
console.log(Math.round(123.456 * 10) / 10); //123.5
console.log(Math.floor(123.456 * 10) / 10); //123.4
console.log(Math.ceil(123.456 * 10) / 10); //123.5

//十の位を基準とする(四捨五入・切り捨て・切り上げ)
console.log(Math.round(125.456 / 10) * 10); //130
console.log(Math.floor(123.456 / 10) * 10); //120
console.log(Math.ceil(125.456 / 10) * 10); //130

}

しかしながら(今日の記事を読み進めてきた方には伝わると思いますが)、Mathオブジェクトのメソッドの戻り値はJavaScriptの数値、つまり浮動小数点なので正確な数値ではなく、あくまで近似値 を返します。

function myFunction1_2_13() {

const x = Math.floor(0.10123 * 10) / 10; //0.1
const y = Math.floor(0.20123 * 10) / 10; //0.2

console.log(x + y === 0.3); //false

}

Numberオブジェクトのメソッド

この問題を解決するために、JavaScriptでは Number.toFixed()メソッド を提供しています。

toFixed()メソッドは、まず、数値を倍精度小数点で評価し、引数に指定された桁数で、小数点以下を表示します。

小数点以下の桁数を表示するために、対象となる桁(引数の次の桁)を 四捨五入して丸め ます。

戻り値は文字列型 です。

function myFunction1_2_14() {

//5.015を倍精度小数点で16桁確認する
console.log((5.015).toFixed(16)); //5.0149999999999997

//小数点以下2桁で表示する(3桁目を四捨五入で丸める)
console.log((5.015).toFixed(2)); //5.01

//5.025を倍精度小数点で17桁確認する
console.log((5.025).toFixed(17)); //5.02500000000000036

//小数点以下2桁で表示する(3桁目を四捨五入で丸める)
console.log((5.025).toFixed(2)); //5.03

//型の確認
console.log(typeof (0.1).toFixed(1)); //string
}

実務では、このような関数が想定されるでしょう。

function myFunction1_2_15() {

/**
* 小数点以下3桁を四捨五入で丸め、2桁の文字列で返す関数
*/
const financial = x => {
return Number.parseFloat(x).toFixed(2);
};

console.log(financial(123)); //'123.00'
console.log(financial(123.456)); //'123.46'
console.log(financial('123456.02円')); //'123456.02'

}

まとめ

以上で、「JavaScriptの浮動小数点を理解しよう」 をお届けしました。

ここから読み始めたかたへ、お伝えしたいことは3つです。

  1. 浮動小数点で得られる結果は、「ある位置で結果を丸めた近似値」 です。
  2. コンピューターが小数点の計算を間違う回避策のひとつは 「間違いを無視する」 です。
  3. 小数点の演算には、小数点を整数に置き換える Mathオブジェクト系のメソッド を使うか、浮動小数点を文字列で返す Number.toFixed()メソッド を使う。

でした。

輪読会でメンバーから 「VBAのCurrency型は、常に1万倍して戻している」 というお話を聞きました。

「GASでも1万倍するクラスを用意すれば良さそう」 というアイディアも出ました。なるほどですね。

次回は、「暗黙の型変換に注意しよう」 をお届けします。

参考資料

JavaScriptの数値型完全理解
ECMAScriptの浮動小数点数の丸め仕様がスゴい
Number.prototype.toFixed() MDN
プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識 矢沢久雄 日経ソフトウェア
『参照渡しと値渡し』-記憶力はそれを鍛えなければ衰える- GAS中級2期卒業LT200821 つじけ

このシリーズの目次

[EffectiveJavaScript輪読会]ノンプロ研EffectiveJavaScript輪読会とは
[EffectiveJavaScript輪読会]どのJavaScriptをつかっているのかを意識しよう
[EffectiveJavaScript輪読会]JavaScriptの浮動小数点を理解しよう
[EffectiveJavaScript輪読会]暗黙の型変換に注意しよう
[EffectiveJavaScript輪読会]オブジェクトラッパーよりもプリミティブが好ましい
[EffectiveJavaScript輪読会]型が異なるときに==を使わない
[EffectiveJavaScript輪読会]セミコロン挿入の限度を学ぼう
[EffectiveJavaScript輪読会]文字列は16ビットの符号単位を並べたシーケンスとして考えよう

タイトルとURLをコピーしました