税込 1,100円、消費税10%、端数は切り捨て。税抜はいくらでしょうか。
1,100 ÷ 1.1 = 1,000 なので、答えは 1,000円 です。ところが PHP・Python・Go・JavaScript の4言語とも、999円(税額 101円)を返しました。
前回は、言語によって答えが「割れた」ケースでした。今回は、4言語がそろって同じように間違えたケースです。言語を変えても直らないので、比べるだけでは気づけません。
1100 / 1.1 は 999.9999999999999
4言語で、同じ割り算をそのまま実行した結果です。
| 言語 | 1100 / 1.1 | 切り捨てると |
|---|---|---|
| PHP | 999.9999999999999 | 999 |
| Python | 999.9999999999999 | 999 |
| Go | 999.9999999999999 | 999 |
| JavaScript | 999.9999999999999 | 999 |
1.1 は、コンピュータの2進数の小数では正確に表せません。実際に入っている値は 1.100000000000000088817841970012523… と、1.1 よりわずかに大きい数です。それで割るので、結果は 1,000 よりわずかに小さくなり、切り捨てると 999 になります。
4言語とも、小数には同じ規格(IEEE 754 の倍精度)を使っています。だから、そろって同じ答えになりました。1,080円・8%(c011)、110円・10%(c012)でも、同じように1円少なくなりました。
PHP は、画面では 1000 と表示する
PHP で echo 1100 / 1.1; とすると、1000 と表示されます。表示用の桁数で丸めているだけで、中身は 999.9999999999999 のままです。floor() にかけると 999 になります。
echo 1100 / 1.1; // 1000
echo floor(1100 / 1.1); // 999
var_dump(1100 / 1.1); // float(999.9999999999999)
途中の値を echo で確かめても、正しく見えてしまいます。
公開中のツールでも起きていた
この仕様は、当社が運営している計算ツール集「シルギア」の、請求書の消費税を計算するツールが元になっています。そのツールでも同じ計算をしていて、税込 1,100円・10%・切り捨てで、税抜を 999円と表示していました。この検証で見つかり、2026年10月9日に直しています。
境目にちょうど乗る値で、答えが変わる
誤差はとても小さいので、ふだんは表に出ません。表に出るのは、切り捨てや四捨五入、「◯以下」の判定の境目に、正しい値がちょうど乗るときです。ほかの仕様でも、同じ形で落ちました。
| 計算 | 正しい値 | 小数での結果 | 何が起きたか |
|---|---|---|---|
| 15m³ × 16.9円(水道料金) | 253.5 | 253.49999999999997 | 四捨五入で 254円のはずが 253円 |
| 0.08GB × 3.5時間 × 25日(通信量) | 7 | 7.000000000000001 | 「7GB 以下」を満たさず、10GB プランを勧めた |
どれも、4言語がそろって同じように間違えました。
偶然正解したケースもある
返品の −1,100円(c018)は、4言語とも正解しました。−999.999… を切り捨てる(負の無限大の方へ)と −1,000 になり、小数の誤差と、負の数での切り捨ての向きの誤り(第3回で取り上げます)が打ち消し合ったためです。
正解表で「通った」からといって、正しく書けているとは限りません。
どう直したか ― 金額を2進数の小数に入れない
金額と税率を、2進数の小数ではなく十進の数として計算しました。
| 言語 | 使ったもの |
|---|---|
| PHP | BcMath\Number(PHP 8.4 から) |
| Python | decimal.Decimal |
| Go | math/big の big.Rat(分数) |
| JavaScript | BigInt で分子と分母を持つ分数 |
Python の例です。
from decimal import Decimal, ROUND_DOWN
(Decimal(1100) / Decimal("1.1")).quantize(Decimal(1), ROUND_DOWN) # 1000
税率は 文字列で Decimal("1.1") と渡します。Decimal(1.1) と小数のまま渡すと、2進数の誤差(1.100000000000000088…)までそのまま十進に写してしまい、意味がありません。
正解表のデータでも、税率や単価は "0.10" のように文字列で持っています。言語ごとに小数の読み方が違うことを、テストに持ち込まないためです。
直した後は、4言語とも請求書の仕様で19件中19件、水道料金の仕様で13件中13件、通信量の仕様で11件中11件が正解と一致しました。
まとめ
- 0.1 や 1.1 は2進数の小数で正確に表せない。1100 / 1.1 は 999.9999999999999 になる
- 4言語とも同じ規格の小数を使うので、そろって同じように間違える。言語を比べるだけでは見つからない
- 誤差が表に出るのは、切り捨て・四捨五入・「◯以下」の境目に正しい値がちょうど乗るとき。テストには境目ちょうどの値を入れる
- 金額は十進の数(
Decimal・BcMath\Number・分数)で計算し、小数は文字列で渡す
この記事のケースの正解表・4言語の結果・直す前と後のコードの違いは、次のページで見られます。
- 請求書の消費税と端数処理(c010・c011・c012・c018)
- 水道料金(上水道・下水道・消費税)(c004)
- データ通信量(ギガ)の目安(c002・c003)
次回は、負の数を入れたときに、Go が止まり、JavaScript が NaN を返したケースを取り上げます。
- 第1回:「四捨五入」と書いたのに、4言語で答えが割れた
- 第2回:1,100 ÷ 1.1 が 1,000 にならない(この記事)
- 第3回:負の数を入れたら、Go は止まり、JavaScript は NaN を返した
- 第4回:大きな整数で、PHP は黙って上限に張りつき、JavaScript は桁が化けた
- 第5回:日本で動かしたときだけ、JavaScript の日付が1日ずれた
- 第6回:1月31日の「1か月後」はいつか
Analyzegear


