どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。今日は第3回目です。
前回のおさらい
前回は、「どのJavaScriptをつかっているのかを意識しよう」をお届けしました。
今回は、「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つです。
- 浮動小数点で得られる結果は、「ある位置で結果を丸めた近似値」 です。
- コンピューターが小数点の計算を間違う回避策のひとつは 「間違いを無視する」 です。
- 小数点の演算には、小数点を整数に置き換える 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ビットの符号単位を並べたシーケンスとして考えよう