Java Tips | 日付・時間:日付範囲生成

Java Java
スポンサーリンク

日付範囲生成とは何をするユーティリティか

「今月の全日付を一覧にしたい」「レポート期間の開始日〜終了日を1日ずつ処理したい」「1週間分のデータを日ごとに集計したい」。
こういうときに必要になるのが「日付範囲生成」です。

日付範囲生成ユーティリティは、
「開始日」と「終了日」を渡すと、その間の LocalDate を順番に並べてくれる小さな道具です。
業務では、集計・レポート・バッチ処理・カレンダー画面など、あらゆるところで使われます。


Java で日付範囲を扱う基本クラス

LocalDate を日付の“基本単位”として使う

日付範囲生成では、LocalDate を使うのが基本です。
LocalDate は「年月日だけ(時間なし)」を表すクラスで、
「2025-03-26」のような“日付そのもの”を扱うのに向いています。

import java.time.LocalDate;

public class LocalDateBasic {

    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate specific = LocalDate.of(2025, 3, 26);

        System.out.println("今日       : " + today);
        System.out.println("特定の日付 : " + specific);
    }
}
Java

時間やタイムゾーンを気にせず、「日単位でループしたい」「日ごとに処理したい」というときは、
まず LocalDate を使う、と覚えておくと良いです。


一番基本的な日付範囲生成

開始日から終了日までを1日ずつ生成する

まずは、最もシンプルな「開始日〜終了日を1日ずつ列挙する」ユーティリティです。

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class DateRange {

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

        List<LocalDate> result = new ArrayList<>();
        LocalDate d = startInclusive;
        while (!d.isAfter(endInclusive)) {
            result.add(d);
            d = d.plusDays(1);
        }
        return result;
    }
}
Java

使い方の例です。

import java.time.LocalDate;
import java.util.List;

public class DateRangeExample {

    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2025, 3, 1);
        LocalDate end   = LocalDate.of(2025, 3, 5);

        List<LocalDate> days = DateRange.daysBetween(start, end);

        for (LocalDate d : days) {
            System.out.println(d);
        }
    }
}
Java

出力はこうなります。

2025-03-01
2025-03-02
2025-03-03
2025-03-04
2025-03-05

ここで重要なのは、
「開始日を含む」「終了日も含む」「1日ずつ plusDays(1) で進める」
という3点です。

この“含む/含まない”のルールをユーティリティ側で固定しておくと、
呼び出し側で迷わなくなります。


「開始日を含む/終了日を含む」を意識する

inclusive / exclusive の考え方

日付範囲には、よく次の2パターンがあります。

開始日を含み、終了日も含む(閉区間)
開始日を含み、終了日は含まない(半開区間)

例えば、レポート期間を「2025-03-01〜2025-03-31」と表示しつつ、
内部的には「2025-04-01 の手前まで」として扱う、という設計もよくあります。

半開区間のユーティリティはこう書けます。

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

    List<LocalDate> result = new ArrayList<>();
    LocalDate d = startInclusive;
    while (d.isBefore(endExclusive)) {
        result.add(d);
        d = d.plusDays(1);
    }
    return result;
}
Java

ここで深掘りしたいのは、
「範囲の定義をユーティリティで統一する」ことの大切さです。

プロジェクト内で「このユーティリティは開始含む・終了含む」「こっちは終了含まない」とバラバラだと、
バグの温床になります。

「日付範囲は基本的に半開区間で扱う」など、チームでルールを決めておくと安全です。


ストリームを使った日付範囲生成

Stream API でスマートに書く

Java 8 以降なら、Stream を使って日付範囲を生成することもできます。

import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class DateRangeStream {

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

        long days = java.time.temporal.ChronoUnit.DAYS.between(startInclusive, endInclusive) + 1;

        return Stream.iterate(startInclusive, d -> d.plusDays(1))
                     .limit(days)
                     .collect(Collectors.toList());
    }
}
Java

ここでのポイントは、
ChronoUnit.DAYS.between(start, end) で「日数差」を求め、
Stream.iterateplusDays(1) しながら limit していることです。

ループ版とやっていることは同じですが、
「日付の列を生成している」という意図がよりはっきり見えます。


業務でよく使う「月」「週」の範囲生成

ある月の全日付を生成する

「今月のカレンダーを表示したい」「月次レポートで日ごとの集計をしたい」といったときに使うパターンです。

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class MonthRange {

    public static List<LocalDate> daysOfMonth(int year, int month) {
        LocalDate first = LocalDate.of(year, month, 1);
        LocalDate last  = first.withDayOfMonth(first.lengthOfMonth());

        List<LocalDate> result = new ArrayList<>();
        LocalDate d = first;
        while (!d.isAfter(last)) {
            result.add(d);
            d = d.plusDays(1);
        }
        return result;
    }
}
Java

使い方の例です。

List<LocalDate> days = MonthRange.daysOfMonth(2025, 3);
days.forEach(System.out::println);
Java

ここで深掘りしたいのは、
lengthOfMonth() を使って「その月の最終日」を安全に求めている点です。

2月は28日だったり29日だったりしますが、
lengthOfMonth() を使えば、うるう年も含めて正しく扱えます。

ある週の全日付を生成する

「週次レポート」「週単位のカレンダー」などで使うパターンです。

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class WeekRange {

    public static List<LocalDate> weekOf(LocalDate anyDate, DayOfWeek firstDayOfWeek) {
        LocalDate start = anyDate;
        while (start.getDayOfWeek() != firstDayOfWeek) {
            start = start.minusDays(1);
        }

        List<LocalDate> result = new ArrayList<>();
        LocalDate d = start;
        for (int i = 0; i < 7; i++) {
            result.add(d);
            d = d.plusDays(1);
        }
        return result;
    }
}
Java

使い方の例です。

List<LocalDate> week = WeekRange.weekOf(LocalDate.of(2025, 3, 26), DayOfWeek.MONDAY);
week.forEach(System.out::println);
Java

ここでのポイントは、
「週の開始曜日(例えば月曜)」を引数で受け取っていることです。

国やシステムによって「週の始まり」が違うので、
ユーティリティ側で固定せず、DayOfWeek で指定できるようにしておくと柔軟です。


セキュリティ・運用の観点から見た日付範囲生成

範囲が“広すぎないか”を必ずチェックする

日付範囲生成は便利ですが、
「開始日:2000-01-01」「終了日:2100-12-31」のような指定をそのまま受け入れると、
36500日以上のリストを作ることになり、メモリや処理時間を食い尽くします。

実務では、例えば次のような制限をユーティリティに組み込むことが多いです。

最大でも 1年分(365日)まで
最大でも 3ヶ月分まで
それ以上は例外にする、あるいは警告ログを出す

こうした制限は、サービス妨害(DoS)的な使われ方や、誤設定による障害を防ぐうえで重要です。

タイムゾーンをまたぐ処理との境界を意識する

日付範囲生成自体は LocalDate で十分ですが、
「JST の日付範囲を UTC の日時に変換して処理する」
といったケースでは、ZonedDateTimeInstant との変換が絡みます。

例えば、「JST の 2025-03-01〜2025-03-31」を UTC の Instant 範囲に変換する場合は、
開始日 00:00 と終了日の翌日 00:00 を JST として ZonedDateTime にし、
それを Instant に変換して半開区間 [start, end) として扱う、などです。

日付範囲生成ユーティリティと、タイムゾーン変換ユーティリティをきちんと分けておくと、
境界のバグを減らせます。


まとめ:日付範囲生成で身につけてほしい感覚

日付範囲生成は、「開始日から終了日までを、日単位できれいに並べる」だけのシンプルな処理です。
でも、その中に大事なポイントがいくつもあります。

LocalDate を日付の基本単位として使う。
開始日・終了日を含むかどうか(閉区間/半開区間)をユーティリティで統一する。
plusDays(1) で1日ずつ進める、あるいは Stream.iterate で生成する。
月・週の範囲生成では lengthOfMonth()DayOfWeek を活用する。
範囲が広すぎないかをチェックし、誤設定やDoS的な利用を防ぐ。

もしあなたのコードのどこかで、
「for (int i = 0; i < 31; i++) { … }」のように“なんとなく日数を決め打ちしている”箇所があれば、
そこを一度、日付範囲生成ユーティリティに置き換えられないか眺めてみてください。

それだけで、コードの意図がはっきりし、
バグも減り、業務システムとしての信頼性が一段上がります。

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