Javaで「小数第N位以下を四捨五入したい」「ある桁はゼロ埋めしたいけど、あとは桁があるときだけ表示したい」などなど、数値を文字列で表現したい場合には、 java.text.DecimalFormat
を使うのが便利ですね。
今回はこのDecimalFormatで四捨五入が失敗するバグを見つけたので紹介します。
環境
$ java -version
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)
- OS: OS X Yosemite 10.10.1
- JDK: Oracle JDK 1.8 u25
現象
import java.text.DecimalFormat;
import java.math.RoundingMode;
class Java8DecimalFormat {
public static void main(String[] args) {
final DecimalFormat df = new DecimalFormat("0.##");
df.setRoundingMode(RoundingMode.HALF_UP);
String hoge = df.format(10.145);
System.out.println(hoge); // 10.14
String fuga = df.format(13.555);
System.out.println(fuga); // 13.55
}
}
四捨五入を期待して RoundingMode.HALF_UP
を指定しているにもかかわらず、挙動としては切り捨てになってしまっています。
実際バグらしい
[#JDK-8062013] DecimalFormat / rounding / HALF_UP - Java Bug System
OpenJDKのバグトラッカーに同様の報告が上がっていました。ver1.8.0u20にも同様のバグがあったので、それなりに前に混入したバグなのかもしれませんね。
テストコードの処理系をJDK1.7からJDK1.8に切り替えた途端にいくつかのテストが通らなくなり、原因を色々探した結果がこれでした。さて、どうしたものか。。。
追記: 1/15 12:40
問題切り分けのために、他のパターンでも実験してみました。
Java7環境
String hoge = df.format(10.145);
System.out.println(hoge); // 10.15
String fuga = df.format(13.555);
System.out.println(fuga); // 13.56
JDK1.7.0u21環境での実行結果です。期待した結果が出ています。
Java8環境別バージョン
10.145 は 10.1449(以下略)という内部数値だったはず
コメント欄で上記のご意見をいただいたので、もう一桁加えてみました。
String hoge = df.format(10.1451);
System.out.println(hoge); // 10.15
String fuga = df.format(13.5551);
System.out.println(fuga); // 13.56
期待した結果が出ました。
おまけ:Java8環境でバイナリ的に綺麗になるように
丸め誤差の問題であれば、バイナリ的に綺麗になる $2^{-3}=0.125$ を処理する分には問題が起きないはずですね。
String piyo = df.format(0.125);
System.out.println(piyo); // 0.13
期待した結果が出ました。
まとめ
というわけで、古き良き浮動小数点の丸め誤差問題でした。
「java.text.DecimalFormatの四捨五入が切り捨てになるバグ」というタイトルで公開しましたが、実際には切り捨てではなく「丸め誤差で5が4扱いになってしまう」という現象でしたので、少しタイトルを変えました。