ナカザンドットネット

それって私の感想ですよね

java.text.DecimalFormatの四捨五入が失敗するバグ

この記事はQiitaからの転載です。


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

現象

// Java8DecimalFormat.java
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環境

// jdk1.7.0_21
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(以下略)という内部数値だったはず

コメント欄で上記のご意見をいただいたので、もう一桁加えてみました。

// jdk1.8.0_25
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$ を処理する分には問題が起きないはずですね。

// jdk1.8.0_25
String piyo = df.format(0.125);
System.out.println(piyo); // 0.13

期待した結果が出ました。

まとめ

というわけで、古き良き浮動小数点の丸め誤差問題でした。

「java.text.DecimalFormatの四捨五入が切り捨てになるバグ」というタイトルで公開しましたが、実際には切り捨てではなく「丸め誤差で5が4扱いになってしまう」という現象でしたので、少しタイトルを変えました。