CODE Polyglot / 直した不具合

割引後の価格が1円ずれ、割引額と足すと元の価格を超える

45円の30%引き → 31円(正しくは32円)

どの言語で起きるか

シルギアのツールは JavaScript で書かれています。この不具合が見つかったケースを、CODE Polyglot で10言語の「よくある書き方」で回した結果です(言語を返した答えごとにまとめ、赤い値は実際に返した間違った答え)。 JavaScript は 4件中4件で外れました。

ケース正解よくある書き方の答え(10言語)
c00330%引きで .5 ちょうど
discounted
32
off
13
with_tax
32
  • PHP・Python・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swiftdiscounted31off14with_tax31
  • C#✓ 正解
c00430%引きで .5 ちょうど(その2)
discounted
60
off
25
with_tax
60
  • PHP・Python・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swiftdiscounted59off26with_tax59
  • C#✓ 正解
c00533%引きで .5 ちょうど
discounted
101
off
49
with_tax
101
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftdiscounted100off50with_tax100
c00890%引きで 0.5
discounted
1
off
4
with_tax
1
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftdiscounted0off5with_tax0

何が起きていたか

割引後の価格を計算するツールで、2つの表示がずれていました。

  • 割引後の価格が1円少なくなる: 45円の30%引きは 45 × 0.7 = 31.5円で、四捨五入して32円が正解です。ツールは 31円 と表示していました。85円の30%引き(59.5 → 60円が正解)も 59円、150円の33%引き(100.5 → 101円が正解)も 100円 でした
  • 割引後と割引額を足すと、元の価格を超える: 1,005円の50%引きで、ツールは割引後 503円・割引額 503円 と表示していました。足すと1,006円で、元の価格より1円多くなります

どのくらいの入力で起きていたか

直す前の式を Node でそのまま動かし、整数の分数で計算した正確な値と比べました。範囲は、元の価格 0〜10,000円(1円刻み)× 割引率 0〜100%(1%刻み)の 1,010,101通り です。正しい値は、仕様 割引後の価格と税込 の定義(割引後は四捨五入、割引額は「元の価格 − 割引後」)です。

ずれ件数
割引後の価格が1円少ない3,098通り
割引後と割引額の和が、元の価格を1円超える21,980通り

どちらも、割引後の価格がちょうど「◯◯.5円」になる組み合わせで起きていました。そうした組み合わせは1,010,101通り中26,000通りで、2つのずれは重なっていません。税込の表示は数えていません。

どうやって見つかったか

CODE Polyglot の3本目の仕様 割引後の価格と税込 で、正解表に「割引後がちょうど .5 になる」ケースを入れました(2026-10-10)。よくある書き方は4言語(PHP・Python・Go・JavaScript)とも、45円・85円の30%引き(c003・c004)、150円の33%引き(c005)、5円の90%引き(c008)で外れました。

元のツールの式を Node で動かすと、同じく1円少ない答えになりました。仕様を作るときに元のツールの計算を読み、割引後と割引額を別々に丸めているため、1,005円の50%引きで和が元の価格を超えることも分かりました。

原因

0.7 は、2進数の小数では正確に表せません。45 × (1 − 30 ÷ 100) は 31.5 ではなく 31.499999999999996 になり、四捨五入すると 31 になります。

和が元の価格を超えるのは、計算の決め方の問題です。元のツールは、丸める前の割引後(502.5)と割引額(502.5)を、表示のときに別々に四捨五入していました。どちらも 503 に繰り上がるので、足すと1円多くなります。

どう直したか

計算の部分を calc.js に分け、価格と割引率を十進の文字列のまま分数(BigInt の分子と分母)にしました。あわせて、定義を2点変えました。

  • 割引額は「元の価格 − 丸めた割引後」にする。足すと必ず元の価格になります
  • 税込は、丸めた割引後(実際に払う税抜の金額)に税をかける
- var discounted = price * (1 - rate / 100);
- var off = price - discounted;
- var withTax = discounted * (1 + taxRate / 100);
- return Math.round(n).toLocaleString("ja-JP") + "円";
+ const discounted = roundHalfUpDiv(P.n * (100n * R.d - R.n), P.d * 100n * R.d);
+ const off = basePrice - discounted;
+ withTax = Number(roundHalfUpDiv(discounted * (100n * T.d + T.n), 100n * T.d));
+ return (2n * num + den) / (2n * den);

33.3% のような小数の割引率も、十進のまま正確に計算します。

確かめたこと

  • 本番が配信している calc.js を、301,101通り(無作為な200,000通り+価格 0〜1,000円 × 割引率 0〜100% の全組み合わせ)で正確な値と突き合わせ、301,101件中301,101件が一致。「割引後+割引額=元の価格」が崩れたのは0件(2026-10-10)
  • 修正後の本番のページをヘッドレス Chrome で開き、45円の30%引き → 32円・13円、150円の33%引き → 101円・49円、1,005円の50%引き → 503円・502円と表示されることを確認
  • シルギア側のテスト19件が、すべて通過

← 直した不具合の一覧へ