31
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptAdvent Calendar 2015

Day 12

すべてがfになる〜整数型のない世界

Last updated at Posted at 2015-12-12

2022年時点での追記
ES2020でJavaScriptにbigintという、整数専用の型が加わっています。
ただし、既存コードに影響を与えないような後付拡張として実装されたので、bigintbigintどうしでしか演算不可、bigintnumber(従来の数値型)の変換はBigInt()Number()の変換関数を利用しなければならない、Mathにある関数の引数や返り値も従来どおりnumberオンリー、というように、「できるだけbigintnumberが混ざらないような実装」となっています。

「整数型がない」という事情は変化しましたが、演算におけるnumberの性質はbigint登場後も全く変化していませんので、記事はそのまま残しておきます。

多くの言語では、数値を入れる型として、整数と浮動小数点数など、いくつかの型が存在します。しかし、JavaScriptはそうではありません。

5 / 2 = ?

C言語を使っていると時々引っかかる話ですが、「整数型同士の演算は整数型になる」ということになっています。そのため、5 / 2を計算すると、切り捨てられて2になります。この挙動は、RubyやJavaでも共通します。そういうわけで、リテラルを書く時点で5.0 / 2のようにしておくなどの対策法が存在します。

一方で、PHPでは、整数同士の除算でも浮動小数点数を返しうるということになっています。5 / 2は2.5です。

JavaScriptでも、5 / 2は2.5になるのですが、これはそもそも意味が違います。JavaScriptの世界には整数型がない1ので、浮動小数点数の5を、同じく浮動小数点数の2で割ったという意味になって、答えは2.5です。

ビット演算は…?

もちろん普通の演算ならば、整数でも浮動小数点数でも計算はできます。ただ、JavaScriptの演算子の中には、ビット演算やシフトなど、明らかに整数であることを前提としたものもいくつか存在しています。いったい、どうなっているのでしょうか。

実は、これらの演算子は、概念的には

  1. オペランドを整数に変換する
  2. 整数同士でビット演算やシフトを行う
  3. 得られた値を浮動小数点数として書き戻す

という流れになっています2。JavaScriptの数値型は、IEEE 754の64ビット浮動小数点数なので、精度は53ビットあります。ビット演算やシフト演算は32ビット整数として行いますので、値に誤差が出る心配なく格納できます。

あとで触れますが、この「ビット演算は整数として行う」ことを利用して、x | 0x >>> 0で整数化してしまう、という手法があります。

整数化する方法

ということで、JavaScriptで整数を作りたい場合、意図的に変換をくわえて整数にする必要があります。なお、整数でない数の最大値は $2^{52}-0.5 = 4503599627370495.5$ で、これより大きな数はすべて整数となります。

まずは、ある意味「正攻法」にあたる、Mathのメソッド群を見てみます。

  • Math.ceil(x)x以上で、最も小さな整数(すでに整数ならもちろんそのまま)。いわゆる「+∞への丸め」。
  • Math.floor(x)x以下で、最も大きな整数(すでに整数ならもちろんそのまま)。いわゆる「-∞への丸め」。
  • Math.round(x)xに最も近い整数(整数以下がちょうど0.5のときは切り上げ)。いわゆる「四捨五入」。

そして、先ほど書いたように、ビット単位の演算やシフト演算を行う場合、32ビットまで丸められますが、ルールが2つあります。

ビット反転(~)、ビット演算子(&|^)、符号付きシフト(<<>>)の場合は、値は符号付き32ビットの値に丸められます。ルールは、以下のようになっています(規格書通りではなく、分かりやすさを優先した表現としています)。

  1. ±∞やNaNのような特殊な値だった場合には+0として終了。
  2. 絶対値が小さくなるように整数へ丸める
  3. 値が-231〜+231-1の範囲内に収まらない場合は、232の倍数だけ足し引きして、この範囲内に収める

ということで、a|0のようにして整数にすると、「0への丸めになる」ことと、「32ビット整数からはみ出した値の場合、わかりづらい挙動になる」ことに注意する必要があります。

なお、符号なしシフトの>>>については、符号なし32ビットへの丸めとなります。処理内容としては、上の符号付き32ビットの丸めと同じで、最後に「負の数なら232を足して正にする」というような感じです。

まとめ

複雑なので、表にまとめてみました。

x Math.ceil(x) Math.floor(x) Math.round(x) ~~x3 x >>> 0
-2147483649.5 -2147483649 -2147483650 -2147483649 2147483647 2147483647
-2147483648.5 -2147483648 -2147483649 -2147483648 -2147483648 2147483648
-1.5 -1 -2 -1 -1 4294967295
-0.3 -0 -1 -0 0 0
0.3 1 0 0 0 0
0.5 1 0 1 0 0
1.5 2 1 2 1 1
2147483647.5 2147483648 2147483647 2147483648 2147483647 2147483647
2147483648.5 2147483649 2147483648 2147483649 -2147483648 2147483648

ご覧のように、時たま-0が出現したり、正負逆転したりと、かなり複雑です。

  1. TypedArrayという、バッファーを型付きでアクセスできるような仕組みはありますが、スカラーとしての整数型はありません。

  2. もちろん、実装によっては、内部的にそのまま整数で保管するなどの最適化を行なっていることはありえますが、それはプログラマー側からは見えません。

  3. 表組みのMarkdownの都合上、x|0と書けないのでこれにしてあります。x|0でもx ^ 0でもx << 0でも本質的に同じです。

31
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?