米の量と炊き上がり・人数

米の合数・生米のグラム・茶碗の杯数から、炊き上がりの量(5g 単位)・茶碗とおにぎりの数・必要な合数(0.5合単位)を出す。

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

言語ごとの結果

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

PHPPHP 8.4.26

よくある書き方11 / 16

直した書き方16 / 16

PythonPython 3.14.7

よくある書き方7 / 16

直した書き方16 / 16

GoGo 1.27.2

よくある書き方11 / 16

直した書き方16 / 16

JavaScriptNode 24.15.0

よくある書き方11 / 16

直した書き方16 / 16

JavaJava 21.0.12

よくある書き方11 / 16

直した書き方16 / 16

C#.NET 10.0.402

よくある書き方7 / 16

直した書き方16 / 16

RustRust 1.99.0

よくある書き方11 / 16

直した書き方16 / 16

RubyRuby 4.0.7

よくある書き方11 / 16

直した書き方16 / 16

KotlinKotlin 2.4.21

よくある書き方11 / 16

直した書き方16 / 16

SwiftSwift 6.3.3

よくある書き方11 / 16

直した書き方16 / 16

なぜ割れたのか

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

2進小数の誤差

2.5合の炊き上がりを、10言語すべてが 865g ではなく 860g にした

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

2.5合(生米 375g)を炊いたときの上限の目安は、375 × 2.3 = 862.5g、5g 単位に四捨五入して 865g が正解です(c002)。10言語すべてが 860g を返しました。

同じことが、4.5合(1,552.5g → 正解 1,555g、c003)・5.5合(1,897.5g → 1,900g、c004)・生米 175g(402.5g → 405g、c012)・生米 25g(57.5g → 60g、c013)でも起きました。どれも 10言語すべてが 5g 少ない値を返しています。公開中の元ツールも同じで、0.1〜10合(0.1合刻み)の100通り中4通り、生米 1〜3,000g の3,000通り中29通りで、上限が 5g 少なくなります。

なぜずれたのか

2.3 は2進数の小数では正確に表せません。375 × 2.3 は 862.5 ではなく 862.49999999999988… になります。5 で割ると 172.49999…、四捨五入して 172、5 を掛けて 860g です。

生米正確な値2進数の小数で計算すると5g 単位
375g(2.5合)862.5862.49999999999988…860(正しくは 865)
675g(4.5合)1,552.51,552.4999999999997…1,550(1,555)
825g(5.5合)1,897.51,897.4999999999997…1,895(1,900)
175g402.5402.49999999999994…400(405)
25g57.557.499999999999992…55(60)

丸め方(0.5 ちょうどを大きい方へ寄せるか、偶数へ寄せるか)とは関係がありません。丸める前の値がすでに 0.5 に届いていないので、どの言語の丸め方でも小さい方に切り下がります。

下限(× 2.1)は、この正解表の入力では2進数の誤差で外れたケースはありませんでした。825 × 2.1 = 1,732.5 のように、2進数でも正確に計算できる組み合わせだったためです(ただし Python と C# は、0.5 ちょうどの丸め方で外しています。解説2)。

どう直したか

生米のグラム数と倍率(2.1・2.2・2.3)を、2進数の小数に直さずに計算しました。グラム数 × 23 ÷ 10 ÷ 5 のように整数の分数にしてから、余りが半分以上なら繰り上げます。

  • Python: fractions.Fraction、Go: big.Rat、Ruby: Rational(正確な分数)
  • Java・Kotlin: BigDecimal と RoundingMode.HALF_UP、C#: decimal と MidpointRounding.AwayFromZero
  • JavaScript: BigInt で分子と分母、PHP・Rust・Swift: 整数の分数

0.5 の丸め方

0.5合の炊き上がりを、Python と C# は 175g ではなく 170g にした

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

0.5合(生米 75g)を炊いたときの上限は 75 × 2.3 = 172.5g、5g 単位に四捨五入して 175g が正解です(c005)。172.5 は2進数の小数でも正確に表せるので、誤差はありません。それでも Python と C# は 170g を返しました。ほかの8言語は 175g でした。

ケース正確な値正解Python・C#
c005 0.5合の上限172.5g(5 で割って 34.5)175g170g
c006 1.5合の下限472.5g(94.5)475g470g
c008 0.31合の生米46.5g47g46g
c010 茶碗1杯 → 0.5合の上限172.5g(34.5)175g170g

5.5合の下限(825 × 2.1 = 1,732.5g → 1,735g)と生米 25g の下限(52.5g → 55g)でも、2言語は 1,730g・50g を返しました(このケースは 解説1 の上限のずれでも落ちています)。

なぜ割れたのか

Python の round() と C# の Math.Round() は、0.5 ちょうどを偶数の方へ丸めます(34.5 → 34、94.5 → 94、46.5 → 46)。ほかの言語の round は 0.5 ちょうどを大きい方(0 から遠い方)へ丸めるので、35・95・47 になりました。

5g 単位に丸めるときは「5 で割って整数に丸め、5 を掛ける」ので、5 で割った値が x.5 ちょうどになる量(2.5g・7.5g…で終わる量)で、丸め方の違いがそのまま 5g の差になります。

どう直したか

丸め方を名前で指定しました。Python は decimal の ROUND_HALF_UP ではなく、正確な分数(Fraction)で余りを比べて繰り上げています。C# は Math.Round(x, MidpointRounding.AwayFromZero) です。

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

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

ケース入力正解よくある書き方の答え(10言語)
c001基本・1合 生米150g・炊き上がり315〜345g
mode
gou
value
1
gou
1
rice_g
150
ml
180
cooked_low
315
cooked_high
345
bowls
2
onigiri
3

✓10言語すべて正解

c0025g の境目・2.5合 375 × 2.3 = 862.5g → 5g 単位で 865g。2進数の小数だと 862.4999… になり、元ツールは 860g と出す なぜ? 解説を読む
mode
gou
value
2.5
gou
2.5
rice_g
375
ml
450
cooked_low
790
cooked_high
865
bowls
6
onigiri
8
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftcooked_high860
c0035g の境目・4.5合 675 × 2.3 = 1,552.5g → 1,555g(元ツールは 1,550g) なぜ? 解説を読む
mode
gou
value
4.5
gou
4.5
rice_g
675
ml
810
cooked_low
1420
cooked_high
1555
bowls
10
onigiri
14
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftcooked_high1550
c0045g の境目・5.5合 825 × 2.3 = 1,897.5g → 1,900g(元ツールは 1,895g) なぜ? 解説を読む
mode
gou
value
5.5
gou
5.5
rice_g
825
ml
990
cooked_low
1735
cooked_high
1900
bowls
12
onigiri
17
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swiftcooked_high1895
  • Python・C#cooked_low1730cooked_high1895
c0050.5 ちょうど(2進数でも正確)・0.5合 75 × 2.1 = 157.5 → 31.5 × 5 → 160g、75 × 2.3 = 172.5 → 34.5 × 5 → 175g。偶数丸めだと 155g・170g なぜ? 解説を読む
mode
gou
value
0.5
gou
0.5
rice_g
75
ml
90
cooked_low
160
cooked_high
175
bowls
1
onigiri
2
  • Python・C#cooked_high170
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c0060.5 ちょうど(2進数でも正確)・1.5合 225 × 2.1 = 472.5 → 94.5 × 5 → 475g。偶数丸めだと 470g なぜ? 解説を読む
mode
gou
value
1.5
gou
1.5
rice_g
225
ml
270
cooked_low
475
cooked_high
520
bowls
3
onigiri
5
  • Python・C#cooked_low470
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c007整数で書く・3合 合は「3」(3.0 ではない)
mode
gou
value
3
gou
3
rice_g
450
ml
540
cooked_low
945
cooked_high
1035
bowls
7
onigiri
9

✓10言語すべて正解

c008グラムの 0.5 ちょうど・0.31合 0.31 × 150 = 46.5g → 47g。偶数丸めだと 46g なぜ? 解説を読む
mode
gou
value
0.31
gou
0.3
rice_g
47
ml
56
cooked_low
100
cooked_high
105
bowls
1
onigiri
1
  • Python・C#rice_g46
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c009人数から・茶碗4杯 600 ÷ 330 = 1.81…合 → 0.5合単位で 2合
mode
servings
value
4
gou
2
rice_g
300
ml
360
cooked_low
630
cooked_high
690
bowls
4
onigiri
6

✓10言語すべて正解

c010人数から・茶碗1杯 0.45…合 → 0.5合 なぜ? 解説を読む
mode
servings
value
1
gou
0.5
rice_g
75
ml
90
cooked_low
160
cooked_high
175
bowls
1
onigiri
2
  • Python・C#cooked_high170
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c011グラムから・300g 2合
mode
grams
value
300
gou
2
rice_g
300
ml
360
cooked_low
630
cooked_high
690
bowls
4
onigiri
6

✓10言語すべて正解

c012グラムから・5g の境目 175 × 2.3 = 402.5g → 405g(元ツールは 400g) なぜ? 解説を読む
mode
grams
value
175
gou
1.2
rice_g
175
ml
210
cooked_low
370
cooked_high
405
bowls
3
onigiri
4
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftcooked_high400
c013グラムから・合は小数第1位 25 ÷ 150 = 0.166…合 → 0.2合 なぜ? 解説を読む
mode
grams
value
25
gou
0.2
rice_g
25
ml
30
cooked_low
55
cooked_high
60
bowls
0
onigiri
1
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swiftcooked_high55
  • Python・C#cooked_low50cooked_high55
c0140合はエラー
mode
gou
value
0
error
invalid_input

✓10言語すべて正解

c015マイナスはエラー
mode
grams
value
-100
error
invalid_input

✓10言語すべて正解

c016数でない入力はエラー
mode
servings
value
abc
error
invalid_input

✓10言語すべて正解

コード

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

Rice.php+82 −37よくある書き方(8482cf8)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * 米の量と炊き上がり・人数。
6− * v1: 元ツールと同じく値を float に読み、掛け算・割り算を float のまま行って、round() で丸める素直な書き方。
7− * 5g 単位は round(x / 5) * 5、0.5合単位は round(x * 2) / 2、合の表示は round(x * 10) / 10 を整数なら整数で書く。
6+ * v2: 値を float に読まず、10進の数字の列(値 = 数字の列 ÷ 10^小数の桁数)のまま扱い、
7+ * 各項目を「数字の列 × 小さな整数 ÷ (小さな整数 × 10^桁数)」の正確な比として、0.5 ちょうどは大きい方へ四捨五入する。
8+ * float だと 375 × 2.3 が 862.4999… になって 5g 単位で 860g に落ち、Python などの偶数丸めは 157.5 ÷ 5 = 31.5 を 31 にする。
9+ * 桁数に上限を置かないため、途中の掛け算・足し算・割り算は数字の列の筆算で行う(bcmath・gmp は使わない)。
810 */
9−const G_PER_GOU = 150;
10−const ML_PER_GOU = 180;
11−const COOK_LO = 2.1;
12−const COOK_MID = 2.2;
13−const COOK_HI = 2.3;
14−const BOWL_G = 150;
15−const ONIGIRI_G = 105;
11+const DECIMAL = '/^[0-9]+(\.[0-9]+)?$/';
1612
17−function round5(float $x): int
13+/** 数字の列 × 小さな整数(筆算) */
14+function mulSmall(string $a, int $k): string
1815 {
19− return (int) round($x / 5) * 5;
16+ $out = '';
17+ $carry = 0;
18+ for ($i = strlen($a) - 1; $i >= 0; $i--) {
19+ $p = (int) $a[$i] * $k + $carry;
20+ $out = ($p % 10) . $out;
21+ $carry = intdiv($p, 10);
22+ }
23+ return ltrim(($carry ? (string) $carry : '') . $out, '0') ?: '0';
2024 }
2125
22−function gouText(float $g): string
26+/** 数字の列どうしの足し算(筆算) */
27+function addDigits(string $a, string $b): string
2328 {
24− $r = round($g * 10) / 10;
25− return $r == floor($r) ? (string) (int) $r : (string) $r;
29+ $n = max(strlen($a), strlen($b));
30+ $a = str_pad($a, $n, '0', STR_PAD_LEFT);
31+ $b = str_pad($b, $n, '0', STR_PAD_LEFT);
32+ $out = '';
33+ $carry = 0;
34+ for ($i = $n - 1; $i >= 0; $i--) {
35+ $s = (int) $a[$i] + (int) $b[$i] + $carry;
36+ $out = ($s % 10) . $out;
37+ $carry = intdiv($s, 10);
38+ }
39+ return ltrim(($carry ? '1' : '') . $out, '0') ?: '0';
40+}
41+
42+/** 数字の列 ÷ 小さな整数 の切り捨て(筆算) */
43+function divSmall(string $a, int $k): string
44+{
45+ $out = '';
46+ $r = 0;
47+ for ($i = 0, $n = strlen($a); $i < $n; $i++) {
48+ $r = $r * 10 + (int) $a[$i];
49+ $out .= intdiv($r, $k);
50+ $r %= $k;
51+ }
52+ return ltrim($out, '0') ?: '0';
53+}
54+
55+/**
56+ * (digits ÷ 10^scale) × mul ÷ div を整数に四捨五入(0 以上なので 0.5 ちょうどは大きい方へ)。
57+ * = floor((2 × digits × mul + div × 10^scale) ÷ (2 × div × 10^scale))。10^scale で割るのは下の桁を落とすだけ
58+ */
59+function roundRatio(string $digits, int $scale, int $mul, int $div): int
60+{
61+ $a = addDigits(mulSmall($digits, 2 * $mul), $div . str_repeat('0', $scale));
62+ $a = strlen($a) > $scale ? substr($a, 0, strlen($a) - $scale) : '0';
63+ return (int) divSmall($a, 2 * $div);
64+}
65+
66+/** 0.1合単位の整数を "3"・"2.5"・"0.2" の形にする */
67+function gouText(int $tenths): string
68+{
69+ return $tenths % 10 === 0 ? (string) intdiv($tenths, 10) : intdiv($tenths, 10) . '.' . $tenths % 10;
2670 }
2771
2872 /** @param array{mode: string, value: string} $in */
2973 function riceAmount(array $in): array
3074 {
31− if (!is_numeric($in['value']) || !in_array($in['mode'], ['gou', 'servings', 'grams'], true)) {
75+ $mode = $in['mode'];
76+ $value = $in['value'];
77+ if (!is_string($value) || !preg_match(DECIMAL, $value) || trim($value, '0.') === ''
78+ || !in_array($mode, ['gou', 'servings', 'grams'], true)) {
3279 return ['error' => 'invalid_input'];
3380 }
34− $v = (float) $in['value'];
35− if ($v <= 0) {
36− return ['error' => 'invalid_input'];
37− }
38− if ($in['mode'] === 'servings') {
39− $gou = round($v * BOWL_G / (G_PER_GOU * COOK_MID) * 2) / 2; // 0.5合単位
40− if ($gou < 0.5) {
41− $gou = 0.5;
42− }
43− $g = $gou * G_PER_GOU;
44− } elseif ($in['mode'] === 'grams') {
45− $g = $v;
46− $gou = $g / G_PER_GOU;
81+ [$int, $frac] = explode('.', $value . '.');
82+ $n = ltrim($int . $frac, '0'); // 値 = $n ÷ 10^$s
83+ $s = strlen($frac);
84+ if ($mode === 'servings') {
85+ // 合 = 杯数 × 150 ÷ (150 × 2.2) = 杯数 × 10 ÷ 22。0.5合単位 = (合 × 2) を四捨五入した半合の数
86+ $halves = max(roundRatio($n, $s, 10, 11), 1);
87+ $tenths = $halves * 5;
88+ [$g, $gs] = [(string) ($halves * 75), 0]; // 生米 = 半合 × 75g
89+ } elseif ($mode === 'grams') {
90+ $tenths = roundRatio($n, $s, 1, 15); // g ÷ 150 × 10
91+ [$g, $gs] = [$n, $s];
4792 } else {
48− $gou = $v;
49− $g = $gou * G_PER_GOU;
93+ $tenths = roundRatio($n, $s, 10, 1);
94+ [$g, $gs] = [mulSmall($n, 150), $s];
5095 }
5196 return [
52− 'gou' => gouText($gou),
53− 'rice_g' => (int) round($g),
54− 'ml' => (int) round($g / G_PER_GOU * ML_PER_GOU),
55− 'cooked_low' => round5($g * COOK_LO),
56− 'cooked_high' => round5($g * COOK_HI),
57− 'bowls' => (int) round($g * COOK_MID / BOWL_G),
58− 'onigiri' => (int) round($g * COOK_MID / ONIGIRI_G),
97+ 'gou' => gouText($tenths),
98+ 'rice_g' => roundRatio($g, $gs, 1, 1),
99+ 'ml' => roundRatio($g, $gs, 6, 5), // g ÷ 150 × 180
100+ 'cooked_low' => roundRatio($g, $gs, 21, 50) * 5, // g × 2.1 ÷ 5
101+ 'cooked_high' => roundRatio($g, $gs, 23, 50) * 5, // g × 2.3 ÷ 5
102+ 'bowls' => roundRatio($g, $gs, 11, 750), // g × 2.2 ÷ 150
103+ 'onigiri' => roundRatio($g, $gs, 11, 525), // g × 2.2 ÷ 105
59104 ];
60105 }

仕様

入力

項目型説明
mode文字列gou(合から)・servings(茶碗の杯数から)・grams(生米のグラムから)
value文字列10進の数("2.5"・"4"・"175")

出力

成功したとき: {"gou": "2.5", "rice_g": 375, "ml": 450, "cooked_low": 790, "cooked_high": 865, "bowls": 6, "onigiri": 8}

基準値(元ツールと同じ): 1合 = 生米 150g = 180ml、炊き上がり = 生米 × 2.1〜2.3(中央 2.2)、茶碗1杯 = 150g、おにぎり1個 = 105g。

まず生米のグラム数 g を決める。

  • gou: g = 合 × 150
  • grams: g = 入力のグラム
  • servings: 必要な合 = 杯数 × 150 ÷ (150 × 2.2) を 0.5合単位に四捨五入(0.5合より少なければ 0.5合)。g = その合 × 150
項目型定義
gou文字列合(gou は入力、grams は g ÷ 150、servings は 0.5合単位に丸めた合)を小数第1位に四捨五入。小数第1位が 0 なら整数で書く("3"・"2.5"・"0.2")
rice_g整数g を整数に四捨五入
ml整数g ÷ 150 × 180 を整数に四捨五入
cooked_low整数g × 2.1 を 5g 単位に四捨五入
cooked_high整数g × 2.3 を 5g 単位に四捨五入
bowls整数g × 2.2 ÷ 150 を整数に四捨五入
onigiri整数g × 2.2 ÷ 105 を整数に四捨五入
  • 四捨五入は、丸める前の正確な値に対して行う。値はすべて 0 以上なので「0.5 ちょうどは大きい方へ」(862.5g → 5g 単位で 865g、46.5g → 47g)
  • 5g 単位の四捨五入は、5 で割って整数に四捨五入し、5 を掛ける

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

元ツールとの違い

元ツールは2進数の小数で掛け算し、Math.round で丸めるので、5g 単位の境目で 5g 少なくなることがある(上記)。本 spec は正確な値を丸めた結果を正解にする。

期待値の作り方

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