31番だけ色がない——Math.ceil の落とし穴とCSSクラス設計
番号ボールの色をMath.ceil(n/10)で出したら31番だけball-4になってCSSがなかった。たった1行の修正で解決した話
登場人物
- リナ社長 … 高校生なのに会社経営するやり手ギャル。テックにも強くてAI活用が得意。
- タクヤ … 入社3年目の男性社員。真面目で少しだけコードが書ける。
今回は、31番のボールだけ色が消えた「Math.ceilの落とし穴」を掘り返すデバッグ回。
タクヤ「社長、一覧テーブルで31番だけ白く見えます。背景色がないみたいで……」
リナ社長「あ、まじじゃん。てかこれ完全に計算ミスだわ」
タクヤ「計算ミスって、数字の計算ですか?」
リナ社長「そー。Claudeが書いたコードに微妙なバグがあった。ちょっと一緒に見てみよ」
何が起きたのか
ミニロト解析ページでは番号ボールの色を数字の範囲で分けている。
| 範囲 | 色 | CSSクラス |
|---|---|---|
| 1〜10 | 青 | ball-1 |
| 11〜20 | 緑 | ball-2 |
| 21〜30 | オレンジ | ball-3 |
この色分けをAstroテンプレートでこう実装していた:
<span class={`ball ball-${Math.ceil(n / 10)}`}>
{String(n).padStart(2, '0')}
</span>
タクヤ「Math.ceil(n / 10) で1〜10は1、11〜20は2……ってなるわけですね」
リナ社長「そーそー。でも31は?」
タクヤ「31 ÷ 10 = 3.1、切り上げで……4!あ、ball-4 になる」
リナ社長「そういうこと。ball-4 なんてCSSに定義してないから色なしになってた」
なぜミニロトで問題が起きるのか
普通のロト6(1〜43)やロト7(1〜37)なら「10ごとに色を分ける」設計でも4グループや5グループのCSSを用意すれば問題ない。
でもミニロトの数字範囲は 1〜31 。
Math.ceil(31 / 10) = 4 になるのに、定義したCSSクラスは ball-1〜ball-3 の3つだけ。31だけが孤立する。
タクヤ「ミニロトって31までしかないから、31が1個だけ仲間外れになっちゃうんですね」
リナ社長「そーなの。“10で割って切り上げ”って式は1〜30なら完璧なんだけど、31があることを考慮してなかった」
解決策:Math.min で上限を3に固定する
直し方はシンプル。Math.ceil の結果が3を超えないように Math.min で上限を指定するだけ。
<!-- ❌ Before(31がball-4になってしまう) -->
<span class={`ball ball-${Math.ceil(n / 10)}`}>
<!-- ✅ After(上限を3に固定) -->
<span class={`ball ball-${Math.min(Math.ceil(n / 10), 3)}`}>
これで Math.ceil(31 / 10) = 4 になっても Math.min(4, 3) = 3 で ball-3(オレンジ)が適用される。
タクヤ「たった1箇所直すだけで解決するんですね」
リナ社長「ただ、同じ式を4箇所で使ってたから全部直さないといけなかった。ヒーローセクション・ホット数字・コールド数字・一覧テーブルの4箇所ね」
タクヤ「同じロジックが複数箇所にあるのか……」
リナ社長「理想はヘルパー関数にまとめることなんだけど、今回はサクッと直す優先で。ワンチャンリファクタは後でもいいしね」
こういうバグを防ぐには
タクヤ「最初からバグを防ぐ方法ってあるんですか?」
リナ社長「あるよ。今回の場合だと、最初にClaudeへの指示に数字範囲を明示することかな」
ミニロトは1〜31の番号を使う。
色分けは1〜10が青、11〜20が緑、21〜31がオレンジ。
31番も必ずオレンジになるようにすること。
タクヤ「仕様を具体的に書くだけで違うんですね」
リナ社長「そー!“10ごとに色分け”って言い方だとAIが素直に実装して31の端数を見落とす。“21〜31はオレンジ”って書けば31を考慮した実装が出てくる」
これは「境界値バグ」の典型例
タクヤ「今回のバグ、なんだか”あるある”な感じがします」
リナ社長「まさに。これ”オフバイワン”とか”境界値バグ”って呼ばれる、プログラミングで一番踏みやすい地雷のひとつなんだよね」
範囲を区切る処理は、真ん中の値はだいたい合っていて、端(最小値・最大値)だけがズレるのが特徴です。1〜30はきれいに3グループに収まるのに、最後の31だけがはみ出した今回はその典型。だからテストするときも、真ん中の値より「最初・最後・境界のすぐ内外」を重点的に確認するのが鉄則です。
今回は4箇所に同じ式が散らばっていたので応急処置で Math.min を入れましたが、本来は色分けロジックを1つの関数に集約しておくと、こうした修正が1箇所で済みます。
// 色分けを1箇所にまとめておく(範囲を明示するので端数バグが起きにくい)
function ballClass(n: number): string {
if (n <= 10) return 'ball-1';
if (n <= 20) return 'ball-2';
return 'ball-3'; // 21〜31 はすべてここに入る
}
タクヤ「Math.ceil の式より、こっちの方が”21〜31はオレンジ”って仕様がそのままコードに出てますね」
リナ社長「そーそー。計算で範囲を出すより、範囲をそのまま書いた方が後から読む人にも優しいんだよね」
ポイントまとめ
リナ社長「まとめるとこれ」
- 数値でCSSクラスを生成するときは端数に注意 → 今回は31が範囲外のクラスを生成していた
Math.minで上限を固定するのが簡単な解決策 →Math.min(Math.ceil(n / 10), 3)- AIへの指示は”例外ケース”も明記する → “21〜31はオレンジ”のように境界を具体的に伝える
タクヤ「これ、AIが書いたコードでも自分で検証することが大事ってことですよね」
リナ社長「まじそれ!AIを信頼しすぎず、ちゃんと動くか見てあげることが大事。AIと人間のいいコンビネーションがえぐい成果を生む、っしょ」
コメント