背景
自分のツールサイトに、税抜と税込を相互に計算するページを置いています。
https://hashitosystem.com/tools/taxcalc/
「税込から税抜」で 1,000 を入れてみたら、税抜 909円・消費税 91円と出ました。
ところが逆に 909 を「税抜から税込」に入れると、税込は 999円です…。
行って戻ると1円ずれる、という話を調べたので残しておきます。
ツールの中身
ツールの計算は、下記の2行だけでした。
amt は入力欄の金額、r は税率を100で割った値(10%なら 0.1)です。
// 税抜→税込(ex2in): 消費税を1円未満切り捨て
ex = amt; tax = Math.floor(ex * r); inc = ex + tax;
// 税込→税抜(in2ex): 税抜を四捨五入
inc = amt; ex = Math.round(inc / (1 + r)); tax = inc - ex;
行きは「消費税を切り捨て」、帰りは「税抜を四捨五入」です。
端数を丸める対象が、行きと帰りで違っていました。
税込1,000円は行きの計算では作れない
行きの計算で、税抜 909 と 910 を入れると下記になります。
税抜 909 → 消費税 90 → 税込 999
税抜 910 → 消費税 91 → 税込 1001
税抜が1円増えると、消費税が切り捨ての境目をまたいで、税込が2円増える場所がある、ということだ。
そのとき間の 1,000 は飛ばされます。
税抜 0〜2000 を全部回して、税込 990〜1110 のうち一度も出てこない額を数えたのが下記です。
const RATE = 10; // 税率(%)
// 税抜基準: 税抜×税率を切り捨てたものを消費税にする
function taxFromExclusive(ex) { return Math.floor(ex * RATE / 100); }
const reach = new Set();
for (let ex = 0; ex <= 2000; ex++) reach.add(ex + taxFromExclusive(ex));
const miss = [];
for (let inc = 990; inc <= 1110; inc++) if (!reach.has(inc)) miss.push(inc);
console.log(miss.join(' '));
// 1000 1011 1022 1033 1044 1055 1066 1077 1088 1099 1110
Node.js で実行すると、11円おきに作れない額が並びました。
1〜100,000 まで広げると、作れない税込額は 9,091 個で、ほぼ11個に1個です。
ツールが 1,000 に対して出した「税抜 909」は、行きに入れ直すと 999 になる数字だった、というわけです。
逆算は「税込基準」として割り戻す
税込 1,000円の商品は普通にあります。
これは税抜から作った値段ではなく、税込で値段を決めて、そこから消費税を割り戻している、と考えると筋が通ります。
割り戻しは、税込額のうち消費税にあたる部分を「税込 × 税率 ÷ (100 + 税率)」で取り出す計算だ。
下記の taxFromInclusive は、そのとき端数を切り捨てる版です(RATE は上と同じ)。
// 税込基準: 税込×税率÷(100+税率) を切り捨てたものを消費税にする
function taxFromInclusive(inc) { return Math.floor(inc * RATE / (100 + RATE)); }
for (const inc of [1000, 1100, 2200]) {
const tax = taxFromInclusive(inc);
console.log(inc, '→ 消費税', tax, '税抜', inc - tax);
}
// 1000 → 消費税 90 税抜 910
// 1100 → 消費税 100 税抜 1000
// 2200 → 消費税 200 税抜 2000
1,000円は消費税 90円・税抜 910円になりました。
ツールの 91円・909円とは、どちらも1円違います。
ツールの四捨五入は、税抜基準とも税込基準とも合わない第3の数字を出していたことになります。
税込 1,000〜10,000 のうち、行きの計算で作れる 8,182 個に限っても、四捨五入の逆算が行きと合わないものが 3,272 個ありました。
0.1 を掛けてから 1.1 で割ると 1円減る
割り戻しを書くとき、最初は inc * 0.1 / 1.1 と書きかけました。
これが下記のように、ちょうど割り切れる額で1円少なくなります。
console.log(Math.floor(1100 * 0.1 / 1.1)); // 99
console.log(Math.floor(1100 * 10 / 110)); // 100
console.log(1100 * 0.1 / 1.1); // 99.99999999999999
0.1 や 1.1 は2進数の小数で正確に表せないので、計算の途中で 100 がわずかに小さくなり、切り捨てで 99 に落ちる、という仕組みだ。
2,200 や 4,400 でも同じことが起きました。
税率を整数のまま掛けて最後に1回だけ割れば、整数どうしの割り算になるので、この問題は出ません。
なぜ両方を残すのか
どちらか片方に揃えればすっきりする気もします。
でも、税抜で値付けする店と、税込で値付けする店のどちらもあり、端数の処理も事業者で違います。
ツールが1つの式だけを「正解」として出すと、どちらかの人には合わない数字になってしまいます…。
なので、逆算は「税抜を丸める」のではなく「消費税を割り戻して丸める」に直し、どちらの基準の計算かを画面に書くのが正しい形だと考えています。
税込から税抜を出すコードを書くなら、税率は整数のまま掛けて割り、消費税のほうを切り捨ててください。
本記事はAI補助で執筆した、個人開発の紹介記事です。