水道料金(上水道・下水道・消費税)

使用量と、上水道・下水道の基本料金・従量単価から、水道料金(税込・税抜)を出す。

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

言語ごとの結果

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

PHPPHP 8.4.26

よくある書き方9 / 13

直した書き方13 / 13

PythonPython 3.14.7

よくある書き方5 / 13

直した書き方13 / 13

GoGo 1.27.2

よくある書き方9 / 13

直した書き方13 / 13

JavaScriptNode 24.15.0

よくある書き方9 / 13

直した書き方13 / 13

なぜ割れたのか

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

2進小数の誤差

単価に小数があると、使用量を掛けただけで1円少なくなった

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

使用量 15m³、従量単価 16.9円/m³ の上水道料金は、15 × 16.9 = 253.5円を四捨五入して 254円 です。4言語とも 253円 を返しました。1.4m³ × 22.5円(c006)、15m³ × 32.3円(c007)、12.5m³ × 35.8円(c008)も、同じように1円少なくなりました。

なぜ割れたのか

16.9 や 32.3 は、2進数の小数では正確に表せません。掛け算の結果はわずかに小さくなります。

計算正確な値2進数の小数での結果
15 × 16.9253.5253.49999999999997
1.4 × 22.531.531.499999999999996
12.5 × 35.8447.5447.49999999999994

0.5 ちょうどのはずが 0.5 に届かないので、どの言語の四捨五入でも切り捨てられます。4言語とも同じ規格の小数を使うので、そろって同じ答えになりました。

水道局の従量単価には、小数を含むものもあります。単価と使用量を掛けてから丸める計算は、この形の誤差をそのまま受けます。

どう直したか

単価と使用量を十進の数のまま掛けました。PHP は BcMath\Number、Python は decimal、Go は math/big.Rat、JavaScript は BigInt を使った分数です。15 × 16.9 は正確に 253.5 になり、四捨五入で 254 になります。

0.5 の丸め方

Python だけ、0.5 ちょうどの料金や消費税を1円少なくした

対象のケースc005c009c010c013 落ちた言語Python

5m³ × 22.1円 = 110.5円(c005)の四捨五入は 111円 です。PHP・Go・JavaScript は 111円を返しましたが、Python だけ 110円 を返しました。消費税でも同じことが起きました。小計 45円の10%は 4.5円(c010)で、正解は 5円 ですが、Python は 4円 でした。

なぜ割れたのか

110.5 や 4.5 は、2進数の小数でも正確に表せます(誤差はありません)。違いは丸め方です。Python 3 の round() は、0.5 ちょうどを偶数の方へ丸めます(110.5 → 110、4.5 → 4)。ほかの3言語の round は、0 から遠い方へ丸めます。

上水道 2,412.5円・下水道 1,282.5円(c013)では、両方とも1円ずつ少なくなり、小計・消費税・合計がまとめてずれました。

どう直したか

decimal の ROUND_HALF_UP を指定して、0.5 ちょうどを繰り上げるようにしました。

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

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

ケース入力正解 PHPPythonGoJavaScript
c001基本・元ツールの初期値(税込)
usage
20
water_base
1200
water_unit
170
sewer_base
900
sewer_unit
150
tax
incl
water
4600
sewer
3900
subtotal
8500
tax
850
total
9350
✓✓✓✓
c002税抜
usage
20
water_base
1200
water_unit
170
sewer_base
900
sewer_unit
150
tax
excl
water
4600
sewer
3900
subtotal
8500
tax
0
total
8500
✓✓✓✓
c003使用量0 基本料金だけ
usage
0
water_base
1200
water_unit
170
sewer_base
900
sewer_unit
150
tax
incl
water
1200
sewer
900
subtotal
2100
tax
210
total
2310
✓✓✓✓
c004小数の単価で .5 ちょうど(浮動小数点で下回る) 15 × 16.9 = 253.5 → 254。浮動小数点では 253.49999999999997 になり 253 なぜ? 解説を読む
usage
15
water_base
0
water_unit
16.9
sewer_base
0
sewer_unit
0
tax
incl
water
254
sewer
0
subtotal
254
tax
25
total
279
water253subtotal253total278water253subtotal253total278water253subtotal253total278water253subtotal253total278
c005小数の単価で .5 ちょうど(偶数丸めで割れる) 5 × 22.1 = 110.5 → 111。偶数丸めだと 110 なぜ? 解説を読む
usage
5
water_base
0
water_unit
22.1
sewer_base
0
sewer_unit
0
tax
incl
water
111
sewer
0
subtotal
111
tax
11
total
122
✓water110subtotal110total121✓✓
c006小数の使用量 1.4 × 22.5 = 31.5 → 32。浮動小数点では 31.499… になり 31 なぜ? 解説を読む
usage
1.4
water_base
0
water_unit
22.5
sewer_base
0
sewer_unit
0
tax
incl
water
32
sewer
0
subtotal
32
tax
3
total
35
water31subtotal31total34water31subtotal31total34water31subtotal31total34water31subtotal31total34
c007上水道の単価で .5 ちょうど(浮動小数点で下回る・その2) 15 × 32.3 = 484.5 → 485。浮動小数点では 484.49999999999994 なぜ? 解説を読む
usage
15
water_base
0
water_unit
32.3
sewer_base
0
sewer_unit
0
tax
incl
water
485
sewer
0
subtotal
485
tax
49
total
534
water484subtotal484tax48total532water484subtotal484tax48total532water484subtotal484tax48total532water484subtotal484tax48total532
c008下水道だけ小数の使用量で .5 ちょうど 12.5 × 35.8 = 447.5 → 448。浮動小数点では 447.49999999999994 なぜ? 解説を読む
usage
12.5
water_base
0
water_unit
0
sewer_base
0
sewer_unit
35.8
tax
incl
water
0
sewer
448
subtotal
448
tax
45
total
493
sewer447subtotal447total492sewer447subtotal447total492sewer447subtotal447total492sewer447subtotal447total492
c009消費税が .5 ちょうど 1005 × 10% = 100.5 → 101 なぜ? 解説を読む
usage
0
water_base
1005
water_unit
0
sewer_base
0
sewer_unit
0
tax
incl
water
1005
sewer
0
subtotal
1005
tax
101
total
1106
✓tax100total1105✓✓
c010消費税が .5 ちょうど(偶数丸めで割れる) 45 × 10% = 4.5 → 5。偶数丸めだと 4 なぜ? 解説を読む
usage
0
water_base
45
water_unit
0
sewer_base
0
sewer_unit
0
tax
incl
water
45
sewer
0
subtotal
45
tax
5
total
50
✓tax4total49✓✓
c011大口の使用量
usage
1000
water_base
12000
water_unit
390.5
sewer_base
9000
sewer_unit
280.3
tax
incl
water
402500
sewer
289300
subtotal
691800
tax
69180
total
760980
✓✓✓✓
c012すべて0 境界値
usage
0
water_base
0
water_unit
0
sewer_base
0
sewer_unit
0
tax
incl
water
0
sewer
0
subtotal
0
tax
0
total
0
✓✓✓✓
c013実在しそうな単価(小数1桁) 上水道 1430 + 25 × 39.3 = 2412.5 → 2413。下水道 860 + 25 × 16.9 = 1282.5 → 1283 なぜ? 解説を読む
usage
25
water_base
1430
water_unit
39.3
sewer_base
860
sewer_unit
16.9
tax
incl
water
2413
sewer
1283
subtotal
3696
tax
370
total
4066
✓water2412sewer1282subtotal3694tax369total4063✓✓

コード

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

Water.php+13 −5よくある書き方(f2cbb97)→ 直した書き方

@@ 変更前 1行目〜 / 変更後 1行目〜 @@
11 <?php
22 declare(strict_types=1);
33
4+use BcMath\Number;
5+
46 /**
57 * 水道料金。
6− * v1: よくある書き方(float と round)。
8+ * v2: 十進(BcMath\Number)で計算し、四捨五入は RoundingMode::HalfAwayFromZero で明示する
9+ * (float だと 15 × 16.9 が 253.49999999999997 になる)。
710 * @return array{water:int, sewer:int, subtotal:int, tax:int, total:int}
811 */
12+function yen(Number $x): int
13+{
14+ return (int) $x->round(0, RoundingMode::HalfAwayFromZero)->value;
15+}
16+
917 function calcBill(string $usage, string $waterBase, string $waterUnit, string $sewerBase, string $sewerUnit, string $tax): array
1018 {
11− $u = (float) $usage;
12− $water = (int) round((float) $waterBase + $u * (float) $waterUnit);
13− $sewer = (int) round((float) $sewerBase + $u * (float) $sewerUnit);
19+ $u = new Number($usage);
20+ $water = yen(new Number($waterBase) + $u * new Number($waterUnit));
21+ $sewer = yen(new Number($sewerBase) + $u * new Number($sewerUnit));
1422 $subtotal = $water + $sewer;
15− $t = $tax === 'incl' ? (int) round($subtotal * 0.1) : 0;
23+ $t = $tax === 'incl' ? yen(new Number($subtotal) * 10 / 100) : 0;
1624 return ['water' => $water, 'sewer' => $sewer, 'subtotal' => $subtotal, 'tax' => $t, 'total' => $subtotal + $t];
1725 }

仕様

入力

項目型説明
usage十進文字列使用量(m³)。0 以上。"12.5" のような小数を含む
water_base十進文字列上水道の基本料金(円)
water_unit十進文字列上水道の従量単価(円/m³)。"16.9" のような小数を含む
sewer_base十進文字列下水道の基本料金(円)
sewer_unit十進文字列下水道の従量単価(円/m³)
tax"incl" | "excl"消費税(10%)を含めるか

出力(すべて整数・円)

項目定義
water四捨五入(water_base + usage × water_unit)
sewer四捨五入(sewer_base + usage × sewer_unit)
subtotalwater + sewer
taxtax = "incl" なら 四捨五入(subtotal × 10 / 100)、"excl" なら 0
totalsubtotal + tax
  • 計算は正確な十進(または有理数)で行う。 2進浮動小数点の誤差を結果に出さない
  • 四捨五入は 0.5 ちょうどを繰り上げる(偶数丸めではない)。値はすべて 0 以上

元ツールとの違い(2026-10-10 に決めた)

元ツールは、上水道・下水道・消費税・合計を、丸める前の値から別々に計算し、表示のときにそれぞれ四捨五入している。そのため、表示された内訳を足しても合計と合わないことがある。本 spec は、丸めた上水道と下水道から小計を作り、その小計に税をかけることで、内訳の合計が必ず一致するようにした。元ツールの「1か月/2か月」の切り替えは表示の文言だけで計算に影響しないため、入力に含めない。

期待値の作り方

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