CODE Polyglot / 直した不具合

本当は同じ単価なのに「A がお得」と判定する

97円・80g と 1,213円・1,000g → 「商品A がお得」(正しくは「どちらも同じ」)

どの言語で起きるか

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

ケース正解よくある書き方の答え(10言語)
c004表示の境目(浮動小数点で下回る)
unit_a
121.3
unit_b
121.3
verdict
same
diff
0.0
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftunit_a"121.2"verdict"A"diff"0.1"
c005表示の境目(浮動小数点で下回る・その2)
unit_a
211.3
unit_b
211.3
verdict
same
diff
0.0
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftunit_a"211.2"verdict"A"diff"0.1"
c006表示の境目(偶数丸めで割れる)
unit_a
81.3
unit_b
81.3
verdict
same
diff
0.0
  • Python・C#unit_a"81.2"verdict"A"diff"0.1"
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解

何が起きていたか

2つの商品の単価を比べて「どちらがお得か」を出すツールで、本当は同じ単価なのに、片方を「お得」と判定していました。

  • 商品A 97円・80g、商品B 1,213円・1,000g: 100gあたりの単価は、A が 97 ÷ 80 × 100 = 121.25円、B が 121.3円です。A を小数第1位に四捨五入すると 121.3円なので、正解は「どちらも同じ」です。ツールは A を 121.2円 と表示し、「商品A がお得(0.1円安い)」 と判定していました
  • 169円・80g(211.25円)と 2,113円・1,000g(211.3円)の組でも、同じように「商品A がお得」でした

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

価格 1〜1,000円(1円刻み)× 内容量 1〜1,000g(1g刻み)の 1,000,000通り で、100gあたりの単価を正確な値と比べました。

ずれ件数
単価の表示が 0.1円少ない1,000,000通り中155通り
単価の表示が 0.1円多い1,000,000通り中0通り

この1,000,000通りから2つを選ぶ組み合わせで、判定も数えました。

ずれ件数
本当は同じ単価なのに「A(または B)がお得」本当は同じ単価の 166,955,732組中41,933組
本当は 0.1円違うのに「どちらも同じ」本当は 0.1円違う 333,114,888組中41,886組

どうやって見つかったか

CODE Polyglot の仕様 単価の比較(どちらがお得か) を作るとき、元のツールと同じ式を4言語で書いて、正確な値と突き合わせました(2026-10-10)。正解表11件のうち、表示の境目を狙った c004(97円・80g)と c005(169円・80g)を、4言語とも「A がお得」と答えました。元のツールと同じ式を Node で評価すると、公開中のツールも同じ答えになることがわかりました。

原因

  • 97 ÷ 80 × 100 を2進数の小数で計算すると、121.25 ではなく 121.24999999999999 になります。0.1円単位で四捨五入すると、0.05 に届かないので 121.2 に切り下がります(解説)
  • ツールは「表示した桁で同じかどうか」を判定していました。そのため、0.1円の切り下がりがそのまま判定の逆転になりました

どう直したか

計算の部分を calc.js に分けました。価格と内容量を十進の文字列のまま整数の分子・分母(BigInt)にし、「単価 × 10」を整数で四捨五入します。判定は、四捨五入した単価どうしで行います。

- var unitA = (ap / aq) * info.base;
- var ra = Math.round(unitA * 10);
- var rb = Math.round(unitB * 10);
- var diff = Math.abs(unitA - unitB);
+ const tenthsA = roundHalfUpDiv(aP.n * aQ.d * base * 10n, aP.d * aQ.n);
+ const tenthsB = roundHalfUpDiv(bP.n * bQ.d * base * 10n, bP.d * bQ.n);
+ if (tenthsA === tenthsB) winner = "same";
+ else winner = tenthsA < tenthsB ? "A" : "B";
+ const diffT = tenthsA > tenthsB ? tenthsA - tenthsB : tenthsB - tenthsA;
+ // round(num/den)(num,den>0・0.5は繰り上げ)
+ function roundHalfUpDiv(num, den) {
+   return (2n * num + den) / (2n * den);
+ }

「◯円安い」の差も、丸める前の単価の差ではなく、丸めた単価どうしの差にしました。判定と差の表示が食い違わないようにするためです。

確かめたこと

  • 本番が配信している calc.js(リポジトリと同じ内容、コミット 76c8844f)で、無作為な100,000通りがすべて正解と一致(判定「どちらも同じ」630件を含む。2026-10-11)
  • 本番のページをブラウザ(ヘッドレス Chrome)で開き、97円・80g と 1,213円・1,000g で「どちらも同じ」と表示されることを確認
  • シルギア側のテスト14件がすべて通過

← 直した不具合の一覧へ