概要

「達成率の合計が 100% にならない」「前年比の符号が逆になっている」――割合計算はロジック自体が単純なだけに、こうした指摘が帳票のリリース直前に飛んでくると原因の切り分けに手間取ります。原因の多くは、double の丸め誤差の蓄積か、分母の定義のあいまいさです。前年比なのか前月比なのか、分母がゼロの月をどう扱うのか。計算式より先にここを固めておかないと、実装は何度も手戻りします。この記事では、BigDecimal による割合・増減率・逆算の実装を軸に、MathContext での精度管理、ゼロ除算のガード、表示桁数の統一といった、レポート系機能で毎回問われるポイントを整理します。

使いどころ

月次売上レポートで前年同月比を算出し、前年実績がゼロの月は「-」表示に振り分ける

KPI ダッシュボードで複数指標の達成率を小数第1位に統一して表示する

「粗利率 35% を確保できる売価」を原価から逆算して見積画面に提示する

コード例

割合と増減率の計算
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class PercentageCalc {

    private static final MathContext MC =
        new MathContext(10, RoundingMode.HALF_UP);

    public static BigDecimal percentage(
            BigDecimal part, BigDecimal total) {
        if (total.compareTo(BigDecimal.ZERO) == 0) {
            throw new ArithmeticException("ゼロ除算");
        }
        return part.divide(total, MC)
            .multiply(new BigDecimal("100"))
            .setScale(1, RoundingMode.HALF_UP);
    }

    public static BigDecimal changeRate(
            BigDecimal previous, BigDecimal current) {
        if (previous.compareTo(BigDecimal.ZERO) == 0) {
            throw new ArithmeticException("ゼロ除算");
        }
        return current.subtract(previous)
            .divide(previous, MC)
            .multiply(new BigDecimal("100"))
            .setScale(1, RoundingMode.HALF_UP);
    }

    public static void main(String[] args) {
        var total = new BigDecimal("2500");
        var part = new BigDecimal("750");
        System.out.println("構成比: " + percentage(part, total) + "%");
        // → 30.0%

        var prev = new BigDecimal("1000");
        var curr = new BigDecimal("1250");
        System.out.println("増減率: " + changeRate(prev, curr) + "%");
        // → 25.0%

        // 減少ケース: 符号はそのままマイナスになる
        System.out.println("増減率: " + changeRate(curr, prev) + "%");
        // → -20.0%(1250 → 1000)

        // 分母ゼロは例外で止め、表示側で業務ルールに振り分ける
        try {
            percentage(part, BigDecimal.ZERO);
        } catch (ArithmeticException e) {
            System.out.println("前年実績なし: -");
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var と record で計算結果を構造化できる。コードの意図が型で伝わりやすくなる。

Java 17
// Java 17: var + record で計算結果を構造化
record CalcResult(BigDecimal value, String label) {}
var part = new BigDecimal("750");
var total = new BigDecimal("2500");
var result = new CalcResult(
    percentage(part, total), "売上構成比");
System.out.println(result.label()
    + ": " + result.value() + "%");

Library Comparison

Apache Commons Math統計計算や高度な数値演算が必要な場合。割合計算だけなら標準 API で十分。
Guava(IntMath / DoubleMath)オーバーフローチェック付きの整数演算や、丸めモード指定付きの除算を簡潔に書きたいとき。BigDecimal ベースの割合計算には直接対応しない。整数演算のユーティリティが中心で、業務系の精度管理には BigDecimal を併用する形になる。

注意点

割り算の結果が割り切れない場合は必ず RoundingMode を指定すること。

パーセント表示は「×100」するタイミングに注意。計算途中で百分率にしない。

ゼロ除算は事前チェックで防ぐ。BigDecimal.ZERO との比較には compareTo を使う。

実務では「前年比」「達成率」など用語ごとに分母が何かを明確にする必要がある。計算ロジックを実装する前に、ビジネス側と定義を合わせておくことで後からの仕様変更を防げる。

FAQ

前年実績が 0 のとき前年比はどう表示すべきですか?

数学的には計算不能なので、ゼロ除算ガードで弾いたうえで「-」や「新規」など業務的に意味の通る表示に振り分けます。この表示ルールは仕様として先に決めておきます。

割合は小数と百分率のどちらで持ち回すべきですか?

内部では 0.35 のような実数で統一し、×100 は表示の直前だけにするのが安全です。計算途中で百分率に変えると二重掛けのバグが起こりやすくなります。

MathContext と setScale はどう使い分けますか?

MathContext は計算途中の有効桁数の確保、setScale は最終表示の小数桁の統一に使います。除算時の ArithmeticException 防止には MathContext が必須です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます