Java Tips | 日付・時間:範囲内判定

Java Java
スポンサーリンク

「範囲内判定」とは何をしたいユーティリティか

業務システムでは、ものすごく頻繁に
「この日付はキャンペーン期間内か?」
「この日時はメンテナンス時間帯に含まれるか?」
「この日付は契約の有効期間に入っているか?」
といった“範囲内かどうか”の判定が出てきます。

毎回 if 文でゴチャゴチャ書くと、
「開始を含む?」「終了も含む?」「どっちがどっち?」と読みづらくなり、境界バグも増えます。
そこで、「範囲内判定」を小さなユーティリティとして切り出しておくと、
コードの意図が一気にクリアになります。


まずは LocalDate での範囲内判定の基本

「開始日 ≤ 対象日 ≤ 終了日」を素直に書く

最もよくあるのが「開始日と終了日があって、その間に入っているか?」という判定です。

import java.time.LocalDate;

public class DateRangeUtils {

    public static boolean isWithinInclusive(LocalDate target,
                                            LocalDate startInclusive,
                                            LocalDate endInclusive) {
        if (endInclusive.isBefore(startInclusive)) {
            throw new IllegalArgumentException("終了日は開始日以降である必要があります");
        }

        boolean notBeforeStart = !target.isBefore(startInclusive); // target >= start
        boolean notAfterEnd    = !target.isAfter(endInclusive);    // target <= end
        return notBeforeStart && notAfterEnd;
    }
}
Java

使い方の例です。

LocalDate target = LocalDate.of(2025, 3, 15);
LocalDate start  = LocalDate.of(2025, 3, 10);
LocalDate end    = LocalDate.of(2025, 3, 20);

boolean inRange = DateRangeUtils.isWithinInclusive(target, start, end);
System.out.println(inRange); // true
Java

ここで重要なのは、
「以上」「以下」を !isBefore / !isAfter で表現していることです。

target >= start
→ target が start より前ではない
!target.isBefore(start)

target <= end
→ target が end より後ではない
!target.isAfter(end)

このパターンを覚えると、範囲内判定がかなりスッキリ書けます。


半開区間(開始含む・終了含まない)の範囲内判定

「開始 ≤ 対象 < 終了」という世界観

日時や期間を扱うとき、
「開始は含むけど、終了は含まない(半開区間)」というルールを採用することがよくあります。

例えば、「2025-03-01〜2025-04-01 の手前まで」を「3月分」とみなすようなケースです。

import java.time.LocalDate;

public class DateRangeUtils {

    public static boolean isWithinHalfOpen(LocalDate target,
                                           LocalDate startInclusive,
                                           LocalDate endExclusive) {
        if (!endExclusive.isAfter(startInclusive)) {
            throw new IllegalArgumentException("終了日は開始日より後である必要があります");
        }

        boolean notBeforeStart = !target.isBefore(startInclusive); // target >= start
        boolean beforeEnd      = target.isBefore(endExclusive);    // target < end
        return notBeforeStart && beforeEnd;
    }
}
Java

使い方の例です。

LocalDate target = LocalDate.of(2025, 3, 31);
LocalDate start  = LocalDate.of(2025, 3, 1);
LocalDate end    = LocalDate.of(2025, 4, 1);

System.out.println(DateRangeUtils.isWithinHalfOpen(target, start, end)); // true
System.out.println(DateRangeUtils.isWithinHalfOpen(LocalDate.of(2025, 4, 1), start, end)); // false
Java

ここで深掘りしたいのは、
「閉区間(開始も終了も含む)」と「半開区間(開始含む・終了含まない)」を、
プロジェクトとしてどちらで統一するか、という設計の話です。

期間をまたいで集計したり、連続する期間をつなげたりするとき、
半開区間の方が“隙間”や“重なり”が出にくく、バグを避けやすいことが多いです。


LocalDateTime / Instant での範囲内判定

LocalDateTime の範囲内判定

日付だけでなく、「日時」で範囲内判定をしたいことも多いです。
LocalDateTime でも考え方は同じです。

import java.time.LocalDateTime;

public class DateTimeRangeUtils {

    public static boolean isWithinInclusive(LocalDateTime target,
                                            LocalDateTime startInclusive,
                                            LocalDateTime endInclusive) {
        if (endInclusive.isBefore(startInclusive)) {
            throw new IllegalArgumentException("終了日時は開始日時以降である必要があります");
        }

        boolean notBeforeStart = !target.isBefore(startInclusive);
        boolean notAfterEnd    = !target.isAfter(endInclusive);
        return notBeforeStart && notAfterEnd;
    }
}
Java

ただし、ここで一つ大事な注意があります。
LocalDateTime はタイムゾーンを持たないので、「同じタイムゾーンで解釈される前提」で比較する必要があります。

システム内で JST と UTC が混在しているような場合、
「絶対的な瞬間」として範囲内判定したいなら InstantZonedDateTime を使う方が安全です。

Instant の範囲内判定

import java.time.Instant;

public class InstantRangeUtils {

    public static boolean isWithinInclusive(Instant target,
                                            Instant startInclusive,
                                            Instant endInclusive) {
        if (endInclusive.isBefore(startInclusive)) {
            throw new IllegalArgumentException("終了は開始以降である必要があります");
        }

        boolean notBeforeStart = !target.isBefore(startInclusive);
        boolean notAfterEnd    = !target.isAfter(endInclusive);
        return notBeforeStart && notAfterEnd;
    }
}
Java

Instant は「世界共通の瞬間」を表すので、
タイムゾーンをまたいだ比較や、ログの時刻範囲チェックなどに向いています。


「範囲オブジェクト」を作ってしまう設計

Range クラスで「意味」を閉じ込める

毎回「start」「end」「inclusive か exclusive か」をバラバラに扱うと、
どこかで必ず混乱します。

そこで、「範囲そのもの」を表す小さなクラスを作ってしまうのも良い設計です。

import java.time.LocalDate;

public class DateRange {

    private final LocalDate startInclusive;
    private final LocalDate endExclusive; // 半開区間に統一

    public DateRange(LocalDate startInclusive, LocalDate endExclusive) {
        if (!endExclusive.isAfter(startInclusive)) {
            throw new IllegalArgumentException("終了日は開始日より後である必要があります");
        }
        this.startInclusive = startInclusive;
        this.endExclusive = endExclusive;
    }

    public boolean contains(LocalDate target) {
        boolean notBeforeStart = !target.isBefore(startInclusive);
        boolean beforeEnd      = target.isBefore(endExclusive);
        return notBeforeStart && beforeEnd;
    }

    public LocalDate getStartInclusive() {
        return startInclusive;
    }

    public LocalDate getEndExclusive() {
        return endExclusive;
    }
}
Java

使い方の例です。

DateRange march = new DateRange(
        LocalDate.of(2025, 3, 1),
        LocalDate.of(2025, 4, 1)
);

System.out.println(march.contains(LocalDate.of(2025, 3, 15))); // true
System.out.println(march.contains(LocalDate.of(2025, 4, 1)));  // false
Java

ここで深掘りしたいのは、
「範囲の意味(開始含む・終了含まない)をクラスに閉じ込める」ことです。

呼び出し側は「contains かどうか」だけを気にすればよく、
境界の細かい条件式を毎回書かなくて済みます。


業務でよくある「範囲内判定」の具体例

キャンペーン期間内かどうか

public class Campaign {

    private final DateRange period;

    public Campaign(DateRange period) {
        this.period = period;
    }

    public boolean isActiveOn(LocalDate date) {
        return period.contains(date);
    }
}
Java

これなら、業務コード側は

if (campaign.isActiveOn(today)) {
    // キャンペーン適用
}
Java

と書くだけで、「期間内かどうか」を自然な日本語に近い形で表現できます。

メンテナンス時間帯かどうか(日時版)

import java.time.LocalDateTime;

public class DateTimeRange {

    private final LocalDateTime startInclusive;
    private final LocalDateTime endExclusive;

    public DateTimeRange(LocalDateTime startInclusive, LocalDateTime endExclusive) {
        if (!endExclusive.isAfter(startInclusive)) {
            throw new IllegalArgumentException("終了日時は開始日時より後である必要があります");
        }
        this.startInclusive = startInclusive;
        this.endExclusive = endExclusive;
    }

    public boolean contains(LocalDateTime target) {
        boolean notBeforeStart = !target.isBefore(startInclusive);
        boolean beforeEnd      = target.isBefore(endExclusive);
        return notBeforeStart && beforeEnd;
    }
}
Java

API の入口などで、

if (maintenanceRange.contains(now)) {
    // メンテナンス中なのでエラーを返す
}
Java

のように使えます。


セキュリティ・運用の観点から見た「範囲内判定」

境界条件のバグはそのまま障害になる

範囲内判定で一番怖いのは、「境界の1日(1秒)」を間違えることです。

キャンペーン終了日を含めるつもりが含めていない。
有効期限が切れているのに、まだ有効と判定してしまう。
メンテナンス終了時刻を過ぎているのに、まだメンテナンス中扱いになっている。

これらは、売上・契約・SLA・信頼性に直結します。
だからこそ、「範囲内判定をユーティリティにまとめる」「閉区間/半開区間を統一する」ことが非常に重要です。

開始・終了の逆転を必ず検出する

開始 > 終了 の期間をそのまま受け入れてしまうと、
「絶対に true にならない範囲」「無限ループの原因」など、地味に危険な状態になります。

ユーティリティや Range クラスのコンストラクタで、
「終了が開始より前なら例外を投げる」
というチェックを必ず入れておくことで、
設定ミスやバグを早期に検出できます。


まとめ:範囲内判定ユーティリティで身につけてほしい感覚

範囲内判定は、「開始」「終了」「対象」の関係をきちんと表現するだけのシンプルな処理です。
でも、その中に大事なポイントが詰まっています。

以上・以下は !isBefore / !isAfter で表現する。
閉区間(開始・終了を含む)か半開区間(開始含む・終了含まない)かを、プロジェクトとして統一する。
開始・終了の逆転は必ずチェックして例外にする。
必要なら「範囲オブジェクト(Range クラス)」を作り、contains で意味を表現する。
日付・日時・Instant など、「何を基準に範囲を見るか」を意識して型を選ぶ。

もしあなたのコードの中に、
if (d.compareTo(start) >= 0 && d.compareTo(end) <= 0)」のような、
パッと見て意味が分かりにくい条件があれば、
それを一度「範囲内判定ユーティリティ」や「Range クラス」に置き換えられないか眺めてみてください。

それだけで、コードの意図がはっきりし、
境界条件のバグも減り、“業務で長く運用できる”日付・時間まわりの設計に近づきます。

タイトルとURLをコピーしました