単価の比較(どちらがお得か)

2つの商品の価格と内容量から、100gあたり(または1個あたり)の単価を出し、どちらがお得かを判定する。

2進小数の誤差0.5 の丸め方

言語ごとの結果

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

PHPPHP 8.4.26

よくある書き方9 / 11

直した書き方11 / 11

PythonPython 3.14.7

よくある書き方8 / 11

直した書き方11 / 11

GoGo 1.27.2

よくある書き方9 / 11

直した書き方11 / 11

JavaScriptNode 24.15.0

よくある書き方9 / 11

直した書き方11 / 11

なぜ割れたのか

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

2進小数の誤差

本当は同じ単価なのに、「A がお得」と判定した

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

商品Aは 97円・80g、商品Bは 1,213円・1,000g です。100gあたりの単価は、A が 97 ÷ 80 × 100 = 121.25円、B が 121.3円 です。単価は小数第1位に四捨五入して表示し、表示した値で比べるので、A も 121.3円になり、判定は どちらも同じ です。

4言語とも、A を 121.2円 と表示し、「A がお得」 と判定しました。

なぜ割れたのか

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

Python はさらに、2進数でも正確に表せる 81.25(13円・16g、c006)でも 81.2 にしました。round() が 0.5 ちょうどを偶数の方へ丸めるためです。

境目の値は、ふつうの商品で出る

80g・16g のような内容量は、菓子や調味料でよくある大きさです。価格 ÷ 内容量が 0.05円の境目にぴったり乗る組み合わせは珍しくありません。「どちらがお得か」を表示の桁で判定するときは、その手前の単価を正確に出しておく必要があります。

どう直したか

価格・内容量を分数のまま割り、0.1円単位の四捨五入も整数で行いました。PHP は BcMath\Number、Python は fractions.Fraction、Go は math/big.Rat、JavaScript は BigInt を使っています。

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

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

ケース入力正解 PHPPythonGoJavaScript
c001同じ単価 どちらも 100gあたり 132円
unit
g
a_price
198
a_qty
150
b_price
264
b_qty
200
unit_a
132.0
unit_b
132.0
verdict
same
diff
0.0
✓✓✓✓
c002A がお得
unit
g
a_price
300
a_qty
500
b_price
200
b_qty
300
unit_a
60.0
unit_b
66.7
verdict
A
diff
6.7
✓✓✓✓
c0031個あたり・B がお得
unit
個
a_price
498
a_qty
6
b_price
328
b_qty
4
unit_a
83.0
unit_b
82.0
verdict
B
diff
1.0
✓✓✓✓
c004表示の境目(浮動小数点で下回る) 97 ÷ 80 × 100 = 121.25 → 121.3 で、B と同じ。浮動小数点では 121.24999… になり 121.2(A がお得と判定してしまう) なぜ? 解説を読む
unit
g
a_price
97
a_qty
80
b_price
1213
b_qty
1000
unit_a
121.3
unit_b
121.3
verdict
same
diff
0.0
unit_a"121.2"verdict"A"diff"0.1"unit_a"121.2"verdict"A"diff"0.1"unit_a"121.2"verdict"A"diff"0.1"unit_a"121.2"verdict"A"diff"0.1"
c005表示の境目(浮動小数点で下回る・その2) 169 ÷ 80 × 100 = 211.25 → 211.3。浮動小数点では 211.24999… なぜ? 解説を読む
unit
g
a_price
169
a_qty
80
b_price
2113
b_qty
1000
unit_a
211.3
unit_b
211.3
verdict
same
diff
0.0
unit_a"211.2"verdict"A"diff"0.1"unit_a"211.2"verdict"A"diff"0.1"unit_a"211.2"verdict"A"diff"0.1"unit_a"211.2"verdict"A"diff"0.1"
c006表示の境目(偶数丸めで割れる) 13 ÷ 16 × 100 = 81.25 → 81.3。2進でも正確だが、偶数丸めだと 81.2 なぜ? 解説を読む
unit
g
a_price
13
a_qty
16
b_price
813
b_qty
1000
unit_a
81.3
unit_b
81.3
verdict
same
diff
0.0
✓unit_a"81.2"verdict"A"diff"0.1"✓✓
c007ml・B がお得
unit
ml
a_price
158
a_qty
500
b_price
248
b_qty
800
unit_a
31.6
unit_b
31.0
verdict
B
diff
0.6
✓✓✓✓
c008価格0円 境界値
unit
g
a_price
0
a_qty
100
b_price
100
b_qty
100
unit_a
0.0
unit_b
100.0
verdict
A
diff
100.0
✓✓✓✓
c009小数の内容量 398 ÷ 1.5 = 265.333… → 265.3
unit
個
a_price
398
a_qty
1.5
b_price
530
b_qty
2
unit_a
265.3
unit_b
265.0
verdict
B
diff
0.3
✓✓✓✓
c010差が 0.1 33.333… → 33.3 と 33.222… → 33.2
unit
g
a_price
100
a_qty
300
b_price
100
b_qty
301
unit_a
33.3
unit_b
33.2
verdict
B
diff
0.1
✓✓✓✓
c011割り切れない同じ単価 どちらも 3333.333… → 3333.3
unit
g
a_price
100
a_qty
3
b_price
200
b_qty
6
unit_a
3333.3
unit_b
3333.3
verdict
same
diff
0.0
✓✓✓✓

コード

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

UnitPrice.php+20 −6よくある書き方(9faa1ab)→ 直した書き方

@@ 変更前 1行目〜 / 変更後 1行目〜 @@
11 <?php
22 declare(strict_types=1);
33
4+use BcMath\Number;
5+
46 /**
57 * 単価の比較(どちらがお得か)。
6− * v1: よくある書き方(float で単価を出し、×10 して round で 0.1 単位にそろえて比べる)。
7− * @return array{unit_a:string, unit_b:string, verdict:string, diff:string}
8+ * v2: 十進(BcMath\Number)で単価を出し、小数第1位への四捨五入は RoundingMode::HalfAwayFromZero で明示する。
9+ * float だと 97 ÷ 80 × 100 が 121.24999… になり、本当は同じ単価を「A がお得」と判定してしまう。
10+ * 割り算は有限桁で打ち切るので、四捨五入の判定に影響しない桁数(20桁)まで出す。
811 */
12+function tenths(string $price, string $qty, int $base): int
13+{
14+ // price × base × 10 ÷ qty を整数に四捨五入 = 単価を 0.1 単位にそろえた値
15+ return (int) (new Number($price) * $base * 10)->div($qty, 20)->round(0, RoundingMode::HalfAwayFromZero)->value;
16+}
17+
18+function fmt1(int $t): string
19+{
20+ return intdiv($t, 10) . '.' . $t % 10;
21+}
22+
23+/** @return array{unit_a:string, unit_b:string, verdict:string, diff:string} */
924 function compareUnitPrice(string $unit, string $aPrice, string $aQty, string $bPrice, string $bQty): array
1025 {
1126 $base = $unit === '個' ? 1 : 100;
12− $ta = (int) round((float) $aPrice / (float) $aQty * $base * 10);
13− $tb = (int) round((float) $bPrice / (float) $bQty * $base * 10);
27+ $ta = tenths($aPrice, $aQty, $base);
28+ $tb = tenths($bPrice, $bQty, $base);
1429 $verdict = $ta === $tb ? 'same' : ($ta < $tb ? 'A' : 'B');
15− $f = fn (int $t): string => number_format($t / 10, 1, '.', '');
16− return ['unit_a' => $f($ta), 'unit_b' => $f($tb), 'verdict' => $verdict, 'diff' => $f(abs($ta - $tb))];
30+ return ['unit_a' => fmt1($ta), 'unit_b' => fmt1($tb), 'verdict' => $verdict, 'diff' => fmt1(abs($ta - $tb))];
1731 }

仕様

入力

項目型説明
unit"g" | "ml" | "個"内容量の単位
a_price・b_price十進文字列商品A・Bの価格(円)。0 以上
a_qty・b_qty十進文字列商品A・Bの内容量。0 より大きい。"1.5" のような小数を含む

出力

項目型定義
unit_a・unit_b十進文字列(小数1桁)単価 = 価格 ÷ 内容量 × 基準量 を小数第1位に四捨五入。基準量は g・ml なら 100、個なら 1
verdict"A" | "B" | "same"丸めた単価が小さい方。丸めた単価が等しければ "same"
diff十進文字列(小数1桁)丸めた単価どうしの差(0 以上)
  • 計算は正確な十進(または有理数)で行う。 2進浮動小数点の誤差を結果に出さない
  • 四捨五入は 0.05 ちょうどを繰り上げる(偶数丸めではない)
  • 出力の小数は常に1桁("132.0")

元ツールとの違い

元ツールは「どちらがお得か」を丸めた単価で判定するが、「◯円安い」の差は丸める前の単価どうしの差を丸めて表示する。本 spec は差も丸めた単価どうしで求めた(判定と差の表示が食い違わないように)。

期待値の作り方

expect は oracle.py で、有理数による定義の直訳から生成した(2026-10-10)。実装ではない。runner は使わない。