Edited at

Javascriptで数値の整数化にビット演算は使わないほうがいい

More than 1 year has passed since last update.


整数化

JavaScriptにおいて、数値を整数化したい、というとき、その手法はいくつかあって、その時々において、使い分けたりするのだが、その中でもよく使われるのが「 ビット演算を用いた方法 」である

JavaScriptで小数を整数にするマニアックな方法とその実行速度 | q-Az

12345.6789 >> 0

// -> 12345

ビットシフトは1桁右に送ると値が1/2になるのだが、0桁送った場合、値が変わらない。しかし、その演算特性からか、小数点以下の値を切り落としてくれるのだ。

これは記述量の面でも、速度の面でもメリットがあり、頻繁に使われるTIPSであるのだが、今回、これが原因で開発したプログラムにバグが生じたので、その話をまとめる。


結論

もう最初に結論を書いてしまうが、"2147483648"という値をビット演算で整数化すると、値が反転してしまう。

2147483647 >> 0

// -> 2147483647
2147483648 >> 0
// -> -2147483648

また、その2倍の"4294967296"という値をビット演算で整数化すると、0になる。

4294967295>>0

// -> -1
4294967296>>0
// -> 0
4294967297>>0
// -> 1

値が充分に小さいのであれば問題ないのだが、この値に近いもしくは、この値を超える数値を扱うなら、ビット演算による整数化はやるべきではない。


原因

このような問題が起きる原因は、Number型が保持できる値と、ビット演算が取り扱える値の差にある。大きな数値を普通に変数に代入できたと安心していると、痛い目を見ることになるのだ。

Number型の値は"1.79E+308"まで保持できるのだが、ビット演算は(おそらく32ビットブラウザにおいて)、32ビット内(-2,147,483,648 〜 2,147,483,647)でしか計算ができないため、それ以上の値(ビット)を切り落としてしまうようだ。

Number.MAX_VALUE - JavaScript | MDN

(Number.MAX_VALUE).toString(2)

// -> "1111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

(Number.MAX_VALUE).toString(2).length
// -> 1024

ビット演算において32ビット目は符号として使われているようなので、値として使えるのは31ビットが最大になる。つまり32ビット目に1がくる値をビット演算すると、値がマイナスになってしまうわけだ。

2の補数 - Wikipedia

(2147483647).toString(2)

// -> "01111111111111111111111111111111"
2147483647>>0
// -> 2147483647

(2147483648).toString(2)
// -> "10000000000000000000000000000000"
2147483648>>0
// -> -2147483648

33ビット目以上の桁に値が入っても、それは切り落とされる。

(5000000000).toString(2)

// -> "100101010000001011111001000000000"
(5000000000>>0).toString(2)
// -> "101010000001011111001000000000"

なるほど、そもそもビットシフトによって小数点以下の値が切り落とされる理由もここにあるようだ。小数の表現が32ビット外の桁を用いて行われているため、この手法により存在しないものとして扱われるのだろう。


回避策

ビット演算は文字数も少なく、使い勝手が良いのだが、無駄なバグに悩まされるくらいなら素直にMath.floorparseIntを使ったほうがいいだろう。速度的な面を考えると、Math.floorの方がよさそうだ。そもそも「プログラミング」というジャンルにおいて「結果が同じだから」という理由で、その挙動が曖昧なTIPSに走るのではなく、ちゃんとそれ用に用意されたメソッドがあるならそれを使うべきなのかもしれない。

2147483648.123>>0

// -> -2147483648
Math.floor(2147483648.123)
// -> 2147483648
parseInt(2147483648.123)
// -> 2147483648