「範囲内判定」とは何をしたいユーティリティか
業務システムでは、ものすごく頻繁に
「この日付はキャンペーン期間内か?」
「この日時はメンテナンス時間帯に含まれるか?」
「この日付は契約の有効期間に入っているか?」
といった“範囲内かどうか”の判定が出てきます。
毎回 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 が混在しているような場合、
「絶対的な瞬間」として範囲内判定したいなら Instant や ZonedDateTime を使う方が安全です。
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;
}
}
JavaInstant は「世界共通の瞬間」を表すので、
タイムゾーンをまたいだ比較や、ログの時刻範囲チェックなどに向いています。
「範囲オブジェクト」を作ってしまう設計
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;
}
}
JavaAPI の入口などで、
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 クラス」に置き換えられないか眺めてみてください。
それだけで、コードの意図がはっきりし、
境界条件のバグも減り、“業務で長く運用できる”日付・時間まわりの設計に近づきます。
