概要

消費税計算のバグは、金額が大きくずれるのではなく「1円だけ合わない」形で現れます。明細ごとに税額を切り捨てて合算した金額と、合計に対して一度だけ税率を掛けた金額は一致しないことがあり、経理部門からの「請求書の合計が合わない」という問い合わせの多くはこれが原因です。さらに double で計算していると、そもそも 0.1 を2進数で正確に表現できないため、端数処理以前の段階で誤差が混入します。この記事では、BigDecimal を使った税抜・税込の相互変換を基本に、RoundingMode の選び方、軽減税率(8%)と標準税率(10%)の混在、明細単位か合計単位かという端数処理の設計判断までを整理します。

使いどころ

販売管理システムで、明細行ごとに税率(8%/10%)を判定して税額を計算し、税率別の小計欄を作る

税込 1,980 円の商品について、経理仕訳用に税抜金額と消費税額を逆算する

見積書の合計欄の端数処理を「明細ごと切り捨て」に統一し、再計算チェックを通す

コード例

消費税計算ユーティリティ
import java.math.BigDecimal;
import java.math.RoundingMode;

public class TaxCalculator {

    private static final BigDecimal STANDARD_RATE =
        new BigDecimal("0.10");
    private static final BigDecimal REDUCED_RATE =
        new BigDecimal("0.08");

    public static BigDecimal withTax(
            BigDecimal price, BigDecimal rate,
            RoundingMode mode) {
        var tax = price.multiply(rate)
            .setScale(0, mode);
        return price.add(tax);
    }

    public static BigDecimal withoutTax(
            BigDecimal priceWithTax, BigDecimal rate,
            RoundingMode mode) {
        return priceWithTax.divide(
            BigDecimal.ONE.add(rate), 0, mode);
    }

    public static void main(String[] args) {
        var price = new BigDecimal("1980");
        System.out.println("税込(10%): "
            + withTax(price, STANDARD_RATE, RoundingMode.FLOOR));
        // → 2178

        // 税込からの逆算: 2178 円 → 税抜 1980 円 に戻る
        System.out.println("税抜: "
            + withoutTax(new BigDecimal("2178"),
                STANDARD_RATE, RoundingMode.FLOOR));
        // → 1980

        // 端数処理のタイミングで合計が変わる例: 335 円 × 3 点(10%)
        var item = new BigDecimal("335");
        var perLine = withTax(item, STANDARD_RATE, RoundingMode.FLOOR)
            .multiply(new BigDecimal("3"));
        var onTotal = withTax(item.multiply(new BigDecimal("3")),
            STANDARD_RATE, RoundingMode.FLOOR);
        System.out.println("明細ごとに切り捨て: " + perLine);  // → 1104
        System.out.println("合計に対して切り捨て: " + onTotal); // → 1105
        // 1 円ずれる。どちらを採用するかは取引先との合意事項
    }
}

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

Version Coverage

switch 式で商品種別による税率の切り替えが簡潔になる。record で金額と税額をまとめると可読性が上がる。

Java 17
// Java 17: switch 式で税率を切り替え
String itemType = "food";
BigDecimal rate = switch (itemType) {
    case "food", "newspaper" -> REDUCED_RATE;
    default -> STANDARD_RATE;
};
BigDecimal total = calcTaxIncluded(price, rate);

Library Comparison

標準 API(BigDecimal + RoundingMode)消費税計算の端数処理を自分で制御したいとき。依存ゼロで動作し、端数処理方式を明示的に指定できる。通貨コードや為替の概念は自前で管理する必要がある。
JSR 354 (Money API)通貨型を厳密に扱いたい場合。多通貨対応が必要な場面。標準化されたが JDK に同梱されていないため外部依存が増える。国内の消費税計算だけなら過剰。
Apache Commons Math高精度の数値演算や統計計算が併せて必要な場合。消費税計算だけなら BigDecimal で十分。依存に見合うかはプロジェクト規模による。

注意点

BigDecimal の生成には new BigDecimal("0.1") のように文字列を使うこと。double リテラルは誤差が入る。

端数処理のタイミング(明細ごとか合計か)は取引先との合意に依存する。

税率は法改正で変わる可能性があるため、定数管理を推奨する。

現場では「請求書の合計が明細の税込小計の合算とずれる」という問題が頻発する。明細ごとに切り捨てか、合計に対して一括で端数処理するかを設計段階で決め、テストデータに端数が出るケースを含めておくこと。

FAQ

明細ごとと合計とで税額が1円ずれるのはなぜですか?

切り捨てを行うタイミングが違うためです。どちらが正しいかではなく取引先との合意の問題なので、請求書の端数処理方式を仕様として明文化しておく必要があります。

double で計算して最後に丸めれば十分ではないですか?

0.1 や 0.08 は2進数で正確に表現できず、丸める前の値がすでにずれていることがあります。金額は入口から BigDecimal で持つのが安全です。

税率はコードにどう持たせるべきですか?

定数化したうえで適用開始日とセットで管理すると、税率改定時に過去日付の取引を正しく再計算できます。改定が見込まれるならマスタテーブル化も有効です。

関連書籍

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

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