LoginSignup
3
3

BigDecimal.setScaleのRoundingMode.UPがこんなに切り上がるわけがない

Last updated at Posted at 2024-03-22

こんにちは。

Java、書いてますか?

今回は BigDecimal.setScale(int, RoundingMode) でだいぶ頭を悩ませた挙動と対策について書きます。
なんか調べてて めちゃくちゃムカついちゃった のでその恨み言もあります。

ちなみに当記事ですが 事実としてこう、対策としてはこう 、ということを書いているだけで、申し訳ないですが理屈の深掘りはできませんでした。

動作環境
IDE:IntelliJ IDEA 2023.3.5 (Community Edition)
OS:Windows11
Java:AmazonCorretto 21.0.1

この記事のまとめ

小数第一位を切り上げるコードを書いたよ!

// 当然 1 になるよね!
BigDecimal.valueOf(1.000000000000001d).setScale(0, RoundingMode.UP);

2になったよ!!!

// 切り上げる前に第二位以下を切り捨てちゃおうね!
BigDecimal.valueOf(1.000000000000001d).setScale(1, RoundingMode.DOWN)
                                      .setScale(0, RoundingMode.UP);

 

前置きとして書いておきたい(飛ばしてもいい)

今回調べるに当たって出てきたキュレーションサイト共がだいぶアレで、

Main.java
public static void main(String[] args) {
    BigDecimal bd = BigDecimal.valueOf(1.2345d);
    System.out.println("bd: " + bd.setScale(1, RoundingMode.DOWN)); // bd: 1.2
    System.out.println("bd: " + bd.setScale(2, RoundingMode.DOWN)); // bd: 1.23
    System.out.println("bd: " + bd.setScale(3, RoundingMode.DOWN)); // bd: 1.24
}

「あびゃびゃ~~~~!これで完璧だよ~~~~ん!!
みんな~~!!広告見まくってウチのスクール通ってね~~~~~!!!」

ってサイトばっかりヒットする。
"イ寺や王見テ殳エンジニアが教え"なくていいからもっと初心者に優しい解説を書け。
しかもそういうサイトってこの例のようにimport文すら省略してるから、IDEがパッケージ名被りで自動補完できずにそこで詰まる本当の初心者だってめちゃくちゃいると思う。そもそもコンパイル通らねえし。

 
それとは一切関係ありませんが こちらのChrome拡張、お勧めです。

どんな挙動をするのか

閑話休題。
BigDecimal型の説明は省略しますが、動作としてはこんな感じ。

Main.java
package sample;

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal bd100 = BigDecimal.valueOf(1.00d);
        BigDecimal bd104 = BigDecimal.valueOf(1.04d);
        BigDecimal bd105 = BigDecimal.valueOf(1.05d);
        BigDecimal bd109 = BigDecimal.valueOf(1.09d);
        BigDecimal bd140 = BigDecimal.valueOf(1.40d);
        BigDecimal bd150 = BigDecimal.valueOf(1.50d);
        BigDecimal bd190 = BigDecimal.valueOf(1.90d);
        System.out.println("bd100 = " + bd100);   // bd100 = 1.0
        System.out.println("bd104 = " + bd104);   // bd104 = 1.04
        System.out.println("bd105 = " + bd105);   // bd105 = 1.05
        System.out.println("bd109 = " + bd109);   // bd109 = 1.09
        System.out.println("bd140 = " + bd140);   // bd140 = 1.4
        System.out.println("bd150 = " + bd150);   // bd150 = 1.5
        System.out.println("bd190 = " + bd190);   // bd190 = 1.9
    }
}

これは当然ですね。

次に、BigDecimal型で「小数第二位を切り捨てたい」とか「小数第一位を四捨五入したい」と言った場合のコード例はこうなります。

Main.java
package sampleapp;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        // この部分は同じ
        BigDecimal bd100 = BigDecimal.valueOf(1.00d);
        BigDecimal bd104 = BigDecimal.valueOf(1.04d);
        BigDecimal bd105 = BigDecimal.valueOf(1.05d);
        BigDecimal bd109 = BigDecimal.valueOf(1.09d);
        BigDecimal bd140 = BigDecimal.valueOf(1.40d);
        BigDecimal bd150 = BigDecimal.valueOf(1.50d);
        BigDecimal bd190 = BigDecimal.valueOf(1.90d);

        // 小数第二位を切り捨てる
        System.out.println("bd100 = " + bd100.setScale(1, RoundingMode.DOWN));   // bd100 = 1.0
        System.out.println("bd104 = " + bd104.setScale(1, RoundingMode.DOWN));   // bd104 = 1.0
        System.out.println("bd105 = " + bd105.setScale(1, RoundingMode.DOWN));   // bd105 = 1.0
        System.out.println("bd109 = " + bd109.setScale(1, RoundingMode.DOWN));   // bd109 = 1.0
        System.out.println("bd140 = " + bd140.setScale(1, RoundingMode.DOWN));   // bd140 = 1.4
        System.out.println("bd150 = " + bd150.setScale(1, RoundingMode.DOWN));   // bd150 = 1.5
        System.out.println("bd190 = " + bd190.setScale(1, RoundingMode.DOWN));   // bd190 = 1.9

        // 小数第一位を四捨五入
        System.out.println("bd100 = " + bd100.setScale(0, RoundingMode.HALF_UP));   // bd100 = 1
        System.out.println("bd104 = " + bd104.setScale(0, RoundingMode.HALF_UP));   // bd104 = 1
        System.out.println("bd105 = " + bd105.setScale(0, RoundingMode.HALF_UP));   // bd105 = 1
        System.out.println("bd109 = " + bd109.setScale(0, RoundingMode.HALF_UP));   // bd109 = 1
        System.out.println("bd140 = " + bd140.setScale(0, RoundingMode.HALF_UP));   // bd140 = 1
        System.out.println("bd150 = " + bd150.setScale(0, RoundingMode.HALF_UP));   // bd150 = 2
        System.out.println("bd190 = " + bd190.setScale(0, RoundingMode.HALF_UP));   // bd190 = 2
    }
}

この他の丸めモード(切り捨てや四捨五入のことを丸めと言います)としては
ROUND_CEILING:正の無限大に近づくように丸めるモードです。
ROUND_FLOOR:負の無限大に近づくように丸めるモードです。
などがありまーす。
あとはjavadoc勝手に調べてねでもその前に ウチのスクールに入るとこんなに便利だよ就職率が云々フリーランスで月収xx万円目指して云々云々~~~!!!!

じゃねんじゃ滅びろ。
Java9以降の癖にBigDecimal.ROUND_UPを使うな。
※:非推奨のためRoundingMode.UPを使うのが正解。

RoundingMode.UPの挙動もすごいぞ!

Main.java
package sampleapp;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * ついでに何の説明もなくtoPlainStringを使った挙句、<br>
 * これで「RoundingMode.UPの解説です!」ってドヤ顔してるサイトも閉鎖すべき.
 */
public class Main {
    public static void main(String[] args) throws Exception {
        BigDecimal bd = BigDecimal.valueOf(12.345d);
        System.out.println("小数第一位を切り上げ = " + bd100.setScale(1, RoundingMode.UP).toPlainString());
        System.out.println("一の位を切り上げ = " + bd100.setScale(0, RoundingMode.UP).toPlainString());
        System.out.println("十の位を切り上げ = " + bd100.setScale(-1, RoundingMode.UP).toPlainString());

        // toPlaingStringを使わなかった場合
        System.out.println("十の位を切り上げ = " + bd100.setScale(-1, RoundingMode.UP));
    }
}
結果
小数第一位を切り上げ =  12.4
一の位を切り上げ = 13
十の位を切り上げ = 20
十の位の切り上げ = 2E+1

どの桁で切り上げ/切り捨てたいか? も設定できて嬉しいですよね。
こんな感じで切り捨てと同じ動きをすると思っていましたが・・・・・・。

Main.java
package sampleapp;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal bd100 = BigDecimal.valueOf(1.00d);
        BigDecimal bd104 = BigDecimal.valueOf(1.04d);
        BigDecimal bd105 = BigDecimal.valueOf(1.05d);
        BigDecimal bd109 = BigDecimal.valueOf(1.09d);
        BigDecimal bd140 = BigDecimal.valueOf(1.40d);
        BigDecimal bd150 = BigDecimal.valueOf(1.50d);
        BigDecimal bd190 = BigDecimal.valueOf(1.90d);
        
        // 第一引数がさっきの四捨五入と同じ桁数だから、小数第一位を切り上げるんだよね
        // ということは当然 "1.00~1.09 → 1" で "1.40~1.90 → 2" じゃない?       // 予想
        System.out.println("bd100 = " + bd100.setScale(0, RoundingMode.UP));   // bd100 = 1
        System.out.println("bd104 = " + bd104.setScale(0, RoundingMode.UP));   // bd104 = 1
        System.out.println("bd105 = " + bd105.setScale(0, RoundingMode.UP));   // bd105 = 1
        System.out.println("bd109 = " + bd109.setScale(0, RoundingMode.UP));   // bd109 = 1
        System.out.println("bd140 = " + bd140.setScale(0, RoundingMode.UP));   // bd140 = 2
        System.out.println("bd150 = " + bd150.setScale(0, RoundingMode.UP));   // bd150 = 2
        System.out.println("bd190 = " + bd190.setScale(0, RoundingMode.UP));   // bd190 = 2
    }
}
結果
bd100 = 1
bd104 = 2  // は?
bd105 = 2  // 
bd109 = 2  //
bd140 = 2
bd150 = 2
bd190 = 2

なんで?

floatやdoubleと言った浮動小数点型は 超小さい数の精度が悪い という話は何度も聞いたことがあると思いますし、厳密にしたければ BigDecimalを使おうね! という話も聞いたことがあると思います。

でも実際はこう。
BigDecimalが担保する厳密さは整数部分まで。

Main.java
package sampleapp;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    // 全部小数第一位の切り上げなので、いずれの期待値も 1 です
    public static void main(String[] args) {
        BigDecimal bd1 = BigDecimal.valueOf(1.00000000000000011d);
        System.out.println("bd1 = " + bd1.setScale(0, RoundingMode.UP));

        BigDecimal bd2 = BigDecimal.valueOf(1.00000000000000012d);
        System.out.println("bd2 = " + bd2.setScale(0, RoundingMode.UP));

        BigDecimal bd3 = BigDecimal.valueOf(1.00000000000000100d);
        System.out.println("bd3 = " + bd3.setScale(0, RoundingMode.UP));
    }
}
結果
bd1 = 1
bd2 = 2
bd3 = 2

無理数が二進数で表現できないという事実からなんとなくは納得できるんですけどね・・・・・・。
doubleだろうがBigDecimalだろうが、極々小さい桁の小数の丸め誤差についてはカバーしきれないって認識です。

対策

小数第n位で切り上げたいなら、その前に小数第(n+1)位で切り捨てる処理を噛ませよう!!

12.3456の小数第二位を切り上げて12.4にしたいなら、
0.0056部分を切り捨てて12.34にしてから切り上げようねって話です。

Main.java
package sampleapp;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal bd100 = BigDecimal.valueOf(1.00d);
        BigDecimal bd104 = BigDecimal.valueOf(1.04d);
        BigDecimal bd105 = BigDecimal.valueOf(1.05d);
        BigDecimal bd109 = BigDecimal.valueOf(1.09d);
        BigDecimal bd140 = BigDecimal.valueOf(1.40d);
        BigDecimal bd150 = BigDecimal.valueOf(1.50d);
        BigDecimal bd190 = BigDecimal.valueOf(1.90d);

        System.out.println("bd100 = " + bd100.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd104 = " + bd104.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd105 = " + bd105.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd109 = " + bd109.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd140 = " + bd140.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd150 = " + bd150.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
        System.out.println("bd190 = " + bd190.setScale(1, RoundingMode.DOWN)
                                             .setScale(0, RoundingMode.UP));
    }
}

結果
// 期待通りだね!
bd100 = 1
bd104 = 1
bd105 = 1
bd109 = 1
bd140 = 2
bd150 = 2
bd190 = 2

参考サイト

BigDecimal - javadoc
RoundingMode - javadoc

SpecialThanks

有象無象のコピペ記事とその運営元
それらを検索結果から除外できるChrome拡張のゴシップブロッカー

3
3
1

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
3
3