Skip to the content.

← 目次← 前: 01次: 03 →

02. 数値の正確な扱い

テーマ: 消費税の内税・外税と端数処理

学習点: float の誤差, decimal.Decimal, 丸めモード(ROUND_HALF_UP / ROUND_HALF_EVEN)

依存: 標準ライブラリのみ / 難易度: 基礎

実行方法

uv run 02_decimal_tax.py

スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは uv が自動的に仮想環境へ導入します。事前の pip install は不要です。

解説

何をするプログラムか

お金の計算では「1 円の誤差」も許されません。ところが Python の float で 0.1 + 0.2 を計算すると 0.30000000000000004 になり、0.3 と一致しません。このスクリプトはまずその現象を実演し、会計計算の正攻法である decimal.Decimal を使って、消費税の外税計算(本体価格 → 税額・税込価格)、内税の逆算(税込価格 → 本体価格・税額)、端数 0.5 円が発生するときの丸めモードの違い、軽減税率 8% と標準税率 10% が混在するレシートの合計、という実務でそのまま登場する 4 つの計算を行います。

コードの読みどころ

理論的背景

float は IEEE 754 の二進浮動小数点です。0.1 は十進では有限桁でも二進では無限循環小数になるため、有限の 64 ビットに収めた時点で必ず誤差が生じます。1 回の計算では 10⁻¹⁷ 程度の微小な差でも、比較(==)や大量の集計では顕在化します。そこで会計処理では十進で正確に表現できる Decimal を使うのが原則です。

丸めモードのうち ROUND_HALF_EVEN(端数 0.5 は偶数側に丸める)は、常に切り上げる四捨五入が持つ上方向への系統的な偏りを打ち消すため、統計・金融の集計で好まれ、IEEE 754 の既定の丸め方式でもあります。

実行結果の見方

冒頭の [float の誤差]False0.30000000000000004 が表示され、同じ計算が Decimal では True になることが出発点です。[丸めモードの違い] では 0.5・2.5 が四捨五入と銀行家丸めで異なる値になり(1.5・3.5 は一致)、偶数側へ丸めるという規則が確認できます。最後のレシートでは、税率の異なる 3 商品の税額が明細ごとに丸められ、小計 1,050 円・消費税 90 円・合計 1,140 円と 1 円の狂いもなく集計されています。

ソースコード

# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""02: 数値の正確な扱い ---------------------------------------------------
テーマ: 消費税の内税・外税と端数処理
学習点: float の誤差, decimal.Decimal, 丸めモード(ROUND_HALF_UP / ROUND_HALF_EVEN)
根拠: 二進浮動小数点(IEEE 754)では 0.1 を有限桁で表現できないため誤差が出る。
      会計処理では十進固定小数点(Decimal)を使うのが原則。
"""
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN, getcontext

getcontext().prec = 28
RATE = Decimal("0.10")          # 標準税率10%
REDUCED = Decimal("0.08")       # 軽減税率8%


def yen(x: Decimal, mode=ROUND_HALF_UP) -> Decimal:
    """円未満を指定モードで丸める(1円単位)。"""
    return x.quantize(Decimal("1"), rounding=mode)


def tax_exclusive(price: Decimal, rate: Decimal = RATE) -> tuple[Decimal, Decimal]:
    """外税: 本体価格から税額と税込価格を返す。"""
    tax = yen(price * rate)
    return tax, price + tax


def tax_inclusive(total: Decimal, rate: Decimal = RATE) -> tuple[Decimal, Decimal]:
    """内税: 税込価格から本体価格と税額を逆算する。"""
    body = yen(total / (1 + rate))
    return body, total - body


def main() -> None:
    print("[float の誤差]")
    print("  0.1 + 0.2 == 0.3 ->", 0.1 + 0.2 == 0.3, " 実際:", repr(0.1 + 0.2))
    print("  Decimal では ->", Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))

    print("\n[外税計算]")
    for p in ["980", "1250", "3333", "105"]:
        tax, total = tax_exclusive(Decimal(p))
        print(f"  本体 {p:>5}円 -> 税 {tax:>4}円 / 税込 {total:>5}円")

    print("\n[内税からの逆算]")
    for t in ["1078", "1000", "550"]:
        body, tax = tax_inclusive(Decimal(t))
        print(f"  税込 {t:>5}円 -> 本体 {body:>5}円 / 税 {tax:>4}円")

    print("\n[丸めモードの違い] 0.5円が発生するケース")
    for v in ["0.5", "1.5", "2.5", "3.5"]:
        d = Decimal(v)
        print(f"  {v}: 四捨五入={yen(d, ROUND_HALF_UP)}  "
              f"銀行家丸め={yen(d, ROUND_HALF_EVEN)}")

    print("\n[軽減税率との混在レシート]")
    items = [("弁当", Decimal("580"), REDUCED), ("お茶", Decimal("150"), REDUCED),
             ("文具", Decimal("320"), RATE)]
    subtotal = sum(p for _, p, _ in items)
    tax_sum = sum(yen(p * r) for _, p, r in items)
    for n, p, r in items:
        print(f"  {n:<4}{p:>5}円 (税率{r:.0%})")
    print(f"  小計 {subtotal}円 / 消費税 {tax_sum}円 / 合計 {subtotal + tax_sum}円")


if __name__ == "__main__":
    main()

実行結果

[float の誤差]
  0.1 + 0.2 == 0.3 -> False  実際: 0.30000000000000004
  Decimal では -> True

[外税計算]
  本体   980円 -> 税   98円 / 税込  1078円
  本体  1250円 -> 税  125円 / 税込  1375円
  本体  3333円 -> 税  333円 / 税込  3666円
  本体   105円 -> 税   11円 / 税込   116円

[内税からの逆算]
  税込  1078円 -> 本体   980円 / 税   98円
  税込  1000円 -> 本体   909円 / 税   91円
  税込   550円 -> 本体   500円 / 税   50円

[丸めモードの違い] 0.5円が発生するケース
  0.5: 四捨五入=1  銀行家丸め=0
  1.5: 四捨五入=2  銀行家丸め=2
  2.5: 四捨五入=3  銀行家丸め=2
  3.5: 四捨五入=4  銀行家丸め=4

[軽減税率との混在レシート]
  弁当    580円 (税率8%)
  お茶    150円 (税率8%)
  文具    320円 (税率10%)
  小計 1050円 / 消費税 90円 / 合計 1140円

← 目次← 前: 01次: 03 →