'java.time.format.DateTimeParseException' の処理方法

JavaBeginner
オンラインで実践に進む

はじめに

Java アプリケーションで日付と時刻データを扱う際、開発者はしばしばjava.time.format.DateTimeParseExceptionに遭遇します。この例外は、アプリケーションが文字列を日付または時刻オブジェクトにパースしようとした際に、文字列のフォーマットが期待されるパターンと一致しない場合や、無効な値が含まれている場合に発生します。

この実験(Lab)では、DateTimeParseExceptionの原因を特定し、それを解決するための効果的なソリューションを実装し、Java アプリケーションで信頼性の高い日付と時刻のパースを行うためのベストプラクティスを採用する方法を学びます。

DateTimeParseException の理解

java.time.format.DateTimeParseExceptionは、Java アプリケーションで日付と時刻の文字列をパースする際に発生する一般的なランタイム例外です。この例外を効果的に処理するためには、その原因と特定方法を理解する必要があります。

DateTimeParseException の原因

DateTimeParseExceptionは、通常、以下のいずれかの理由で発生します。

  1. フォーマットの不一致: 入力文字列のフォーマットが、DateTimeFormatterで指定されたパターンと一致しない。
  2. 無効な日付/時刻の値: 入力文字列に、無効な日付または時刻(2 月 30 日など)を表す値が含まれている。
  3. 要素の欠落または余分な要素: 入力文字列に必要な要素が欠落しているか、予期しない追加要素が含まれている。

典型的なDateTimeParseExceptionのシナリオを実演するために、簡単な Java プログラムを作成しましょう。まず、WebIDE を開き、新しい Java ファイルを作成します。

  1. 左側のサイドバーで、ファイルエクスプローラーセクションに移動します。
  2. /home/labex/projectフォルダーを右クリックします。
  3. 「New File」を選択し、ParseExceptionDemo.javaという名前を付けます。

次に、以下のコードをコピーしてファイルに貼り付けます。

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

public class ParseExceptionDemo {
    public static void main(String[] args) {
        // Example 1: Format mismatch
        String dateStr1 = "2023/05/15";

        try {
            // Trying to parse with incorrect format (expects yyyy-MM-dd)
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDate date = LocalDate.parse(dateStr1, formatter);
            System.out.println("Parsed date: " + date);
        } catch (DateTimeParseException e) {
            System.out.println("Example 1 - Format mismatch error:");
            System.out.println("Exception: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
            System.out.println("Error position: " + e.getErrorIndex());
        }

        System.out.println("\n----------------------------------------\n");

        // Example 2: Invalid date value
        String dateStr2 = "2023-02-30";

        try {
            // February 30th is an invalid date
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDate date = LocalDate.parse(dateStr2, formatter);
            System.out.println("Parsed date: " + date);
        } catch (DateTimeParseException e) {
            System.out.println("Example 2 - Invalid date error:");
            System.out.println("Exception: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
            System.out.println("Input string: " + e.getParsedString());
        }
    }
}

このプログラムをコンパイルして実行するには、WebIDE でターミナルを開き、以下のコマンドを実行します。

cd ~/project
javac ParseExceptionDemo.java
java ParseExceptionDemo

次のような出力が表示されるはずです。

Example 1 - Format mismatch error:
Exception: java.time.format.DateTimeParseException
Message: Text '2023/05/15' could not be parsed at index 4
Error position: 4

----------------------------------------

Example 2 - Invalid date error:
Exception: java.time.format.DateTimeParseException
Message: Text '2023-02-30' could not be parsed: Invalid date 'February 30'
Input string: 2023-02-30

DateTimeParseException の分析

例外情報を分析してみましょう。

  1. 最初の例では、スラッシュ(2023/05/15)を含む日付を、ハイフン(yyyy-MM-dd)を期待するフォーマッタを使用してパースしようとしているため、例外が発生します。エラーインデックス 4 は、パースが失敗した最初のスラッシュ文字を指しています。

  2. 2 番目の例では、2 月 30 日はどの年にも有効な日付ではないため、例外が発生します。フォーマットは一致していますが、実際の日付の値が無効です。

DateTimeParseExceptionは、パースの問題を診断するのに役立つ情報を提供します。

  • エラーメッセージは、何が問題だったのかを説明します。
  • getParsedString()メソッドは、パースできなかった入力文字列を返します。
  • getErrorIndex()メソッドは、パースが失敗した位置を返します。

これらの詳細を理解することは、アプリケーションで日付パースの問題を効果的にデバッグし、解決するために不可欠です。

フォーマットの不一致の問題の解決

DateTimeParseExceptionの最も一般的な原因の 1 つは、入力文字列のフォーマットと、DateTimeFormatterが期待するフォーマットとの間の不一致です。このステップでは、この種の問題を解決する方法を学びます。

正しいフォーマットパターンの使用

DateTimeFormatterを使用する際には、入力文字列の実際のフォーマットと一致するパターンを指定することが重要です。Java は、日付と時刻を正確にフォーマットする方法を定義できる、柔軟なパターン構文を提供しています。

フォーマットの不一致の問題を解決する方法を実演するために、新しいファイルを作成しましょう。

  1. WebIDE で、/home/labex/projectフォルダーを右クリックします。
  2. 「New File」を選択し、FormatMismatchSolution.javaという名前を付けます。
  3. 以下のコードをコピーして貼り付けます。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class FormatMismatchSolution {
    public static void main(String[] args) {
        // Different date string formats
        String[] dateStrings = {
            "2023/05/15",  // format: yyyy/MM/dd
            "15-05-2023",  // format: dd-MM-yyyy
            "May 15, 2023" // format: MMMM d, yyyy
        };

        for (String dateStr : dateStrings) {
            parseWithCorrectFormatter(dateStr);
            System.out.println("----------------------------------------");
        }
    }

    private static void parseWithCorrectFormatter(String dateStr) {
        System.out.println("Trying to parse: " + dateStr);

        // Try with yyyy/MM/dd pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'yyyy/MM/dd': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'yyyy/MM/dd' pattern");
        }

        // Try with dd-MM-yyyy pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'dd-MM-yyyy': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'dd-MM-yyyy' pattern");
        }

        // Try with MMMM d, yyyy pattern
        try {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy");
            LocalDate date = LocalDate.parse(dateStr, formatter);
            System.out.println("Success using 'MMMM d, yyyy': " + date);
            return;
        } catch (DateTimeParseException e) {
            System.out.println("Failed with 'MMMM d, yyyy' pattern");
        }

        System.out.println("Could not parse date string with any of the available formatters");
    }
}

このプログラムをコンパイルして実行します。

cd ~/project
javac FormatMismatchSolution.java
java FormatMismatchSolution

次のような出力が表示されるはずです。

Trying to parse: 2023/05/15
Success using 'yyyy/MM/dd': 2023-05-15
----------------------------------------
Trying to parse: 15-05-2023
Failed with 'yyyy/MM/dd' pattern
Success using 'dd-MM-yyyy': 2023-05-15
----------------------------------------
Trying to parse: May 15, 2023
Failed with 'yyyy/MM/dd' pattern
Failed with 'dd-MM-yyyy' pattern
Success using 'MMMM d, yyyy': 2023-05-15
----------------------------------------

DateTimeFormatter パターンの理解

DateTimeFormatterは、日付または時刻のさまざまな部分を表すためにパターン文字を使用します。以下は、最も一般的なパターン文字の一部です。

  • y: 年(例:yyyyは 2023)
  • M: 月(例:MMは 05、MMMは May、MMMMは May)
  • d: 日(例:ddは 15)
  • H: 時(0-23)(例:HHは 14)
  • m: 分(例:mmは 30)
  • s: 秒(例:ssは 45)

これらのパターン文字を、リテラル(スラッシュ、ハイフン、スペース、コンマなど)と組み合わせて、日付文字列の正確なフォーマットに一致させることができます。

柔軟な日付パーサーの作成

次に、複数のフォーマットを自動的に処理できる、より柔軟な日付パーサーを作成しましょう。

  1. WebIDE で、FlexibleDateParser.javaという名前の新しいファイルを作成します。
  2. 以下のコードをコピーして貼り付けます。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;

public class FlexibleDateParser {
    // List of common date formats
    private static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
        DateTimeFormatter.ofPattern("yyyy-MM-dd"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd"),
        DateTimeFormatter.ofPattern("MM/dd/yyyy"),
        DateTimeFormatter.ofPattern("dd-MM-yyyy"),
        DateTimeFormatter.ofPattern("d MMM yyyy"),
        DateTimeFormatter.ofPattern("MMMM d, yyyy")
    );

    public static void main(String[] args) {
        // Test with different date formats
        String[] dates = {
            "2023-05-15",
            "2023/05/15",
            "05/15/2023",
            "15-05-2023",
            "15 May 2023",
            "May 15, 2023"
        };

        for (String dateStr : dates) {
            try {
                LocalDate date = parseDate(dateStr);
                System.out.println("Successfully parsed '" + dateStr + "' to: " + date);
            } catch (IllegalArgumentException e) {
                System.out.println("Failed to parse '" + dateStr + "': " + e.getMessage());
            }
        }
    }

    /**
     * Tries to parse a date string using multiple formatters
     * @param dateStr the date string to parse
     * @return the parsed LocalDate
     * @throws IllegalArgumentException if the string cannot be parsed with any formatter
     */
    public static LocalDate parseDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            throw new IllegalArgumentException("Date string cannot be null or empty");
        }

        for (DateTimeFormatter formatter : FORMATTERS) {
            try {
                // Try to parse with this formatter
                return LocalDate.parse(dateStr, formatter);
            } catch (DateTimeParseException e) {
                // This formatter didn't work, continue to the next one
                continue;
            }
        }

        // If we get here, none of the formatters worked
        throw new IllegalArgumentException("Cannot parse date: " + dateStr +
                                          ". Supported formats: yyyy-MM-dd, yyyy/MM/dd, " +
                                          "MM/dd/yyyy, dd-MM-yyyy, d MMM yyyy, MMMM d, yyyy");
    }
}

このプログラムをコンパイルして実行します。

cd ~/project
javac FlexibleDateParser.java
java FlexibleDateParser

次のような出力が表示されるはずです。

Successfully parsed '2023-05-15' to: 2023-05-15
Successfully parsed '2023/05/15' to: 2023-05-15
Successfully parsed '05/15/2023' to: 2023-05-15
Successfully parsed '15-05-2023' to: 2023-05-15
Successfully parsed '15 May 2023' to: 2023-05-15
Successfully parsed 'May 15, 2023' to: 2023-05-15

この柔軟な日付パーサーは、複数の日付フォーマットを処理できるため、さまざまなソースからの日付入力を扱う際に、アプリケーションをより堅牢にします。各フォーマッタを順番に試し、機能するものが見つかるか、すべての可能性を使い果たすまで試します。

未知または可変のフォーマットで日付をパースする必要がある場合は、このアプローチを使用することを忘れないでください。

無効な日付値の処理

DateTimeParseExceptionのもう 1 つの一般的な原因は、入力文字列に、2 月 30 日や 9 月 31 日など、論理的に無効な日付値が含まれている場合です。このステップでは、これらのケースを効果的に処理する方法を探ります。

無効な日付の問題点

日付文字列のフォーマットが指定されたパターンと一致していても、日付自体が無効な場合、パースは失敗します。これは、Java の日時 API が、有効な日付のみが作成されるように検証を実行するためです。

無効な日付の処理を試すために、新しいファイルを作成しましょう。

  1. WebIDE で、/home/labex/projectフォルダーを右クリックします。
  2. 「New File」を選択し、InvalidDateHandler.javaという名前を付けます。
  3. 以下のコードをコピーして貼り付けます。
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class InvalidDateHandler {
    public static void main(String[] args) {
        // Array of date strings - some valid, some invalid
        String[] dateStrings = {
            "2023-04-30", // Valid - April has 30 days
            "2023-04-31", // Invalid - April has only 30 days
            "2023-02-28", // Valid - February 2023 has 28 days
            "2023-02-29", // Invalid - 2023 is not a leap year
            "2024-02-29", // Valid - 2024 is a leap year
            "2023-13-01"  // Invalid - Month 13 doesn't exist
        };

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        System.out.println("Testing dates with simple parsing:");
        for (String dateStr : dateStrings) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                System.out.println("Valid date: " + dateStr + " => " + date);
            } catch (DateTimeParseException e) {
                System.out.println("Invalid date: " + dateStr + " => " + e.getMessage());
            }
        }

        System.out.println("\nTesting dates with manual validation:");
        for (String dateStr : dateStrings) {
            if (isValidDate(dateStr)) {
                try {
                    LocalDate date = LocalDate.parse(dateStr, formatter);
                    System.out.println("Valid date: " + dateStr + " => " + date);
                } catch (DateTimeParseException e) {
                    // This should not happen if isValidDate returns true
                    System.out.println("Unexpected error for " + dateStr + ": " + e.getMessage());
                }
            } else {
                System.out.println("Invalid date detected: " + dateStr);
            }
        }
    }

    /**
     * Validates a date string by checking if it represents a valid date.
     *
     * @param dateStr the date string in yyyy-MM-dd format
     * @return true if the date is valid, false otherwise
     */
    public static boolean isValidDate(String dateStr) {
        try {
            // Split the date string into components
            String[] parts = dateStr.split("-");
            if (parts.length != 3) {
                return false;
            }

            int year = Integer.parseInt(parts[0]);
            int month = Integer.parseInt(parts[1]);
            int day = Integer.parseInt(parts[2]);

            // Check basic ranges
            if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
                return false;
            }

            // Validate the date by trying to create it
            LocalDate.of(year, month, day);
            return true;
        } catch (DateTimeException | NumberFormatException e) {
            // DateTimeException is thrown for invalid dates
            // NumberFormatException is thrown if parts aren't numbers
            return false;
        }
    }
}

このプログラムをコンパイルして実行します。

cd ~/project
javac InvalidDateHandler.java
java InvalidDateHandler

次のような出力が表示されるはずです。

Testing dates with simple parsing:
Valid date: 2023-04-30 => 2023-04-30
Invalid date: 2023-04-31 => Text '2023-04-31' could not be parsed: Invalid date 'April 31'
Valid date: 2023-02-28 => 2023-02-28
Invalid date: 2023-02-29 => Text '2023-02-29' could not be parsed: Invalid date 'February 29' as '2023' is not a leap year
Valid date: 2024-02-29 => 2024-02-29
Invalid date: 2023-13-01 => Text '2023-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13

Testing dates with manual validation:
Valid date: 2023-04-30 => 2023-04-30
Invalid date detected: 2023-04-31
Valid date: 2023-02-28 => 2023-02-28
Invalid date detected: 2023-02-29
Valid date: 2024-02-29 => 2024-02-29
Invalid date detected: 2023-13-01

堅牢な日付パーサーの実装

次に、フォーマットの不一致と無効な日付の両方を適切に処理できる、より堅牢な日付パーサーを作成しましょう。前のステップの柔軟なフォーマット処理と、無効な日付の検証を組み合わせます。

  1. WebIDE で、RobustDateParser.javaという名前の新しいファイルを作成します。
  2. 以下のコードをコピーして貼り付けます。
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class RobustDateParser {
    // List of common date formats
    private static final List<DateTimeFormatter> FORMATTERS = Arrays.asList(
        DateTimeFormatter.ofPattern("yyyy-MM-dd"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd"),
        DateTimeFormatter.ofPattern("MM/dd/yyyy"),
        DateTimeFormatter.ofPattern("dd-MM-yyyy"),
        DateTimeFormatter.ofPattern("d MMM yyyy"),
        DateTimeFormatter.ofPattern("MMMM d, yyyy")
    );

    public static void main(String[] args) {
        // Test with various dates, including invalid ones
        List<String> testDates = Arrays.asList(
            "2023-05-15",    // valid, standard format
            "2023/05/15",    // valid, with slashes
            "05/15/2023",    // valid, US format
            "15-05-2023",    // valid, European format
            "15 May 2023",   // valid, with month name
            "May 15, 2023",  // valid, with month name first
            "2023-02-29",    // invalid: 2023 is not a leap year
            "2023-04-31",    // invalid: April has 30 days
            "2023-13-01",    // invalid: month 13 doesn't exist
            "random text",   // invalid format
            ""               // empty string
        );

        for (String dateStr : testDates) {
            Optional<LocalDate> result = parseDate(dateStr);
            if (result.isPresent()) {
                System.out.println("Successfully parsed '" + dateStr + "' to: " + result.get());
            } else {
                System.out.println("Could not parse '" + dateStr + "' - Invalid or unsupported format");
            }
        }
    }

    /**
     * Attempts to parse a date string using multiple formatters.
     * Returns an Optional containing the parsed date if successful,
     * or an empty Optional if parsing fails with all formatters.
     *
     * @param dateStr the date string to parse
     * @return an Optional containing the parsed LocalDate or empty if parsing fails
     */
    public static Optional<LocalDate> parseDate(String dateStr) {
        // Guard against null or empty input
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return Optional.empty();
        }

        List<String> errors = new ArrayList<>();

        // Try each formatter
        for (DateTimeFormatter formatter : FORMATTERS) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                return Optional.of(date);
            } catch (DateTimeParseException e) {
                errors.add("Failed with pattern " + formatter.toString() + ": " + e.getMessage());
            }
        }

        // If we get here, all formatters failed
        System.out.println("Debug - All formatters failed for '" + dateStr + "'");
        for (String error : errors) {
            System.out.println("  " + error);
        }

        return Optional.empty();
    }
}

このプログラムをコンパイルして実行します。

cd ~/project
javac RobustDateParser.java
java RobustDateParser

出力には、正常にパースできる日付文字列と、エラーが発生する日付文字列が表示されます。

Successfully parsed '2023-05-15' to: 2023-05-15
Successfully parsed '2023/05/15' to: 2023-05-15
Successfully parsed '05/15/2023' to: 2023-05-15
Successfully parsed '15-05-2023' to: 2023-05-15
Successfully parsed '15 May 2023' to: 2023-05-15
Successfully parsed 'May 15, 2023' to: 2023-05-15
Debug - All formatters failed for '2023-02-29'
  Failed with pattern ... Text '2023-02-29' could not be parsed: Invalid date 'February 29' as '2023' is not a leap year
  ...
Could not parse '2023-02-29' - Invalid or unsupported format
Debug - All formatters failed for '2023-04-31'
  ...
Could not parse '2023-04-31' - Invalid or unsupported format
Debug - All formatters failed for '2023-13-01'
  ...
Could not parse '2023-13-01' - Invalid or unsupported format
Debug - All formatters failed for 'random text'
  ...
Could not parse 'random text' - Invalid or unsupported format
Could not parse '' - Invalid or unsupported format

無効な日付を処理するための主要なテクニック

このソリューションでは、堅牢な日付パースのためにいくつかの重要なテクニックを使用しています。

  1. 複数のフォーマットのサポート: さまざまな入力フォーマットを処理するために、異なるフォーマッタでのパースを試みます。
  2. Optional の戻り値の型: Java のOptionalを使用して、パースが失敗した場合を明確に示します。
  3. 詳細なエラーロギング: デバッグ目的でエラー情報を収集します。
  4. 入力検証: パースを試みる前に、null または空の入力を確認します。
  5. 正常な失敗: 例外をスローするのではなく、空の Optional を返します。

これらのテクニックを実装することにより、アプリケーションは、無効な日付が含まれている可能性のあるユーザー入力や外部システムからのデータに対処する場合でも、より堅牢に日付パースを処理できます。

日時パースのベストプラクティスの実装

この最終ステップでは、日付と時刻の両方の値を処理するための業界のベストプラクティスを実装する、包括的な日時パーサーを作成します。また、DateTimeParseExceptionを回避および処理するために、プロの Java 開発者が使用する追加のテクニックについても探ります。

本番環境対応の日時パーサーの作成

これまで学んだすべてのベストプラクティスを組み込んだユーティリティクラスを作成しましょう。

  1. WebIDE で、/home/labex/projectフォルダーを右クリックします。
  2. 「New File」を選択し、DateTimeParserUtil.javaという名前を付けます。
  3. 以下のコードをコピーして貼り付けます。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

/**
 * Utility class for parsing date and time strings in a robust manner.
 */
public class DateTimeParserUtil {
    // Common date formatters with STRICT resolver for validation
    private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList(
        createFormatter("yyyy-MM-dd"),
        createFormatter("yyyy/MM/dd"),
        createFormatter("MM/dd/yyyy"),
        createFormatter("dd-MM-yyyy"),
        createFormatter("d MMM yyyy"),
        createFormatter("MMMM d, yyyy")
    );

    // Common time formatters
    private static final List<DateTimeFormatter> TIME_FORMATTERS = Arrays.asList(
        createFormatter("HH:mm:ss"),
        createFormatter("HH:mm"),
        createFormatter("h:mm a"),
        createFormatter("h:mm:ss a")
    );

    // Common date-time formatters
    private static final List<DateTimeFormatter> DATETIME_FORMATTERS = Arrays.asList(
        createFormatter("yyyy-MM-dd HH:mm:ss"),
        createFormatter("yyyy-MM-dd'T'HH:mm:ss"),
        createFormatter("yyyy-MM-dd HH:mm"),
        createFormatter("MM/dd/yyyy HH:mm:ss"),
        createFormatter("dd-MM-yyyy HH:mm:ss")
    );

    /**
     * Creates a DateTimeFormatter with strict resolver style.
     */
    private static DateTimeFormatter createFormatter(String pattern) {
        return new DateTimeFormatterBuilder()
                .appendPattern(pattern)
                .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
                .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
                .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
                .toFormatter()
                .withResolverStyle(ResolverStyle.STRICT)
                .withLocale(Locale.US);
    }

    /**
     * Parses a string into a LocalDate.
     *
     * @param dateStr the date string to parse
     * @return an Optional containing the parsed LocalDate, or empty if parsing fails
     */
    public static Optional<LocalDate> parseDate(String dateStr) {
        if (dateStr == null || dateStr.trim().isEmpty()) {
            return Optional.empty();
        }

        for (DateTimeFormatter formatter : DATE_FORMATTERS) {
            try {
                LocalDate date = LocalDate.parse(dateStr, formatter);
                return Optional.of(date);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        return Optional.empty();
    }

    /**
     * Parses a string into a LocalTime.
     *
     * @param timeStr the time string to parse
     * @return an Optional containing the parsed LocalTime, or empty if parsing fails
     */
    public static Optional<LocalTime> parseTime(String timeStr) {
        if (timeStr == null || timeStr.trim().isEmpty()) {
            return Optional.empty();
        }

        for (DateTimeFormatter formatter : TIME_FORMATTERS) {
            try {
                LocalTime time = LocalTime.parse(timeStr, formatter);
                return Optional.of(time);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        return Optional.empty();
    }

    /**
     * Parses a string into a LocalDateTime.
     *
     * @param dateTimeStr the date-time string to parse
     * @return an Optional containing the parsed LocalDateTime, or empty if parsing fails
     */
    public static Optional<LocalDateTime> parseDateTime(String dateTimeStr) {
        if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) {
            return Optional.empty();
        }

        // First try with combined date-time formatters
        for (DateTimeFormatter formatter : DATETIME_FORMATTERS) {
            try {
                LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter);
                return Optional.of(dateTime);
            } catch (DateTimeParseException e) {
                // Try next formatter
            }
        }

        // Then try to split into date and time parts
        String[] parts = dateTimeStr.split(" ", 2);
        if (parts.length == 2) {
            Optional<LocalDate> date = parseDate(parts[0]);
            Optional<LocalTime> time = parseTime(parts[1]);

            if (date.isPresent() && time.isPresent()) {
                return Optional.of(LocalDateTime.of(date.get(), time.get()));
            }
        }

        return Optional.empty();
    }

    /**
     * Attempts to parse a string as a date, time, or date-time value.
     *
     * @param input the string to parse
     * @return a string describing the parsed result or an error message
     */
    public static String parseAny(String input) {
        Optional<LocalDate> date = parseDate(input);
        if (date.isPresent()) {
            return "Parsed as date: " + date.get();
        }

        Optional<LocalTime> time = parseTime(input);
        if (time.isPresent()) {
            return "Parsed as time: " + time.get();
        }

        Optional<LocalDateTime> dateTime = parseDateTime(input);
        if (dateTime.isPresent()) {
            return "Parsed as date-time: " + dateTime.get();
        }

        return "Could not parse: " + input;
    }
}

次に、ユーティリティを実演するためのテストクラスを作成しましょう。

  1. WebIDE で、DateTimeParsingDemo.javaという名前の新しいファイルを作成します。
  2. 以下のコードをコピーして貼り付けます。
public class DateTimeParsingDemo {
    public static void main(String[] args) {
        // Test with various date, time, and date-time strings
        String[] inputs = {
            // Valid dates
            "2023-05-15",
            "05/15/2023",
            "15 May 2023",

            // Valid times
            "14:30:00",
            "2:30 PM",
            "14:30",

            // Valid date-times
            "2023-05-15 14:30:00",
            "2023-05-15T14:30:00",
            "05/15/2023 14:30:00",
            "15-05-2023 14:30:00",

            // Invalid examples
            "2023-02-30",
            "25:30:00",
            "2023-13-01",
            "Not a date or time",
            ""
        };

        for (String input : inputs) {
            String result = DateTimeParserUtil.parseAny(input);
            System.out.println("Input: \"" + input + "\" → " + result);
        }
    }
}

このプログラムをコンパイルして実行します。

cd ~/project
javac DateTimeParserUtil.java DateTimeParsingDemo.java
java DateTimeParsingDemo

次のような出力が表示されるはずです。

Input: "2023-05-15" → Parsed as date: 2023-05-15
Input: "05/15/2023" → Parsed as date: 2023-05-15
Input: "15 May 2023" → Parsed as date: 2023-05-15
Input: "14:30:00" → Parsed as time: 14:30
Input: "2:30 PM" → Parsed as time: 14:30
Input: "14:30" → Parsed as time: 14:30
Input: "2023-05-15 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "2023-05-15T14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "05/15/2023 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "15-05-2023 14:30:00" → Parsed as date-time: 2023-05-15T14:30
Input: "2023-02-30" → Could not parse: 2023-02-30
Input: "25:30:00" → Could not parse: 25:30:00
Input: "2023-13-01" → Could not parse: 2023-13-01
Input: "Not a date or time" → Could not parse: Not a date or time
Input: "" → Could not parse:

Java での日時パースのベストプラクティス

ユーティリティクラスに実装されている主要なベストプラクティスを見てみましょう。

1. ResolverStyle.STRICT設定を使用する

.withResolverStyle(ResolverStyle.STRICT)

STRICT リゾルバースタイルは、有効な日付と時刻のみが受け入れられるようにします。これにより、「2 月 31 日」を 3 月 3 日にパースするような問題を回避できます。

2. デフォルトのロケールを指定する

.withLocale(Locale.US)

常にロケールを指定して、特に月名と AM/PM インジケーターについて、一貫したパース動作を確保します。

3. Optional の戻り値の型を使用する

public static Optional<LocalDate> parseDate(String dateStr) {
    // ...
    return Optional.empty();
}

Optionalを使用すると、パースが失敗する可能性があることを明確に伝え、呼び出し元のコードでこのケースを明示的に処理する必要があります。

4. 複数のフォーマットをサポートする

private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList(
    createFormatter("yyyy-MM-dd"),
    createFormatter("yyyy/MM/dd"),
    // ...
);

複数のフォーマットをサポートすることで、さまざまな入力ソースを扱う際のパースロジックの堅牢性が向上します。

5. 欠落している時刻フィールドをデフォルトにする

.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)

欠落している時刻フィールドをデフォルトにすることで、特に異なる日時型間の変換を行う場合に、より柔軟なパースが可能になります。

6. 入力検証

if (dateStr == null || dateStr.trim().isEmpty()) {
    return Optional.empty();
}

不要な例外を回避するために、常にパースを試みる前に、入力を検証します。

7. 正常なエラー処理

try {
    LocalDate date = LocalDate.parse(dateStr, formatter);
    return Optional.of(date);
} catch (DateTimeParseException e) {
    // Try next formatter
}

例外を適切に処理し、パースが失敗した場合は明確なフィードバックを提供します。

8. スマートな複合パース

// Then try to split into date and time parts
String[] parts = dateTimeStr.split(" ", 2);
if (parts.length == 2) {
    Optional<LocalDate> date = parseDate(parts[0]);
    Optional<LocalTime> time = parseTime(parts[1]);

    if (date.isPresent() && time.isPresent()) {
        return Optional.of(LocalDateTime.of(date.get(), time.get()));
    }
}

複雑なパースタスクをより単純なコンポーネントに分割すると、多くの場合、より良い結果が得られます。

これらのベストプラクティスを実装することにより、さまざまな入力を適切に処理し、パースが失敗した場合は明確なフィードバックを提供する、堅牢な日付と時刻のパースロジックを作成できます。

まとめ

この実験では、Java アプリケーションでjava.time.format.DateTimeParseExceptionを効果的に処理する方法を学びました。以下の点で実践的な経験を積みました。

  1. DateTimeParseException の理解: フォーマットの不一致や無効な日付値など、この例外の一般的な原因について学びました。

  2. フォーマットの不一致の問題の解決: 複数の日付フォーマットを処理し、フォーマットの不一致を適切に処理できる柔軟な日付パーサーを実装しました。

  3. 無効な日付値の処理: 2 月 30 日や 4 月 31 日など、無効な日付値を検出して処理するための堅牢なソリューションを作成しました。

  4. ベストプラクティスの実装: 業界のベストプラクティスを組み込んだ包括的な日時パーサーユーティリティを構築しました。これには以下が含まれます。

    • STRICT リゾルバースタイルの使用
    • デフォルトのロケールの指定
    • Optional 値の返却
    • 複数のフォーマットのサポート
    • 入力の検証
    • 正常なエラー処理の実装
    • 複雑なパースタスクの分解

これらのプラクティスに従うことで、さまざまなソースからの日時データを扱う際に Java アプリケーションをより堅牢にし、予期しない例外の発生を減らし、ユーザーにより良いエクスペリエンスを提供できます。

効果的な日時パースは、多くのアプリケーション、特にユーザー入力、データインポート、または外部システムとの統合を扱うアプリケーションにとって不可欠であることを忘れないでください。この実験で学んだテクニックは、Java プロジェクトでこれらのシナリオをより効果的に処理するのに役立ちます。