請求書の消費税と端数処理

税抜・税込の金額から消費税額を出す。切り捨て・四捨五入・切り上げを選べる、請求書向けの計算。

2進小数の誤差0.5 の丸め方負の数大きな整数

言語ごとの結果

正解表 19件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方14 / 19

直した書き方19 / 19

PythonPython 3.14.7

よくある書き方11 / 19

直した書き方19 / 19

GoGo 1.27.2

よくある書き方14 / 19

直した書き方19 / 19

JavaScriptNode 24.15.0

よくある書き方12 / 19

直した書き方19 / 19

なぜ割れたのか

よくある書き方が落ちたケースを、原因ごとにまとめた解説です(3本)。書いてある「落ちた言語」が今回の検証結果と一致することを確かめてから載せています。

0.5 の丸め方

「四捨五入」の結果が、言語によって3通りに割れた

対象のケースc004c005c016 落ちた言語PythonJavaScript

税抜 1,045円・10%・四捨五入の消費税は、104.5 円を四捨五入して 105円 が正解です。ところが Python だけ 104円 を返しました(c004)。返品の −1,045円(c016)では、Python と JavaScript が −104円 を返し、PHP と Go だけが正解の −105円を返しました。

なぜ割れたのか

どの言語も「四捨五入らしい関数」を持っていますが、0.5 ちょうどの扱いが違います。

言語使った関数104.5−104.50.5 ちょうどの扱い
PHPround()105−1050 から遠い方へ
Pythonround()104−104偶数の方へ(銀行丸め)
Gomath.Round()105−1050 から遠い方へ
JavaScriptMath.round()105−104正の無限大の方へ

Python 3 の round() は偶数丸めです。104.5 は偶数の 104 に、50.5(c005)は偶数の 50 になります。JavaScript の Math.round() は 0.5 を常に大きい方へ丸めるので、正の数では四捨五入と同じに見えますが、負の数では −104.5 → −104 と、0 に近い方へ寄ります。

どう直したか

言語の既定に任せず、丸め方を名前で指定しました。Python では decimal の丸めモードを使います。

ROUNDING = {
    "floor": ROUND_DOWN,     # 切り捨て
    "round": ROUND_HALF_UP,  # 四捨五入(偶数丸めではない)
    "ceil": ROUND_UP,        # 切り上げ
}

PHP(RoundingMode::HalfAwayFromZero)も同じ考え方です。Go と JavaScript には丸めモードを指定する仕組みがないため、商と余りから四捨五入を自分で書きました。

2進小数の誤差

税込からの割戻しで、税抜が1円少なくなった

対象のケースc010c011c012 落ちた言語PHPPythonGoJavaScript

税込 1,100円・10%・切り捨てで税抜を求めると、1,100 ÷ 1.1 = 1,000円 ちょうどです。ところが4言語とも 999円(税額 101円)を返しました。1,080円・8%(c011)、110円・10%(c012)も同じように1円少なくなりました。

なぜ割れたのか

0.1 や 1.1 は、コンピュータの2進数の小数では正確に表せません。そのため 1100 / 1.1 の結果は 1000 ではなく 999.9999999999999 になります。これを切り捨てると 999 です。4言語とも同じ規格(IEEE 754)の小数を使うので、そろって同じ答えになりました。言語で割れたのではなく、全員が同じように間違えたケースです。

同じ計算は、仕様の元になったシルギアの公開ツールでも起きていて、この検証をきっかけに直しました。

偶然正解したケース

返品の −1,100円(c018)は、4言語とも正解しました。−999.999… を数学の切り捨て(負の無限大の方向)にかけると −1000 になり、小数の誤差と切り捨ての向きの誤り(返品の切り捨てが逆向きになる問題)が打ち消し合ったためです。正解したからといって、正しく書けているとは限りません。

どう直したか

金額と税率を、2進数の小数ではなく十進の数として計算しました。PHP は BcMath\Number、Python は decimal、Go は math/big.Rat(分数)、JavaScript は BigInt を使った分数です。1,100 ÷ 1.1 はどれでも正確に 1,000 になります。

負の数

返品(負の金額)の切り捨てと切り上げが、逆向きになった

対象のケースc015c017 落ちた言語PHPPythonGoJavaScript

返品 −1,055円・10%の消費税は −105.5円です。商習慣の切り捨てでは −105円、切り上げでは −106円 になります(同じ額の請求をちょうど打ち消す値)。ところが4言語とも、切り捨てで −106円、切り上げで −105円 を返しました。

なぜ割れたのか

各言語の floor(切り捨て)と ceil(切り上げ)は、数学の定義どおりに動きます。

  • floor は負の無限大の方向へ丸める:−105.5 → −106
  • ceil は正の無限大の方向へ丸める:−105.5 → −105

正の数では日常の「切り捨て・切り上げ」と同じ結果になるので気づきにくく、負の数を入れて初めて向きが逆だと分かります。

どう直したか

日常の意味は「絶対値で切り捨てる(0 の方向)」「絶対値で切り上げる(0 から遠い方向)」です。これに合う丸め方を名前で指定しました。

  • PHP: RoundingMode::TowardsZero と RoundingMode::AwayFromZero
  • Python: decimal の ROUND_DOWN と ROUND_UP(どちらも絶対値基準)
  • Go・JavaScript: 0 の方向に切り捨てる整数の割り算の結果に、符号を見て 1 を足す処理を書いた

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。

ケース入力正解 PHPPythonGoJavaScript
c001基本・税抜から・10%・切り捨て
basis
exclusive
amount
1000
rate
0.10
rounding
floor
exclusive
1000
tax
100
inclusive
1100
✓✓✓✓
c002端数0.5・切り捨て 105.5 を切り捨て
basis
exclusive
amount
1055
rate
0.10
rounding
floor
exclusive
1055
tax
105
inclusive
1160
✓✓✓✓
c003端数0.5・切り上げ 105.5 を切り上げ
basis
exclusive
amount
1055
rate
0.10
rounding
ceil
exclusive
1055
tax
106
inclusive
1161
✓✓✓✓
c004端数0.5ちょうど・四捨五入(偶数丸めだと割れる) 104.5 → 105。偶数丸め(Python の round など)だと 104 になる なぜ? 解説を読む
basis
exclusive
amount
1045
rate
0.10
rounding
round
exclusive
1045
tax
105
inclusive
1150
✓tax104inclusive1149✓✓
c005任意税率5%・端数0.5ちょうど・四捨五入 50.5 → 51。偶数丸めだと 50 なぜ? 解説を読む
basis
exclusive
amount
1010
rate
0.05
rounding
round
exclusive
1010
tax
51
inclusive
1061
✓tax50inclusive1060✓✓
c006軽減8%・切り捨て 98.72 を切り捨て
basis
exclusive
amount
1234
rate
0.08
rounding
floor
exclusive
1234
tax
98
inclusive
1332
✓✓✓✓
c007軽減8%・四捨五入 98.72 → 99
basis
exclusive
amount
1234
rate
0.08
rounding
round
exclusive
1234
tax
99
inclusive
1333
✓✓✓✓
c0081円・切り上げ 0.1 → 1。1円未満の端数だけでも繰り上がる
basis
exclusive
amount
1
rate
0.10
rounding
ceil
exclusive
1
tax
1
inclusive
2
✓✓✓✓
c0090円 境界値。-0 を出さないこと
basis
exclusive
amount
0
rate
0.10
rounding
round
exclusive
0
tax
0
inclusive
0
✓✓✓✓
c010税込1100円から割戻し・10%・切り捨て 1100 ÷ 1.1 = 1000 ちょうど。2進浮動小数点では 999.999… になり、切り捨てると 999 なぜ? 解説を読む
basis
inclusive
amount
1100
rate
0.10
rounding
floor
exclusive
1000
tax
100
inclusive
1100
exclusive999tax101exclusive999tax101exclusive999tax101exclusive999tax101
c011税込1080円から割戻し・8%・切り捨て 1080 ÷ 1.08 = 1000 ちょうど。浮動小数点だと 999.999… なぜ? 解説を読む
basis
inclusive
amount
1080
rate
0.08
rounding
floor
exclusive
1000
tax
80
inclusive
1080
exclusive999tax81exclusive999tax81exclusive999tax81exclusive999tax81
c012税込110円から割戻し・10%・切り捨て 小さい金額でも同じ罠を踏むか なぜ? 解説を読む
basis
inclusive
amount
110
rate
0.10
rounding
floor
exclusive
100
tax
10
inclusive
110
exclusive99tax11exclusive99tax11exclusive99tax11exclusive99tax11
c013税込1000円から割戻し・切り捨て 909.0909… を切り捨て。税額は差し引きなので 91
basis
inclusive
amount
1000
rate
0.10
rounding
floor
exclusive
909
tax
91
inclusive
1000
✓✓✓✓
c014税込1000円から割戻し・切り上げ 909.0909… を切り上げ
basis
inclusive
amount
1000
rate
0.10
rounding
ceil
exclusive
910
tax
90
inclusive
1000
✓✓✓✓
c015負の金額(返品)・切り捨て -105.5 → -105(0方向)。数学の floor だと -106 なぜ? 解説を読む
basis
exclusive
amount
-1055
rate
0.10
rounding
floor
exclusive
-1055
tax
-105
inclusive
-1160
tax-106inclusive-1161tax-106inclusive-1161tax-106inclusive-1161tax-106inclusive-1161
c016負の金額(返品)・四捨五入 -104.5 → -105(0から遠い方へ)。JS の Math.round は -104 なぜ? 解説を読む
basis
exclusive
amount
-1045
rate
0.10
rounding
round
exclusive
-1045
tax
-105
inclusive
-1150
✓tax-104inclusive-1149✓tax-104inclusive-1149
c017負の金額(返品)・切り上げ -105.5 → -106(0から遠い方へ)。数学の ceil だと -105 なぜ? 解説を読む
basis
exclusive
amount
-1055
rate
0.10
rounding
ceil
exclusive
-1055
tax
-106
inclusive
-1161
tax-105inclusive-1160tax-105inclusive-1160tax-105inclusive-1160tax-105inclusive-1160
c018負の税込金額から割戻し・切り捨て c010 の符号反転。負の側と浮動小数点の両方を踏む
basis
inclusive
amount
-1100
rate
0.10
rounding
floor
exclusive
-1000
tax
-100
inclusive
-1100
✓✓✓✓
c0192^53 を超える金額 倍精度浮動小数点で正確に表せない整数(2^53+1)。JSON の数値の読み方でも割れる
basis
exclusive
amount
9007199254740993
rate
0.10
rounding
floor
exclusive
9007199254740993
tax
900719925474099
inclusive
9907919180215092
✓✓✓exclusive9007199254740992

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

TaxRounding.php+20 −9よくある書き方(ddd1b64)→ 直した書き方

@@ 変更前 1行目〜 / 変更後 1行目〜 @@
11 <?php
22 declare(strict_types=1);
33
4+use BcMath\Number;
5+
46 /**
57 * 請求書の消費税と端数処理。
6− * v1: 素直に書いた版(float と組み込みの floor / round / ceil)。
8+ * v2: 十進(BcMath\Number)で計算し、端数処理は絶対値基準の RoundingMode で明示する。
79 */
8−function applyRounding(float $value, string $mode): int
10+const DIV_SCALE = 20;
11+
12+function roundingMode(string $mode): RoundingMode
913 {
10− return (int) match ($mode) {
11− 'round' => round($value),
12− 'ceil' => ceil($value),
13− default => floor($value),
14+ return match ($mode) {
15+ 'floor' => RoundingMode::TowardsZero, // 切り捨て(負でも 0 方向)
16+ 'round' => RoundingMode::HalfAwayFromZero, // 四捨五入(偶数丸めではない)
17+ 'ceil' => RoundingMode::AwayFromZero, // 切り上げ(負なら -∞ 方向)
18+ default => throw new InvalidArgumentException("unknown rounding: {$mode}"),
1419 };
20+}
21+
22+function toYen(Number $value, string $mode): int
23+{
24+ return (int) $value->round(0, roundingMode($mode))->value;
1525 }
1626
1727 /** @return array{exclusive:int, tax:int, inclusive:int} */
1828 function calcInvoiceTax(string $basis, int $amount, string $rate, string $rounding): array
1929 {
20− $r = (float) $rate;
30+ $a = new Number($amount);
31+ $r = new Number($rate);
2132 if ($basis === 'exclusive') {
22− $tax = applyRounding($amount * $r, $rounding);
33+ $tax = toYen($a * $r, $rounding);
2334 return ['exclusive' => $amount, 'tax' => $tax, 'inclusive' => $amount + $tax];
2435 }
25− $exclusive = applyRounding($amount / (1 + $r), $rounding);
36+ $exclusive = toYen($a->div(1 + $r, DIV_SCALE), $rounding);
2637 return ['exclusive' => $exclusive, 'tax' => $amount - $exclusive, 'inclusive' => $amount];
2738 }

仕様

入力

項目型説明
basis"exclusive" | "inclusive"金額が税抜か税込か
amount整数(円)金額。負の値を許す(返品・値引き)
rate十進文字列税率。"0.10"・"0.08"・任意("0.05" など)
rounding"floor" | "round" | "ceil"1円未満の端数処理(切り捨て・四捨五入・切り上げ)

出力

項目型説明
exclusive整数(円)税抜金額
tax整数(円)消費税額
inclusive整数(円)税込金額

計算

計算は正確な十進(または有理数)で行う。 2進浮動小数点の誤差を結果に出してはいけない。

税抜から(basis = "exclusive")

exclusive = amount
tax       = 端数処理(amount × rate)
inclusive = exclusive + tax

税込から・割戻し(basis = "inclusive")

inclusive = amount
exclusive = 端数処理(amount ÷ (1 + rate))
tax       = inclusive − exclusive

端数処理は税抜金額に対してかける(Shirugear の元ツールと同じ)。

端数処理

日本の商慣行としての意味で定義する。言語の既定の丸めに合わせない。

rounding意味例(正)例(負)
floor切り捨て=絶対値の1円未満を捨てる(0方向)105.5 → 105−105.5 → −105
round四捨五入=絶対値の 0.5 以上を繰り上げる(0から遠い方へ)104.5 → 105−104.5 → −105
ceil切り上げ=絶対値の1円未満を繰り上げる(0から遠い方へ)105.5 → 106−105.5 → −106
  • 四捨五入は偶数丸め(銀行丸め)ではない
  • 負の金額(返品・値引き)は、同額の正の請求をちょうど打ち消す値にする。 つまり f(−x) = −f(x)。数学の floor/ceil(負の無限大/正の無限大方向)とは負の側で答えが変わる
元ツールは負の金額を入力エラーにしている。本 spec では返品・値引きを扱うために負の値を許し、 上の定義で答えを決めた(2026-10-09)。

範囲

  • amount は 64bit 符号付き整数に収まる範囲(c019 は 2^53 を超える値)
  • 入力エラー(不正な basis など)は Phase 0 では扱わない