はじめに
個人的なことですが、今年もふるさと納税をしました。
そのうちのひとつの自治体からこんな案内(LINE)が届きました。
「寄付した人にお米3Kgプレゼントキャンペーン!!」
なんとも太っ腹な企画です。もちろん参加しようと思って確認したら..
一見普通の案内に見えますが、エンジニアの皆さんなら「あれ?」と思う部分があるはずです。
何がおかしいのか
問題はここです!
- 質問文:「1万円以上のご寄附のお申込みはお済みでしょうか?」
- 注意書き:「1万円以下の場合、本キャンペーンにはお申し込みいただけません」
1万円ちょうどの場合、どっちなんでしょうか?
ちなみに私のこの自治体への寄付額は1万円です。対象なの? 対象外なの!?
この記事では、PythonとC#のサンプルコードを記載しています。
エンジニアあるある:境界値問題
これ、まさにプログラミングでよくやらかす「境界値エラー」と同じ構造ですよね。というか、色々思い出してしまいました。
# ❌ よくある間違い
if donation >= 10000:
print("キャンペーン対象です")
if donation <= 10000:
print("キャンペーン対象外です")
# donation = 10000 の場合、両方出力される...
// ❌ よくある間違い
if (donation >= 10000)
{
Console.WriteLine("キャンペーン対象です");
}
if (donation <= 10000)
{
Console.WriteLine("キャンペーン対象外です");
}
// donation = 10000 の場合、両方出力される...
まぁ、こんな書き方はあんまりしないかもしれないけれど...「結局どっちなん?」って突っ込みたくなるパターン。
正しい条件分岐とは
パターン1: 1万円を含む場合
if donation >= 10000:
print("キャンペーン対象です")
else:
print("キャンペーン対象外です(1万円未満)")
if (donation >= 10000)
{
Console.WriteLine("キャンペーン対象です");
}
else
{
Console.WriteLine("キャンペーン対象外です(1万円未満)");
}
パターン2: 1万円を含まない場合
if donation > 10000:
print("キャンペーン対象です")
else:
print("キャンペーン対象外です(1万円以下)")
if (donation > 10000)
{
Console.WriteLine("キャンペーン対象です");
}
else
{
Console.WriteLine("キャンペーン対象外です(1万円以下)");
}
他にもある境界値問題の事例
せっかくなのでこんなパターンもあります。
1. 配列のインデックス
配列の最後の要素にアクセスしようとして、つい <= を使ってしまうパターンです。
# ❌ 間違い
for i in range(len(arr) + 1): # +1が余計
print(arr[i]) # IndexErrorが発生
# ✅ 正解
for i in range(len(arr)):
print(arr[i])
// ❌ 間違い
for (int i = 0; i <= arr.Length; i++)
{
Console.WriteLine(arr[i]); // IndexOutOfRangeExceptionが発生
}
// ✅ 正解
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
2. 年齢判定
18歳ちょうどの人が成人なのか未成年なのか、どちらの条件にも当てはまってしまいます。
# ❌ 曖昧な仕様
if age >= 18:
print("成人")
if age < 18:
print("未成年")
# 18歳ちょうどの場合の扱いが不明確
// ❌ 曖昧な仕様
if (age >= 18)
{
Console.WriteLine("成人");
}
if (age < 18)
{
Console.WriteLine("未成年");
}
// 18歳ちょうどの場合の扱いが不明確
3. 日付の範囲指定
「2024年中」という範囲を指定したつもりが、12月31日の23時59分59秒までしか含まれません。
# ❌ よくある間違い
from datetime import datetime
start_date = datetime(2024, 1, 1)
end_date = datetime(2024, 12, 31)
if start_date <= date <= end_date:
# 2024-12-31 23:59:59 は含まれるが
# 2025-01-01 00:00:00 は含まれない
# でも2024-12-31の深夜24時は?
pass
// ❌ よくある間違い
var startDate = new DateTime(2024, 1, 1);
var endDate = new DateTime(2024, 12, 31);
if (date >= startDate && date <= endDate)
{
// 2024-12-31 23:59:59 は含まれるが
// 2025-01-01 00:00:00 は含まれない
// でも2024-12-31の深夜24時は?
}
4. うるう年の境界値問題(実際の事例)
2024年2月29日、うるう年が原因で多数のシステム障害が発生しました。スギ薬局の約1300店舗で処方箋登録ができなくなったり、神奈川・新潟・岡山・愛媛の4県警で運転免許証の発行に遅れが生じました。
# ❌ うるう年を考慮していない処理
if month == 2 and day == 29:
# エラー: 2月29日は存在しない前提で処理
raise ValueError("2月29日は無効な日付")
// ❌ うるう年を考慮していない処理
if (month == 2 && day == 29)
{
// エラー: 2月29日は存在しない前提で処理
throw new InvalidOperationException("2月29日は無効な日付");
}
テストケースを書く重要性
境界値問題を防ぐには、必ず境界値のテストケースを書きましょう。
pythonのテストケース
import unittest
class TestDonationEligibility(unittest.TestCase):
def test_寄附金額9999円は対象外(self):
# Arrange
donation = 9999
# Act
result = is_eligible(donation)
# Assert
self.assertFalse(result)
def test_寄附金額10000円は対象かどうか仕様要確認(self):
# Arrange
donation = 10000
# Act
result = is_eligible(donation)
# Assert
# この部分で仕様を明確にする必要がある
self.assertTrue(result) # or self.assertFalse(result)
def test_寄附金額10001円は対象(self):
# Arrange
donation = 10001
# Act
result = is_eligible(donation)
# Assert
self.assertTrue(result)
def is_eligible(donation):
# 実装例(要仕様確認)
return donation >= 10000 # または donation > 10000
<details
C#のテストケース
[TestClass]
public class DonationEligibilityTests
{
[TestMethod]
public void 寄附金額9999円は対象外()
{
// Arrange
int donation = 9999;
// Act
bool result = IsEligible(donation);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void 寄附金額10000円は対象かどうか仕様要確認()
{
// Arrange
int donation = 10000;
// Act
bool result = IsEligible(donation);
// Assert
// この部分で仕様を明確にする必要がある
Assert.IsTrue(result); // or Assert.IsFalse(result);
}
[TestMethod]
public void 寄附金額10001円は対象()
{
// Arrange
int donation = 10001;
// Act
bool result = IsEligible(donation);
// Assert
Assert.IsTrue(result);
}
private bool IsEligible(int donation)
{
// 実装例(要仕様確認)
return donation >= 10000; // または donation > 10000
}
}
仕様書レビューの重要性
この自治体の案内のような問題は、実はコードを書く前の仕様定義段階で発生しています。
エンジニアとして大切なのは...
- 仕様の曖昧さを見つける眼力
- 境界値を意識した質問をする習慣
- テストケースで仕様を明確化する技術
これは絶対覚えておこう!!(初心者向け)
「以上」「以下」「超」「未満」の使い分けは、エンジニアの基本スキルです。今回は1万円にこだわってこんな感じで...
改めて表でみるとこんな感じです。
| 表現 | 数式 | 例(基準 = 10000円) | 含まれる値 |
|---|---|---|---|
| 以上 | >= |
10000円以上 | 10000円〜 |
| 以下 | <= |
10000円以下 | 0〜10000円 |
| 超 | > |
10000円超 | 10001円〜 |
| 未満 | < |
10000円未満 | 0〜9999円 |
ポイント
「以上・以下」と「超・未満」を混在させないこと!
まとめ
- 境界値問題は、プログラミングだけでなく日常生活にも潜んでいる
- 「以上」「以下」「超」「未満」の使い分けは、エンジニアの基本スキル
- 仕様レビューの段階で境界値問題を発見できるエンジニアになろう
- テストケースは境界値を必ず含めよう
- 日付・時間の境界値問題は、実際に大きなシステム障害を引き起こすことがある
過去から未来へ続く「年問題」の系譜
境界値問題は過去にも大きな社会問題を引き起こし、今後も私たちを待ち受けています。
過去に起きた大規模障害対策
-
2000年問題(Y2K):年を下2桁で管理していたシステムが2000年を1900年と誤認識し、世界中で対策に数百億ドルが投入された
参考:2000年問題 - Wikipedia
現在進行中・今後予想される問題
-
2025年問題(昭和100年問題):昭和で年を管理している古いシステムが、昭和100年で2桁オーバーフロー
参考:昭和100年問題 Wikipedia -
2038年問題:UNIX時間(1970年1月1日からの秒数)が32ビット整数の上限に到達し、システムが1901年に戻る
参考:今のうちに知っておこう。2038年問題 | Sqripts -
2020年問題:Y2K対策で「1920年-2019年」の範囲設定をしたシステムが、すでに問題が発生中
参考:年問題 - Wikipedia
これらの問題に共通するのは「当時は十分だと思われた設計が、時間の経過で限界を迎える」ことです。
現在開発しているシステムも、10年後・20年後に同じ問題を起こさないよう、境界値を意識した設計が求められます。レガシーシステムの保守だけでなく、未来の「時限爆弾」を作らないことを意識しておきたいです。
おまけ:自治体の方へ
もしこの記事を見ていたら、正しくはこう書きましょう!
[ 1万円もOKなら ]
「1万円以上のご寄附の場合、キャンペーン対象です。1万円未満の場合は対象外です。」
[ 1万円はNGなら ]
「1万円超のご寄附の場合、キャンペーン対象です。1万円以下の場合は対象外です。」
ちなみに、私(1万円寄付)はキャンペーン応募しました。
お米楽しみにお待ちしています!

