概要
消費税計算のバグは、金額が大きくずれるのではなく「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 円ずれる。どちらを採用するかは取引先との合意事項
}
}Version Coverage
switch 式で商品種別による税率の切り替えが簡潔になる。record で金額と税額をまとめると可読性が上がる。
// 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
注意点
BigDecimal の生成には new BigDecimal("0.1") のように文字列を使うこと。double リテラルは誤差が入る。
端数処理のタイミング(明細ごとか合計か)は取引先との合意に依存する。
税率は法改正で変わる可能性があるため、定数管理を推奨する。
現場では「請求書の合計が明細の税込小計の合算とずれる」という問題が頻発する。明細ごとに切り捨てか、合計に対して一括で端数処理するかを設計段階で決め、テストデータに端数が出るケースを含めておくこと。
FAQ
切り捨てを行うタイミングが違うためです。どちらが正しいかではなく取引先との合意の問題なので、請求書の端数処理方式を仕様として明文化しておく必要があります。
0.1 や 0.08 は2進数で正確に表現できず、丸める前の値がすでにずれていることがあります。金額は入口から BigDecimal で持つのが安全です。
定数化したうえで適用開始日とセットで管理すると、税率改定時に過去日付の取引を正しく再計算できます。改定が見込まれるならマスタテーブル化も有効です。