洗濯洗剤・柔軟剤の量

水量か洗濯物の重さと、パッケージの基準(水◯Lあたり◯mL)から、洗剤と柔軟剤の量を小数第1位まで出す。

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

言語ごとの結果

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

PHPPHP 8.4.26

よくある書き方12 / 14

直した書き方14 / 14

PythonPython 3.14.7

よくある書き方9 / 14

直した書き方14 / 14

GoGo 1.27.2

よくある書き方12 / 14

直した書き方14 / 14

JavaScriptNode 24.15.0

よくある書き方12 / 14

直した書き方14 / 14

JavaJava 21.0.12

よくある書き方12 / 14

直した書き方14 / 14

C#.NET 10.0.402

よくある書き方9 / 14

直した書き方14 / 14

RustRust 1.99.0

よくある書き方12 / 14

直した書き方14 / 14

RubyRuby 4.0.7

よくある書き方12 / 14

直した書き方14 / 14

KotlinKotlin 2.4.21

よくある書き方12 / 14

直した書き方14 / 14

SwiftSwift 6.3.3

よくある書き方12 / 14

直した書き方14 / 14

なぜ割れたのか

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

2進小数の誤差

水13.2L・基準40Lあたり25gで、10言語すべてが 8.3g ではなく 8.2g にした

対象のケースc007c008 落ちた言語PHPPythonGoJavaScriptJavaC#RustRubyKotlinSwift

水 13.2L、洗剤の基準が「水40Lあたり25g」のとき(c007)、洗剤の量は 13.2 ÷ 40 × 25 = 8.25g、小数第1位に四捨五入して 8.3g が正解です。10言語すべてが 8.2g を返しました。水 11.6L(7.25g → 正解 7.3g、c008)も、10言語すべてが 7.2g でした。

なぜずれたのか

13.2 も 11.6 も、2進数の小数では正確に表せません。13.2 ÷ 40 × 25 を2進数の小数で計算すると 8.2499999999999982、11.6 ÷ 40 × 25 は 7.2499999999999991 になります。どちらも 0.05 の境目のわずかに下なので、どの言語の丸め方でも小さい方に切り下がります。

同じ基準でも、水 2L(1.25g、c005)や 10L(6.25g、c006)は2進数の小数でも正確に計算できるので、このずれは起きません(そちらは別の理由で Python と C# が外しています。解説2)。

どう直したか

水量と基準を、2進数の小数に直さずに計算しました。水量 × 基準の量 ÷ 基準の水量 を分数のまま持ち、余りが半分以上なら繰り上げます。

  • Python: fractions.Fraction、Go: big.Rat、Ruby: Rational(正確な分数)
  • Java・Kotlin: BigDecimal と RoundingMode.HALF_UP
  • C#: decimal で、0.1 単位の商と余りを比べる
  • JavaScript: BigInt で分子と分母、PHP・Rust・Swift: 整数の分数

0.5 の丸め方

水2L・基準40Lあたり25gの 1.25g を、Python と C# は 1.2g にした

対象のケースc005c006c011 落ちた言語PythonC#

水 2L、洗剤の基準が「水40Lあたり25g」のとき(c005)、洗剤の量は 2 ÷ 40 × 25 = 1.25g、小数第1位に四捨五入して 1.3g が正解です。1.25 は2進数の小数でも正確に表せるので、誤差はありません。それでも Python と C# は 1.2g を返しました。ほかの8言語は 1.3g でした。

水 10L(6.25g → 正解 6.3g、c006)でも、柔軟剤の基準を「水40Lあたり25mL」にした水 2L(1.25mL → 1.3mL、c011)でも、同じ2言語が 6.2g・1.2mL を返しました。

なぜ割れたのか

Python の round(x, 1) と C# の Math.Round(x, 1) は、0.5 ちょうどを偶数の方へ丸めます(1.25 → 1.2、6.25 → 6.2)。ほかの言語の丸め方(10倍して round、Ruby の round(1) など)は 0 から遠い方へ丸めるので、1.3・6.3 でした。

どう直したか

解説1 と同じく分数のまま計算し、余りが半分以上なら繰り上げました。C# は decimal の割り算で商が28桁に丸められるのを避けるため、0.1 単位の商と余りを整数で比べています。

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

正解は実装より先に決めて固定しています。右の欄は、よくある書き方の答えを言語ごとにまとめたものです(赤い値は実際に返した間違った答え)。

ケース入力正解よくある書き方の答え(10言語)
c001基本・水45L 45 ÷ 30 × 10 = 15
basis
water
value
45
ref_water
30
ref_amount
10
water_l
45
detergent
15
softener
null

✓10言語すべて正解

c002割り切れない・水50L 16.66… → 16.7
basis
water
value
50
ref_water
30
ref_amount
10
water_l
50
detergent
16.7
softener
null

✓10言語すべて正解

c003重さから・縦型4.5kg 4.5 × 10 = 45L
basis
weight
value
4.5
machine
top
ref_water
30
ref_amount
10
water_l
45
detergent
15
softener
null

✓10言語すべて正解

c004重さから・ドラム式5kg 5 × 6 = 30L
basis
weight
value
5
machine
drum
ref_water
30
ref_amount
10
water_l
30
detergent
10
softener
null

✓10言語すべて正解

c0050.05 ちょうど(2進数でも正確)・水2L 2 ÷ 40 × 25 = 1.25 → 1.3。偶数丸めだと 1.2 なぜ? 解説を読む
basis
water
value
2
ref_water
40
ref_amount
25
water_l
2
detergent
1.3
softener
null
  • Python・C#detergent"1.2"
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c0060.05 ちょうど(2進数でも正確)・水10L 6.25 → 6.3。偶数丸めだと 6.2 なぜ? 解説を読む
basis
water
value
10
ref_water
40
ref_amount
25
water_l
10
detergent
6.3
softener
null
  • Python・C#detergent"6.2"
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c0070.05 の境目・2進数で小さく入る 8.25 → 8.3。2進数の小数で計算すると 8.2499999999999982 になる なぜ? 解説を読む
basis
water
value
13.2
ref_water
40
ref_amount
25
water_l
13.2
detergent
8.3
softener
null
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftdetergent"8.2"
c0080.05 の境目・2進数で小さく入る(2) 7.25 → 7.3。2進数の小数で計算すると 7.2499999999999991 になる なぜ? 解説を読む
basis
water
value
11.6
ref_water
40
ref_amount
25
water_l
11.6
detergent
7.3
softener
null
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftdetergent"7.2"
c009重さから・水量に小数 4.4 × 6 = 26.4L、26.4 ÷ 30 × 10 = 8.8
basis
weight
value
4.4
machine
drum
ref_water
30
ref_amount
10
water_l
26.4
detergent
8.8
softener
null

✓10言語すべて正解

c010柔軟剤も 柔軟剤も 15mL
basis
water
value
45
ref_water
30
ref_amount
10
soft_water
30
soft_amount
10
water_l
45
detergent
15
softener
15

✓10言語すべて正解

c011柔軟剤の境目 柔軟剤 1.25 → 1.3 なぜ? 解説を読む
basis
water
value
2
ref_water
30
ref_amount
10
soft_water
40
soft_amount
25
water_l
2
detergent
0.7
softener
1.3
  • Python・C#softener"1.2"
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c012基準の水量が0なら量は出さない detergent は null
basis
water
value
45
ref_water
0
ref_amount
10
water_l
45
detergent
null
softener
null

✓10言語すべて正解

c013水0Lはエラー
basis
water
value
0
ref_water
30
ref_amount
10
error
invalid_input

✓10言語すべて正解

c014数でない入力はエラー
basis
weight
value
abc
machine
top
ref_water
30
ref_amount
10
error
invalid_input

✓10言語すべて正解

コード

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

Laundry.php+38 −15よくある書き方(101f71e)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * 洗濯洗剤・柔軟剤の量。
6− * v1: 元ツールと同じく値を float に読み、水量 ÷ 基準の水量 × 基準の量 を float のまま計算して、round($x, 1) で丸める素直な書き方。
7− * 小数第1位が 0 なら整数で書く。
6+ * v2: 値を float に読まず、10^桁数 倍した整数(分子)と 10^桁数(分母)の組として10進のまま読む。
7+ * 水量 ÷ 基準の水量 × 基準の量 を分子・分母の整数の掛け算で正確な比にして、小数第1位に「0.5 ちょうどは大きい方へ」四捨五入する。
8+ * float だと 13.2 ÷ 40 × 25 が 8.2499999999999982 になり、v1 の round($x, 1) は 8.2 を返した(PHP 8.4.4)。
9+ * 整数が PHP_INT_MAX を超えると float になるが、strict_types なので int の引数に渡したところで TypeError になり、黙って誤差の入った値は返さない。
810 */
11+const DECIMAL = '/\A[0-9]+(\.[0-9]+)?\z/';
912 const WATER_PER_KG = ['top' => 10, 'drum' => 6];
1013
11−function fmt1(float $x): string
14+/** "2.5" → [25, 10](値 = 分子 ÷ 分母)。読めなければ null */
15+function readValue(mixed $s): ?array
1216 {
13− $r = round($x, 1);
14− return $r == floor($r) ? (string) (int) $r : (string) $r;
17+ if (!is_string($s) || !preg_match(DECIMAL, $s)) {
18+ return null;
19+ }
20+ [$int, $frac] = array_pad(explode('.', $s), 2, '');
21+ $n = ltrim($int . $frac, '0');
22+ if (strlen($n) > 18 || strlen($frac) > 18) {
23+ throw new RangeException('too many digits'); // int(64ビット)に入りきらない
24+ }
25+ return [(int) $n, 10 ** strlen($frac)];
1526 }
1627
17−function amount(float $water, mixed $refWater, mixed $refAmount): ?string
28+/** 分子 ÷ 分母(どちらも 0 以上・分母は正)を小数第1位に四捨五入して "8.3"・"15" の形にする */
29+function fmt1(int $num, int $den): string
1830 {
19− if (!is_numeric($refWater) || !is_numeric($refAmount) || (float) $refWater <= 0) {
31+ $tenths = intdiv(2 * 10 * $num + $den, 2 * $den); // floor(num × 10 ÷ den + 1/2)
32+ return $tenths % 10 === 0 ? (string) intdiv($tenths, 10) : intdiv($tenths, 10) . '.' . $tenths % 10;
33+}
34+
35+/** @param array{int, int} $water */
36+function amount(array $water, mixed $refWater, mixed $refAmount): ?string
37+{
38+ $rw = readValue($refWater);
39+ $ra = readValue($refAmount);
40+ if ($rw === null || $ra === null || $rw[0] === 0) {
2041 return null; // 基準が読めない・0 以下 → 量は出さない
2142 }
22− return fmt1($water / (float) $refWater * (float) $refAmount);
43+ // (w.n / w.d) ÷ (rw.n / rw.d) × (ra.n / ra.d)
44+ return fmt1($water[0] * $rw[1] * $ra[0], $water[1] * $rw[0] * $ra[1]);
2345 }
2446
2547 /** @param array{basis: string, value: string, ref_water: string, ref_amount: string, machine?: string, soft_water?: string, soft_amount?: string} $in */
2648 function laundry(array $in): array
2749 {
2850 $machine = $in['machine'] ?? 'top';
29− if (!is_numeric($in['value']) || !in_array($in['basis'], ['water', 'weight'], true) || !isset(WATER_PER_KG[$machine])) {
51+ $v = readValue($in['value'] ?? null);
52+ if ($v === null || $v[0] === 0 || !in_array($in['basis'] ?? null, ['water', 'weight'], true) || !isset(WATER_PER_KG[$machine])) {
3053 return ['error' => 'invalid_input'];
3154 }
32− $v = (float) $in['value'];
33− if ($v <= 0) {
34− return ['error' => 'invalid_input'];
35− }
36− $water = $in['basis'] === 'water' ? $v : $v * WATER_PER_KG[$machine];
55+ $water = $in['basis'] === 'water' ? $v : [$v[0] * WATER_PER_KG[$machine], $v[1]];
3756 $softener = isset($in['soft_water']) ? amount($water, $in['soft_water'], $in['soft_amount'] ?? null) : null;
38− return ['water_l' => fmt1($water), 'detergent' => amount($water, $in['ref_water'], $in['ref_amount']), 'softener' => $softener];
57+ return [
58+ 'water_l' => fmt1($water[0], $water[1]),
59+ 'detergent' => amount($water, $in['ref_water'] ?? null, $in['ref_amount'] ?? null),
60+ 'softener' => $softener,
61+ ];
3962 }

仕様

入力

項目型説明
basis文字列water(水量から)・weight(洗濯物の重さから)
value文字列水量(L)か重さ(kg)。10進の数
machine文字列basis が weight のとき使う。top(縦型・1kg あたり水 10L)・drum(ドラム式・6L)。省略すると top
ref_water文字列洗剤の基準の水量(L)。パッケージの「水◯Lあたり」
ref_amount文字列洗剤の基準の量(mL か g)。パッケージの「◯mL」
soft_water・soft_amount文字列柔軟剤の基準(任意。省略すると柔軟剤は出さない)

出力

成功したとき: {"water_l": "45", "detergent": "15", "softener": null}

項目型定義
water_l文字列水量(water は入力、weight は 重さ × 10 か × 6)を小数第1位に四捨五入
detergent文字列か null水量 ÷ 基準の水量 × 基準の量 を小数第1位に四捨五入。基準の水量か量が10進の数として読めない・基準の水量が 0 以下なら null
softener文字列か null柔軟剤の基準があれば同じ計算。無ければ null
  • 小数第1位が 0 なら整数で書く("15"・"16.7")
  • 四捨五入は、丸める前の正確な値に対して行う。値は 0 以上なので「0.5 ちょうどは大きい方へ」(1.25 → 1.3、8.25 → 8.3)

判定できないとき: {"error": "invalid_input"}。value が 10進の数として読めない(空・文字・マイナス)、または 0 のとき。

元ツールとの違い

  • 元ツールは基準の水量が 0 以下のとき「基準を確認」と表示する。本 spec は detergent を null にする
  • 元ツールは入力を parseFloat で読む。本 spec は 10進の数の文字列だけを受け付ける

期待値の作り方

expect は oracle.py で、上の定義を Python の fractions.Fraction(正確な分数)で計算して生成した(2026-10-24)。実装ではない。runner は使わない。