たなかのJava日記

どんなことをやったか(学んだか)、どこで詰まったか(わからなかったか)、どこで工夫したかの記録です。

「西暦からサッカーワールドカップ開催年」を判別する

サッカーワールドカップ開幕まであと7日です。
前のワールドカップはいつだったかな~と気になったので、
以前「西暦からサッカーワールドカップ開催年」を判別するコードを作成しました。

下記がJavadocを書いた際に作成したコードです。

 /**
     * 渡された西暦年がサッカーワールドカップ開催年であるかどうかを判定する
     * @param year 西暦
     * @return サッカーワールドカップ開催都市であればture
     * @throws IllegalArgumentException まだオリンピック開催が確定していない年を渡した場合
     */
    public boolean isSoccerWorldCupYear(int year) throws IllegalArgumentException {
        if (2026 < year) {
            throw new IllegalArgumentException("2026年までサポートしています。入力" + year);
        }
        return year % 4 == 2;
    }

サッカーワールドカップは、西暦を4で割って余りが2になる年に開催されます。
また、現時点では次回大会の2026年までの開催が確定しています。
その旨をコードに落とし込んだのですが、必ずしも4年に1度開催しているわけではありません。
例えば、同じく4年に1度開催しているオリンピックはコロナで延期になりましたね。
サッカーワールドカップも戦争で中止になった年があります。
ということは、このコードにはバグがあると言えます。
(他にも西暦18年は、4で割って余りが2になりますが、当然ワールドカップは開催されていません)
大した修正量ではないので、すぐに直してしまえばいいのですが、
Junitを学んだのでテストコードを書いてから修正を行いたいと思います。

テストを書く前に、まずはテスト項目を洗い出しました。
境界値下限 ⇒ サッカーワールドカップ開催以前の年はfalse
通常開催年 ⇒ 4年周期の開催年はtrue
非開催年 ⇒ 4年周期から外れる非開催年はfalse
中止年 ⇒ 戦争で中止になった年はfalse
延期開催年 ⇒ 4年間隔ではない例外的な開催年はtrue
サッカーワールドカップには延期開催年は無かったため、テスト項目から除外。
境界値上限 ⇒ 開催地が決定している年よりも後は例外発生

テスト項目をもとに、テストコードを書くと下記のようになりました。

package soccer;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@SuppressWarnings("ALL")
class WorldCupTest {

    @Test
    void 境界値下限() {
        // サッカーワールドカップ開始以前
        assertFalse(new WorldCup().isSoccerWorldCupYear(1890), "1890年");
        assertFalse(new WorldCup().isSoccerWorldCupYear(1925), "1925年");
        assertFalse(new WorldCup().isSoccerWorldCupYear(1929), "1929年");
        // 初回開催年
        assertTrue(new WorldCup().isSoccerWorldCupYear(1930), "1930年");
    }

    @Test
    void 四年周期の開催年() {
        int years = {1950, 1986, 2002, 2018};
        for (int year: years) {
            assertTrue(new WorldCup().isSoccerWorldCupYear(year), year + "年");
        }
    }

    @Test
    void 戦争による開催中止年() {
        int years = {1942, 1946};
        for (int year: years) {
            assertFalse(new WorldCup().isSoccerWorldCupYear(year), year + "年");
        }
    }

    @Test
    void 境界値上限() {
        assertDoesNotThrow(

                     () -> new WorldCup().isSoccerWorldCupYear(2025));
        assertDoesNotThrow(

                     () -> new WorldCup().isSoccerWorldCupYear(2026));

        // 開催が決定している2026年より後は例外が発生すること
        assertThrows(IllegalArgumentException.class,
                () -> new WorldCup().isSoccerWorldCupYear(2027));
        assertThrows(IllegalArgumentException.class,
                () -> new WorldCup().isSoccerWorldCupYear(2028));
    }
}

このテストコードを実行するともちろん失敗します。
次に失敗したケースのテストが通るように実装を進めていきます。

package soccer;

public class WorldCup {
    /**
     * 渡された西暦年がサッカーワールドカップ開催年であるかどうかを判定する
     * @param year 西暦
     * @return サッカーワールドカップ開催都市であればture
     * @throws IllegalArgumentException まだオリンピック開催が確定していない年を渡した場合
     */
    public boolean isSoccerWorldCupYear(int year) throws IllegalArgumentException {
        // 境界値下限
        if (year < 1930) {
            return false;
        }
        // 戦争で中止
        if (year == 1942 || year == 1946) {
            return false;
        }
        // 境界値上限を超えたら例外発生
        if (year > 2026) {
            throw new IllegalArgumentException("2026年までサポートしています。入力" + year);
        }
        // 四年周期の開催年
        return year % 4 == 2;
    }
}

テストケースが通るようになりました。
今回は軽く実装していましたが、実装よりもテストを先に書く手法を
テスト駆動開発」と呼ぶらしく注目されている手法とのことです。

 

※コードのあちこちで文字サイズがバラバラだったり、表示がおかしかったりしますが、理由がわかりません。ご了承ください。