ローレル指数

身長と体重から、子どもの体格の目安になるローレル指数(体重 ÷ 身長³ × 10)と区分、標準の体重の範囲を出す。

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

言語ごとの結果

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

PHPPHP 8.4.26

よくある書き方9 / 13

直した書き方13 / 13

PythonPython 3.14.7

よくある書き方9 / 13

直した書き方13 / 13

GoGo 1.27.2

よくある書き方7 / 13

直した書き方13 / 13

JavaScriptNode 24.15.0

よくある書き方7 / 13

直した書き方13 / 13

JavaJava 21.0.12

よくある書き方7 / 13

直した書き方13 / 13

C#.NET 10.0.402

よくある書き方7 / 13

直した書き方13 / 13

RustRust 1.99.0

よくある書き方7 / 13

直した書き方13 / 13

RubyRuby 4.0.7

よくある書き方9 / 13

直した書き方13 / 13

KotlinKotlin 2.4.21

よくある書き方7 / 13

直した書き方13 / 13

SwiftSwift 6.3.3

よくある書き方7 / 13

直した書き方13 / 13

なぜ割れたのか

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

2進小数の誤差

指数がちょうど100なのに「やせ」——身長1.3mの3乗で、境目のわずかに下に落ちた

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

身長 130cm・体重 21.97kg(c004)のローレル指数は、21.97 ÷ 1.3³ × 10 = 100 ちょうどです。区分は「100未満がやせ」なので、正解は やせ気味 です。10言語中7言語が やせ と判定しました。

ほかの境目でも同じことが起きました。

ケース正確な指数正解の区分下の区分にした言語
c004 130cm・21.97kg100やせ気味7言語(C#・Go・Java・JavaScript・Kotlin・Rust・Swift)
c005 130cm・25.2655kg115ふつう(標準)同じ7言語
c006 160cm・59.392kg145太り気味10言語すべて
c007 110cm・21.296kg160肥満10言語すべて

公開中の元ツールも同じで、区分のちょうど境目になる入力(身長 100.0〜180.0cm で、体重が小数4桁以内で表せるもの)44通り中19通りで、下の区分と判定します。

なぜ割れたのか

1.3・1.6・1.1 は2進数の小数では正確に表せず、3乗すると少し大きくなります。

身長3回掛け(h*h*h)累乗の関数(h ** 3・pow)
1.3m2.19700000000000052.197
1.6m4.0960000000000014.096000000000001
1.1m1.33100000000000041.3310000000000004

割る数が少し大きいので、指数は境目のわずかに下(99.99999999999997、144.99999999999997、159.99999999999994)になり、< 100 の比較で下の区分に入ります。

c004・c005 で言語が割れたのは、3乗の書き方の違いです。Python・PHP・Ruby は hm ** 3 と書き、累乗の関数が 1.3³ を 2.197 ちょうどに計算したので、指数がちょうど 100・115 になって通りました。3回掛けた言語(JavaScript・Java・C#・Kotlin・Swift)と、math.Pow・powi(3) の Go・Rust は落ちました。1.6 と 1.1 は、どちらの書き方でも同じく少し大きくなるので、10言語すべてが落ちました。累乗の書き方で救われたのは、たまたまです。

どう直したか

身長と体重を2進数の小数に直さず、正確な分数で計算しました。比べるときも割り算をせず、「体重 × 10 < 境目 × (身長 ÷ 100)³」のように掛け算の形に直して、整数・分数のまま比べています。

  • Python: fractions.Fraction、Go: big.Rat、Ruby: Rational
  • Java・Kotlin: BigDecimal
  • C#: decimal の掛け算で比べる
  • JavaScript: BigInt の分子と分母、PHP・Rust・Swift: 整数の分数

2進小数の誤差

指数 31.25 を、10言語すべてが 31.3 ではなく 31.2 と表示した

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

身長 160cm・体重 12.8kg(c009)の指数は 12.8 ÷ 1.6³ × 10 = 31.25、小数第1位に四捨五入して 31.3 が正解です。10言語すべてが 31.2 と表示しました。体重 64kg(156.25 → 156.3、c010)も、10言語すべてが 156.2 でした。

なぜずれたのか

1.6³ が2進数の小数で 4.096000000000001 と少し大きくなるので、指数は 31.249999999999996・156.24999999999997 と、0.05 の境目のわずかに下になります。小数1桁の書式(%.1f・toFixed(1)・ToString("F1")・format!("{:.1}") など)は、この値をそのまま丸めるので、どの言語でも小さい方になりました。丸め方(0.5 ちょうどを偶数へ寄せるか)とは関係がありません。

元ツールも同じ書き方で、身長 100.0〜180.0cm × 体重 10.0〜80.0kg(0.1刻み)の561,501通り中3通りで、表示が正確な四捨五入と違います(いずれも身長 160cm)。

どう直したか

解説1 と同じく正確な分数で計算し、小数第1位に「0.5 ちょうどは大きい方へ」四捨五入しました。Java・Kotlin は BigDecimal の divide(…, 1, HALF_UP) で、正確な商を1回だけ丸めます。C# は decimal の割り算が28桁で一度丸まる(二重の丸めになる)のを避けるため、整数の商と余りで繰り上げを決めています。

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

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

ケース入力正解よくある書き方の答え(10言語)
c001基本
height_cm
130
weight_kg
28
index
127.4
category
ふつう(標準)
normal_low
25.3
normal_high
31.9

✓10言語すべて正解

c002やせ
height_cm
150
weight_kg
30
index
88.9
category
やせ
normal_low
38.8
normal_high
48.9

✓10言語すべて正解

c003肥満
height_cm
120
weight_kg
30
index
173.6
category
肥満
normal_low
19.9
normal_high
25.1

✓10言語すべて正解

c004境目ちょうど100・2進数で小さく入る 21.97 ÷ 1.3³ × 10 = 100 ちょうど → やせ気味。2進数の小数で計算すると 99.999999999999972 になり、元ツールは「やせ」と判定する なぜ? 解説を読む
height_cm
130
weight_kg
21.97
index
100.0
category
やせ気味
normal_low
25.3
normal_high
31.9
  • Go・JavaScript・Java・C#・Rust・Kotlin・Swiftcategory"やせ"
  • PHP・Python・Ruby✓ 正解
c005境目ちょうど115・2進数で小さく入る 115 ちょうど → ふつう(標準)。2進数の小数だと 114.99999999999997 なぜ? 解説を読む
height_cm
130
weight_kg
25.2655
index
115.0
category
ふつう(標準)
normal_low
25.3
normal_high
31.9
  • Go・JavaScript・Java・C#・Rust・Kotlin・Swiftcategory"やせ気味"
  • PHP・Python・Ruby✓ 正解
c006境目ちょうど145・2進数で小さく入る 145 ちょうど → 太り気味。2進数の小数だと 144.99999999999997 なぜ? 解説を読む
height_cm
160
weight_kg
59.392
index
145.0
category
太り気味
normal_low
47.1
normal_high
59.4
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftcategory"ふつう(標準)"
c007境目ちょうど160・2進数で小さく入る 160 ちょうど → 肥満。2進数の小数だと 159.99999999999994 なぜ? 解説を読む
height_cm
110
weight_kg
21.296
index
160.0
category
肥満
normal_low
15.3
normal_high
19.3
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftcategory"太り気味"
c008境目ちょうど100・2進数でも正確 100 ちょうど → やせ気味
height_cm
100
weight_kg
10
index
100.0
category
やせ気味
normal_low
11.5
normal_high
14.5

✓10言語すべて正解

c009指数が .x5 ちょうど 31.25 → 31.3。2進数の小数で計算すると 31.249999999999996 になり、元ツールは 31.2 と出す なぜ? 解説を読む
height_cm
160
weight_kg
12.8
index
31.3
category
やせ
normal_low
47.1
normal_high
59.4
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftindex"31.2"
c010指数が .x5 ちょうど(2) 156.25 → 156.3。2進数の小数で計算すると 156.24999999999997(元ツールは 156.2) なぜ? 解説を読む
height_cm
160
weight_kg
64
index
156.3
category
太り気味
normal_low
47.1
normal_high
59.4
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftindex"156.2"
c011表示は 115.0 でも区分は やせ気味 指数 114.96… は四捨五入すると 115.0 だが、区分は丸める前の値で決める(115 未満なので やせ気味)
height_cm
100.3
weight_kg
11.6
index
115.0
category
やせ気味
normal_low
11.6
normal_high
14.6

✓10言語すべて正解

c012身長0はエラー
height_cm
0
weight_kg
30
error
invalid_input

✓10言語すべて正解

c013数でない入力はエラー
height_cm
130
weight_kg
abc
error
invalid_input

✓10言語すべて正解

コード

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

Roehrer.php+122 −19よくある書き方(0044896)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * ローレル指数。
6− * v1: 身長・体重を float に読み、体重 ÷ (身長 ÷ 100) ** 3 × 10 を計算して、区分は丸める前の指数を < で比べ、
7− * 指数とふつうの体重の目安を sprintf('%.1f') で書く素直な書き方。
6+ * v2: 身長・体重の文字列を float に読まず、小数点を外した数字の列(10^桁数 倍した整数)として10進のまま読み、
7+ * 指数を「分子 ÷ 分母」の整数の組で正確に持つ。区分は分子と「境目 × 分母」を比べ、
8+ * 表示は小数第1位に「0.5 ちょうどは大きい方へ」四捨五入する。
9+ * v1 の float では 160cm・59.392kg の指数 145 ちょうどが 144.99999999999997 になって「ふつう(標準)」に落ち、
10+ * 160cm・12.8kg の 31.25 が 31.249999999999996 になって sprintf('%.1f') で 31.2 に落ちた。
11+ * 身長の3乗に 10 の累乗を掛けると int の上限(約 9.2 × 10^18)を超えうるので、整数は数字の列のまま筆算する
12+ * (bcmath・gmp は入っていない環境があるので使わない)。
813 */
14+const DECIMAL = '/^[0-9]+(\.[0-9]+)?$/D'; // D: $ を末尾の改行の前で一致させない
15+const BANDS = [['100', 'やせ'], ['115', 'やせ気味'], ['145', 'ふつう(標準)'], ['160', '太り気味']];
916
10−function roehrerCategory(float $index): string
17+/** 先頭の 0 を外す(0 は "0") */
18+function digitsNorm(string $a): string
1119 {
12− if ($index < 100) return 'やせ';
13− if ($index < 115) return 'やせ気味';
14− if ($index < 145) return 'ふつう(標準)';
15− if ($index < 160) return '太り気味';
16− return '肥満';
20+ $a = ltrim($a, '0');
21+ return $a === '' ? '0' : $a;
22+}
23+
24+/** 0 以上の整数(数字の列)の大小。-1・0・1 */
25+function digitsCmp(string $a, string $b): int
26+{
27+ return strlen($a) <=> strlen($b) ?: strcmp($a, $b) <=> 0;
28+}
29+
30+function digitsAdd(string $a, string $b): string
31+{
32+ $n = max(strlen($a), strlen($b));
33+ $a = str_pad($a, $n, '0', STR_PAD_LEFT);
34+ $b = str_pad($b, $n, '0', STR_PAD_LEFT);
35+ $out = '';
36+ $carry = 0;
37+ for ($i = $n - 1; $i >= 0; $i--) {
38+ $s = (int) $a[$i] + (int) $b[$i] + $carry;
39+ $out = ($s % 10) . $out;
40+ $carry = intdiv($s, 10);
41+ }
42+ return digitsNorm(($carry ? '1' : '') . $out);
43+}
44+
45+/** $a - $b($a >= $b のとき) */
46+function digitsSub(string $a, string $b): string
47+{
48+ $b = str_pad($b, strlen($a), '0', STR_PAD_LEFT);
49+ $out = '';
50+ $borrow = 0;
51+ for ($i = strlen($a) - 1; $i >= 0; $i--) {
52+ $d = (int) $a[$i] - (int) $b[$i] - $borrow;
53+ $borrow = $d < 0 ? 1 : 0;
54+ $out = ($d + 10 * $borrow) . $out;
55+ }
56+ return digitsNorm($out);
57+}
58+
59+function digitsMul(string $a, string $b): string
60+{
61+ $la = strlen($a);
62+ $lb = strlen($b);
63+ $acc = array_fill(0, $la + $lb, 0);
64+ for ($i = $la - 1; $i >= 0; $i--) {
65+ for ($j = $lb - 1; $j >= 0; $j--) {
66+ $acc[$i + $j + 1] += (int) $a[$i] * (int) $b[$j];
67+ }
68+ }
69+ for ($k = $la + $lb - 1; $k > 0; $k--) {
70+ $acc[$k - 1] += intdiv($acc[$k], 10);
71+ $acc[$k] %= 10;
72+ }
73+ return digitsNorm(implode('', $acc));
74+}
75+
76+/** 筆算の割り算。[商, 余り] */
77+function digitsDivMod(string $a, string $b): array
78+{
79+ $q = '';
80+ $r = '0';
81+ for ($i = 0, $n = strlen($a); $i < $n; $i++) {
82+ $r = digitsNorm($r . $a[$i]);
83+ $d = 0;
84+ while (digitsCmp($r, $b) >= 0) {
85+ $r = digitsSub($r, $b);
86+ $d++;
87+ }
88+ $q .= $d;
89+ }
90+ return [digitsNorm($q), $r];
91+}
92+
93+/** 0 以上の $num ÷ $den を小数第1位に四捨五入(0.5 ちょうどは大きい方へ)して "31.3" の形にする */
94+function fmt1(string $num, string $den): string
95+{
96+ [$q, $r] = digitsDivMod($num . '0', $den); // 10 倍して割る
97+ if (digitsCmp(digitsAdd($r, $r), $den) >= 0) {
98+ $q = digitsAdd($q, '1');
99+ }
100+ $q = str_pad($q, 2, '0', STR_PAD_LEFT);
101+ return substr($q, 0, -1) . '.' . substr($q, -1);
102+}
103+
104+/** "21.97" → ['2197', 2](値 = 数字の列 ÷ 10^桁数)。読めなければ null */
105+function readDecimal($s): ?array
106+{
107+ if (!is_string($s) || !preg_match(DECIMAL, $s)) {
108+ return null;
109+ }
110+ [$int, $frac] = explode('.', $s . '.');
111+ return [digitsNorm($int . $frac), strlen($frac)];
17112 }
18113
19114 /** @param array{height_cm: string, weight_kg: string} $in */
20115 function roehrer(array $in): array
21116 {
22− if (!is_numeric($in['height_cm']) || !is_numeric($in['weight_kg'])) {
117+ $h = readDecimal($in['height_cm'] ?? null);
118+ $w = readDecimal($in['weight_kg'] ?? null);
119+ if ($h === null || $w === null || $h[0] === '0' || $w[0] === '0') {
23120 return ['error' => 'invalid_input'];
24121 }
25− $height = (float) $in['height_cm'];
26− $weight = (float) $in['weight_kg'];
27− if ($height <= 0 || $weight <= 0) {
28− return ['error' => 'invalid_input'];
122+ // 身長(m)³ = h³ ÷ 10^(3 × 桁数 + 6)
123+ $cubeNum = digitsMul(digitsMul($h[0], $h[0]), $h[0]);
124+ $cubeDen = '1' . str_repeat('0', 3 * $h[1] + 6);
125+ // 指数 = 体重 ÷ 身長(m)³ × 10 = (w × cubeDen × 10) ÷ (10^桁数 × cubeNum)
126+ $num = $w[0] . substr($cubeDen, 1) . '0';
127+ $den = $cubeNum . str_repeat('0', $w[1]);
128+ $category = '肥満';
129+ foreach (BANDS as [$bound, $name]) {
130+ if (digitsCmp($num, digitsMul($bound, $den)) < 0) {
131+ $category = $name;
132+ break;
133+ }
29134 }
30− $hm = $height / 100;
31− $index = $weight / $hm ** 3 * 10;
32135 return [
33− 'index' => sprintf('%.1f', $index),
34− 'category' => roehrerCategory($index),
35− 'normal_low' => sprintf('%.1f', 115 * $hm ** 3 / 10),
36− 'normal_high' => sprintf('%.1f', 145 * $hm ** 3 / 10),
136+ 'index' => fmt1($num, $den),
137+ 'category' => $category,
138+ 'normal_low' => fmt1(digitsMul('115', $cubeNum), $cubeDen . '0'),
139+ 'normal_high' => fmt1(digitsMul('145', $cubeNum), $cubeDen . '0'),
37140 ];
38141 }

仕様

入力

項目型説明
height_cm文字列身長(cm)。10進の数
weight_kg文字列体重(kg)。10進の数

出力

成功したとき: {"index": "127.4", "category": "ふつう(標準)", "normal_low": "25.3", "normal_high": "31.9"}

項目型定義
index文字列体重 ÷ (身長 ÷ 100)³ × 10 を小数第1位に四捨五入。必ず小数1桁で書く("100.0")
category文字列丸める前の正確な指数で決める。100未満「やせ」、115未満「やせ気味」、145未満「ふつう(標準)」、160未満「太り気味」、それ以上「肥満」
normal_low・normal_high文字列「ふつう(標準)」の体重の目安。115 × (身長 ÷ 100)³ ÷ 10 と、145 × … ÷ 10 を小数第1位に四捨五入
  • 四捨五入は、丸める前の正確な値に対して行う。0.5 ちょうどは大きい方へ(31.25 → 31.3)
  • 区分は丸める前の値で決めるので、表示が「115.0」でも区分が「やせ気味」になることがある(c011。指数 114.96…)。元ツールと同じ決め方

判定できないとき: {"error": "invalid_input"}。身長か体重が10進の数として読めないとき、または 0 以下のとき。

元ツールとの違い

元ツールは身長 ÷ 100 の3乗を2進数の小数で計算するので、指数がちょうど境目の値になる入力で、境目のわずかに下になり、下の区分と判定することがある(上記)。本 spec は正確な値で区分を決める。

期待値の作り方

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