こんにちは。
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);
前置きとして書いておきたい(飛ばしてもいい)
今回調べるに当たって出てきたキュレーションサイトの内容が中々薄くって、
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がパッケージ名被りで自動補完できずにそこで詰まる本当の初心者だってめちゃくちゃいると思う。コンパイルも通らないし。
どんな挙動をするのか
閑話休題。
BigDecimal型の説明は省略しますが、動作としてはこんな感じ。
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型で「小数第二位を切り捨てたい」とか「小数第一位を四捨五入したい」と言った場合のコード例はこうなります。
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
}
}
※BigDecimal.ROUND_UP
は既に非推奨のためRoundingMode.UP
を使うのが正解。
RoundingMode.UPの挙動もすごいぞ!
package sampleapp;
import java.math.BigDecimal;
import java.math.RoundingMode;
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
どの桁で切り上げ/切り捨てたいか? も設定できて嬉しいですよね。
こんな感じで切り捨てと同じ動きをすると思っていましたが・・・・・・。
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が担保する厳密さは整数部分まで。
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にしてから切り上げようねって話です。
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