3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エンジニアが絶対間違えてはいけない「境界値問題」の話

Posted at

はじめに

個人的なことですが、今年もふるさと納税をしました。
そのうちのひとつの自治体からこんな案内(LINE)が届きました。

「寄付した人にお米3Kgプレゼントキャンペーン!!」
なんとも太っ腹な企画です。もちろん参加しようと思って確認したら..

001 (1).png

一見普通の案内に見えますが、エンジニアの皆さんなら「あれ?」と思う部分があるはずです。

何がおかしいのか

問題はここです!

  • 質問文:「1万円以上のご寄附のお申込みはお済みでしょうか?」
  • 注意書き:「1万円以下の場合、本キャンペーンにはお申し込みいただけません」

1万円ちょうどの場合、どっちなんでしょうか?
ちなみに私のこの自治体への寄付額は1万円です。対象なの? 対象外なの!?

この記事では、PythonとC#のサンプルコードを記載しています。

エンジニアあるある:境界値問題

これ、まさにプログラミングでよくやらかす「境界値エラー」と同じ構造ですよね。というか、色々思い出してしまいました。

python
# ❌ よくある間違い
if donation >= 10000:
    print("キャンペーン対象です")
if donation <= 10000:
    print("キャンペーン対象外です")

# donation = 10000 の場合、両方出力される...
C#
// ❌ よくある間違い
if (donation >= 10000)
{
    Console.WriteLine("キャンペーン対象です");
}
if (donation <= 10000)
{
    Console.WriteLine("キャンペーン対象外です");
}

// donation = 10000 の場合、両方出力される...

まぁ、こんな書き方はあんまりしないかもしれないけれど...「結局どっちなん?」って突っ込みたくなるパターン。

正しい条件分岐とは

パターン1: 1万円を含む場合

python
if donation >= 10000:
    print("キャンペーン対象です")
else:
    print("キャンペーン対象外です(1万円未満)")
C#
if (donation >= 10000)
{
    Console.WriteLine("キャンペーン対象です");
}
else
{
    Console.WriteLine("キャンペーン対象外です(1万円未満)");
}

パターン2: 1万円を含まない場合

python
if donation > 10000:
    print("キャンペーン対象です")
else:
    print("キャンペーン対象外です(1万円以下)")
C#
if (donation > 10000)
{
    Console.WriteLine("キャンペーン対象です");
}
else
{
    Console.WriteLine("キャンペーン対象外です(1万円以下)");
}

他にもある境界値問題の事例

せっかくなのでこんなパターンもあります。

1. 配列のインデックス

配列の最後の要素にアクセスしようとして、つい <= を使ってしまうパターンです。

python
# ❌ 間違い
for i in range(len(arr) + 1):  # +1が余計
    print(arr[i])  # IndexErrorが発生

# ✅ 正解
for i in range(len(arr)):
    print(arr[i])
C#
// ❌ 間違い
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歳ちょうどの人が成人なのか未成年なのか、どちらの条件にも当てはまってしまいます。

python
# ❌ 曖昧な仕様
if age >= 18:
    print("成人")
if age < 18:
    print("未成年")
# 18歳ちょうどの場合の扱いが不明確
C#
// ❌ 曖昧な仕様
if (age >= 18)
{
    Console.WriteLine("成人");
}
if (age < 18)
{
    Console.WriteLine("未成年");
}
// 18歳ちょうどの場合の扱いが不明確

3. 日付の範囲指定

「2024年中」という範囲を指定したつもりが、12月31日の23時59分59秒までしか含まれません。

python
# ❌ よくある間違い
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
C#
// ❌ よくある間違い
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県警で運転免許証の発行に遅れが生じました。

python
# ❌ うるう年を考慮していない処理
if month == 2 and day == 29:
    # エラー: 2月29日は存在しない前提で処理
    raise ValueError("2月29日は無効な日付")
C#
// ❌ うるう年を考慮していない処理
if (month == 2 && day == 29)
{
    // エラー: 2月29日は存在しない前提で処理
    throw new InvalidOperationException("2月29日は無効な日付");
}

テストケースを書く重要性

境界値問題を防ぐには、必ず境界値のテストケースを書きましょう。

pythonのテストケース
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#のテストケース
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. 仕様の曖昧さを見つける眼力
  2. 境界値を意識した質問をする習慣
  3. テストケースで仕様を明確化する技術

これは絶対覚えておこう!!(初心者向け)

以上」「以下」「」「未満」の使い分けは、エンジニアの基本スキルです。今回は1万円にこだわってこんな感じで...

002 (2).png

改めて表でみるとこんな感じです。

表現 数式 例(基準 = 10000円) 含まれる値
以上 >= 10000円以上 10000円〜
以下 <= 10000円以下 0〜10000円
> 10000円超 10001円〜
未満 < 10000円未満 0〜9999円

ポイント
「以上・以下」と「超・未満」を混在させないこと!

まとめ

  • 境界値問題は、プログラミングだけでなく日常生活にも潜んでいる
  • 以上」「以下」「」「未満」の使い分けは、エンジニアの基本スキル
  • 仕様レビューの段階で境界値問題を発見できるエンジニアになろう
  • テストケースは境界値を必ず含めよう
  • 日付・時間の境界値問題は、実際に大きなシステム障害を引き起こすことがある

過去から未来へ続く「年問題」の系譜

境界値問題は過去にも大きな社会問題を引き起こし、今後も私たちを待ち受けています。

過去に起きた大規模障害対策

  • 2000年問題(Y2K):年を下2桁で管理していたシステムが2000年を1900年と誤認識し、世界中で対策に数百億ドルが投入された
    参考:2000年問題 - Wikipedia

現在進行中・今後予想される問題

これらの問題に共通するのは「当時は十分だと思われた設計が、時間の経過で限界を迎える」ことです。

現在開発しているシステムも、10年後・20年後に同じ問題を起こさないよう、境界値を意識した設計が求められます。レガシーシステムの保守だけでなく、未来の「時限爆弾」を作らないことを意識しておきたいです。

おまけ:自治体の方へ

もしこの記事を見ていたら、正しくはこう書きましょう!

[ 1万円もOKなら ]
「1万円以上のご寄附の場合、キャンペーン対象です。1万円未満の場合は対象外です。」

[ 1万円はNGなら ]
「1万円のご寄附の場合、キャンペーン対象です。1万円以下の場合は対象外です。」

ちなみに、私(1万円寄付)はキャンペーン応募しました。
お米楽しみにお待ちしています!

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?