こんな記事を見かけた
先ほどからイマイチな実装として紹介してきた「0.5足して小数点以下を切り捨てる」が内部実装だと書いてありますので、上の挙動は仕様だと言えそうです。この記述だけでエッジケースの分かるJavaプログラマがどれほどいるかは疑問ですが、文書化されていること自体は素晴らしいと僕は当時から絶賛していました。
ところで、Java 7以降のマニュアルからはこの内部実装に関する記述が消えているようです。もちろん内部的には0.5を足す実装のまま変わっていません。意地悪な見方をすると、Java 6まで仕様だったのがJava 7からバグになったと言えるかもしれません。
これを読んでJavaのMath.round()はバグっている、という人が居るかもしれないが、実際のところそうではない。仕様にはこの関数が四捨五入するとはどこにも書かれておらず、引数で指定したdouble値に一番近いlong値を返すと書かれているだけである。
なので、たとえ10進表現で0.5になっていても2進として0に近いほうであればMath.roundで0が返ってくることは仕様通りであり、バグということにはならない。
Returns the closest long to the argument, with ties rounding up.
http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29
しかし前述の記事の中にあった2つのエッジケースはそうではない。2進表現で考えたとしても引数のdouble値に一番近い整数を返していないからである。
四捨五入のエッジケース1:0.49999999999999994
0.5より小さい倍精度浮動小数点数の中で最大の数が0.49999999999999994です
:
四捨五入のエッジケース2:9007199254740991.0
9007199254740991.0を四捨五入するとなぜか繰り上がって9007199254740992.0になってしまう実装があります。
:
Javaの四捨五入にはバグなのか仕様なのか微妙なエッジケースが今でも存在する
さて、上記2つのエッジケースについてですが、Javaのround関数は2つとも間違いっぽい方に丸めます。
おかしいと思ってこれを調べたところ、Javaでは過去に修正済の問題のようだった。
間違いっぽい方にまるめていたのはJava6の頃で、Java7の途中で修正されたのである。そのバグの修正と合わせて「Math.roundが(long)Math.floor(a + 0.5d)と等価である」という記載も削除されていることが下記のページに記されている。
http://bugs.java.com/view_bug.do?bug_id=6430675
ところで、Java 7以降のマニュアルからはこの内部実装に関する記述が消えているようです。もちろん内部的には0.5を足す実装のまま変わっていません。
どのJavaを見てこのようなことを書かれたのかは少し気になる。
- OracleのJDKは上で書いたように修正済みである
- OpenJDKは修正前の仕様のように見える(http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/java/lang/Math.java#Math.round%28double%29 / ”the result is equal to the value of the expression: (long)Math.floor(a + 0.5d)”の記載がまだ残っている)
- Android6.0で使われているluniも修正前の仕様である ( https://android.googlesource.com/platform/libcore.git/+/android-6.0.1_r46/luni/src/main/java/java/lang/Math.java / 同様に"The result is equivalent to (long) Math.floor(d+0.5)."と記載されている)
つまりJavaは実装によって結果は違うし、10進世界に住む我々には直感的じゃない結果が出ているかもしれないけど、それぞれのドキュメントと実際の動きが違うことはなく一貫しているのように思える。Rubyのドキュメントがどうなっているか調べてないけど「四捨五入」とか書いちゃってたら10進で処理しないと正しい結果を得られないのではないかと思う。