Java Tips | 文字列処理:日付形式チェック

Java Java
スポンサーリンク

日付形式チェックは「“存在しない日付”を早めにはじく」ための技

業務システムでは、日付入力は定番中の定番です。
生年月日、締め日、請求日、支払日、予約日、納期…。

ここで何もチェックしないと、
2024-13-01(13月)、2024-02-30(2月30日)、abcd-ef-gh みたいな値が平気でDBに入ります。
後で集計や帳票出力で落ちたり、「なんでこんな日付が入ってるの?」と未来の自分が苦しみます。

日付形式チェックの役割は、まさにここです。
「この文字列は“日付としてパースしてよさそうか”を事前に判定し、
おかしなものは早めにはじく」ためのフィルタだと思ってください。


まずは「どんな書式の日付を許すか」を決める

“フォーマット”を決めないと、チェックは始まらない

数値と同じで、日付も「どんな形で入力してもらうか」を先に決める必要があります。

2024-02-03 のような yyyy-MM-dd 形式にするのか。
2024/02/03 のようなスラッシュ区切りにするのか。
20240203 のような8桁数字にするのか。

フォーマットが決まらないと、チェックのしようがありません。

ここでは、実務でよく使われるこの3つを例にします。

yyyy-MM-dd(例:2024-02-03)
yyyy/MM/dd(例:2024/02/03)
yyyyMMdd(例:20240203)

そして、Java 8 以降で使える java.time パッケージを前提に話を進めます。


基本:LocalDate と DateTimeFormatter でチェックする

「パースできるかどうか」で形式と存在チェックを同時にやる

日付形式チェックで一番大事なポイントは、
「文字列の形だけでなく、“実在する日付かどうか”まで見たい」 ということです。

2024-02-30 は、\\d{4}-\\d{2}-\\d{2} という形には合っていますが、
実在しない日付です。
これを通してしまうと、後で必ず困ります。

そこで使うのが LocalDate.parseDateTimeFormatter です。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;

public final class DateValidator {

    private DateValidator() {}

    private static final DateTimeFormatter YMD_DASH =
            DateTimeFormatter.ofPattern("uuuu-MM-dd")
                             .withResolverStyle(ResolverStyle.STRICT);

    public static boolean isValidYmdDash(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            LocalDate.parse(text, YMD_DASH);
            return true;
        } catch (DateTimeParseException e) {
            return false;
        }
    }
}
Java

使い方はこうなります。

System.out.println(DateValidator.isValidYmdDash("2024-02-03")); // true
System.out.println(DateValidator.isValidYmdDash("2024-02-29")); // true(うるう年)
System.out.println(DateValidator.isValidYmdDash("2023-02-29")); // false(存在しない日付)
System.out.println(DateValidator.isValidYmdDash("2024-13-01")); // false(13月)
System.out.println(DateValidator.isValidYmdDash("2024-2-3"));   // false(0埋め必須)
System.out.println(DateValidator.isValidYmdDash("abcd-ef-gh")); // false
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「ResolverStyle.STRICT を使って“厳密な日付チェック”をしている」ことです。
STRICT にすることで、
2024-02-30 のような存在しない日付や、
2024-2-3 のようなフォーマット違いをきちんと弾いてくれます。

二つ目は、「uuuu を使っている」ことです。
yyyy でも動きますが、uuuujava.time で推奨される“年”の表現です。
(細かい歴史的理由はありますが、「java.time では uuuu を使う」と覚えておけばOKです。)

三つ目は、「例外を使って“パースできるかどうか”で判定している」ことです。
LocalDate.parse が成功すれば「形式もOK・日付としてもOK」、
DateTimeParseException が出れば「どこかがおかしい」と判断できます。


スラッシュ区切り yyyy/MM/dd に対応する

フォーマット違いはフォーマッタを変えるだけ

yyyy/MM/dd 形式も、ほぼ同じ書き方でチェックできます。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;

public final class DateValidator {

    private DateValidator() {}

    private static final DateTimeFormatter YMD_SLASH =
            DateTimeFormatter.ofPattern("uuuu/MM/dd")
                             .withResolverStyle(ResolverStyle.STRICT);

    public static boolean isValidYmdSlash(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            LocalDate.parse(text, YMD_SLASH);
            return true;
        } catch (DateTimeParseException e) {
            return false;
        }
    }
}
Java

使い方はこうです。

System.out.println(DateValidator.isValidYmdSlash("2024/02/03")); // true
System.out.println(DateValidator.isValidYmdSlash("2024/2/3"));   // false(0埋め必須)
System.out.println(DateValidator.isValidYmdSlash("2024/13/01")); // false
Java

ここでのポイントは、「フォーマットごとにフォーマッタを用意しておくと、コードが読みやすい」ことです。
YMD_DASHYMD_SLASH のように名前を付けておくと、
「このメソッドはどんな形式を期待しているのか」が一目で分かります。


8桁数字 yyyyMMdd に対応する

システム間連携やCSVでよくある形式

システム間連携やCSVでは、20240203 のような8桁数字で日付を扱うことも多いです。

これも同じようにチェックできます。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;

public final class DateValidator {

    private DateValidator() {}

    private static final DateTimeFormatter YMD_COMPACT =
            DateTimeFormatter.ofPattern("uuuuMMdd")
                             .withResolverStyle(ResolverStyle.STRICT);

    public static boolean isValidYmdCompact(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            LocalDate.parse(text, YMD_COMPACT);
            return true;
        } catch (DateTimeParseException e) {
            return false;
        }
    }
}
Java

使い方はこうです。

System.out.println(DateValidator.isValidYmdCompact("20240203")); // true
System.out.println(DateValidator.isValidYmdCompact("20240230")); // false
System.out.println(DateValidator.isValidYmdCompact("2024-02-03"));// false
Java

ここでのポイントは、「“見た目は単なる8桁数字”でも、LocalDate にパースすることで“存在チェック”までできる」ことです。
単に \\d{8} だけ見ていると、20240230 も通ってしまいますが、
LocalDate.parse を通すことで、そこまで含めてチェックできます。


「形式チェック」と「範囲チェック」は分けて考える

未来日・過去日を制限したい場合

日付形式チェックは、「その文字列が日付として成立しているか」を見るだけです。
「未来日はNG」「過去10年以内だけOK」といったビジネスルールは、
別のレイヤーで見るべき話 です。

例えば、「生年月日は今日より未来はNG」というチェックは、こう書けます。

import java.time.LocalDate;

public final class BirthDateValidator {

    private BirthDateValidator() {}

    public static boolean isValidBirthDate(String text) {
        if (!DateValidator.isValidYmdDash(text)) {
            return false;
        }
        LocalDate date = LocalDate.parse(text, DateValidator.YMD_DASH);
        return !date.isAfter(LocalDate.now());
    }
}
Java

YMD_DASH を外から使えるように public にするか、
parseYmdDash のようなメソッドを用意してもよいです。)

ここでの重要ポイントは、「形式チェックに通ったあとで、安心して LocalDate として扱っている」ことです。
isValidYmdDash が true なら、LocalDate.parse で例外は出ない前提で書けます。


正規表現だけで日付をチェックしようとしない

「形だけ合っていても、存在しない日付」は必ず出てくる

初心者がやりがちな失敗が、
「正規表現だけで日付をチェックしようとする」ことです。

例えば、こんなパターンを書きたくなります。

// yyyy-MM-dd っぽいもの
"^\\d{4}-\\d{2}-\\d{2}$"
Java

これは「形」としては合っていますが、
2024-13-012024-02-30 も通ってしまいます。

これを正規表現だけで完全に防ごうとすると、
「月ごとに日数が違う」「うるう年」などを全部パターンに埋め込むことになり、
ほぼ読めない怪物になります。

日付は、正規表現ではなく“日付ライブラリに任せる”のが正解です。
LocalDate.parseResolverStyle.STRICT を使えば、
「形」と「存在」の両方を、シンプルなコードでチェックできます。


例題:入力フォームでの日付形式チェック

例えば、「締め日」を yyyy-MM-dd 形式で入力してもらうフォームを考えます。

サーバ側では、こんな感じで使えます。

String closingDateText = request.getParameter("closingDate");

if (closingDateText == null || closingDateText.isBlank()) {
    errors.add("締め日を入力してください。");
} else if (!DateValidator.isValidYmdDash(closingDateText.trim())) {
    errors.add("締め日は yyyy-MM-dd 形式で正しい日付を入力してください。(例:2024-02-03)");
} else {
    LocalDate closingDate = LocalDate.parse(closingDateText.trim(), DateValidator.YMD_DASH);
    // ここから先は LocalDate として安全に扱える
}
Java

ここでのポイントは三つです。

一つ目は、「未入力チェック」と「形式チェック」を分けている」ことです。
「入力してください」と「形式が正しくありません」は、ユーザーに伝えたい内容が違うので、
条件もメッセージも分けて書いたほうが親切です。

二つ目は、「形式チェックに通ったあとで、安心して LocalDate にパースしている」ことです。
これにより、後続の処理は「日付として正しいものだけ」を前提に書けます。

三つ目は、「trim() をどこでかけるかを決めている」ことです。
入力値の前後の空白を許すかどうかはポリシー次第ですが、
許すなら、チェック前に trim() しておくのが定番です。


まとめ:日付形式チェックユーティリティで身につけたい感覚

日付形式チェックは、「形だけ見る」のではなく、
「その文字列が“実在する日付としてパースできるか”を見るフィルタ」です。

押さえておきたいのは、

どのフォーマットを許すかを先に決める
LocalDate.parseDateTimeFormatterResolverStyle.STRICT
形式と存在を同時にチェックする
ビジネスルール(未来日NGなど)は、形式チェックとは別に書く

という3つの感覚です。

もしあなたのコードのどこかに、

LocalDate date = LocalDate.parse(request.getParameter("date"));
Java

のような行がそのまま書かれていたら、
そこを題材にして、ここで作った DateValidator.isValidYmdDash
isValidYmdSlash を一度挟んでみてください。

それだけで、「存在しない日付がシステムに入り込む」「パース時に例外で落ちる」といった事故を、かなり減らすことができます。

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