1. 序章

正規表現は、適切に使用すると、さまざまな種類のパターンを照合するための強力なツールです。

この記事では、 java .util.regex パッケージを使用して、特定のStringに有効な日付が含まれているかどうかを判断します。

正規表現の概要については、Java正規表現APIのガイドを参照してください。

2. 日付形式の概要

国際グレゴリオ暦に関連して有効な日付を定義します。 私たちのフォーマットは一般的なパターンに従います:YYYY-MM-DD。

2月29日の日を含む年であるleap年の概念も含めましょう。 グレゴリオ暦によると、100で割り切れるものを除いて、400で割り切れるものを含めて、年数を4で均等に割ることができる場合、うるう年と呼びます。

他のすべての場合年を通常と呼びます。

有効な日付の例:

  • 2017-12-31
  • 2020-02-29
  • 2400-02-29

無効な日付の例:

  • 2017/12/31 :トークンの区切り文字が正しくありません
  • 2018-1-1 :先行ゼロがありません
  • 2018-04-31 :4月の日数が間違っています
  • 2100-02-29 :値を 100 で割ったため、今年は飛躍しないため、2月は28日間に制限されます

3. ソリューションの実装

正規表現を使用して日付を照合するので、最初に、単一のmatchsメソッドを提供するインターフェースDateMatcherをスケッチします。

public interface DateMatcher {
    boolean matches(String date);
}

以下に実装を段階的に示し、最後に完全なソリューションに向けて構築します。

3.1. ブロードフォーマットとのマッチング

まず、マッチャーのフォーマット制約を処理する非常に単純なプロトタイプを作成します。

class FormattedDateMatcher implements DateMatcher {

    private static Pattern DATE_PATTERN = Pattern.compile(
      "^\\d{4}-\\d{2}-\\d{2}$");

    @Override
    public boolean matches(String date) {
        return DATE_PATTERN.matcher(date).matches();
    }
}

ここでは、有効な日付がダッシュで区切られた3つの整数グループで構成されている必要があることを指定しています。最初のグループは4つの整数で構成され、残りの2つのグループはそれぞれ2つの整数を持ちます。

一致する日付: 2017-12-31 2018-01-31 0000-00-00 1029-99-72

一致しない日付: 2018-01 2018-01-XX 2020/02/29

3.2. 特定の日付形式との一致

2番目の例では、日付トークンの範囲とフォーマットの制約を受け入れます。 簡単にするために、関心を1900〜2999年に制限しました。

一般的な日付形式との一致に成功したので、それをさらに制限する必要があります。日付が実際に正しいことを確認するには、次のようにします。

^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$

ここでは、一致する必要がある整数範囲の3つのグループを紹介しました。

  • (19|2[0-9])[0-9]{2} 19または2Xで始まり、その後に2桁の数字が続く数字を照合することにより、制限された年の範囲をカバーします。
  • 0[1-9]|1[012] 01-12の範囲の月番号に一致します
  • 0[1-9]|[12][0-9]|3[01] 01-31の範囲の日番号に一致します

一致する日付: 1900-01-01 2205-02-31 2999-12-31

一致しない日付: 1899-12-31 2018-05-35 2018-13-05 3000-01-01 [ X97X]、 2018-01-XX

3.3. 2月29日とのマッチング

うるう年を正しく一致させるには、最初にうるう年に遭遇したことを識別し、次に2月29日をそれらの年の有効な日付として受け入れるようにする必要があります。

制限された範囲のうるう年の数は十分に大きいため、適切な分割可能性ルールを使用してそれらをフィルタリングする必要があります。

  • 数値の最後の2桁で形成される数値が4で割り切れる場合、元の数値は4で割り切れる
  • 数値の下2桁が00の場合、数値は100で割り切れます。

解決策は次のとおりです。

^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$

パターンは次の部分で構成されています。

  • 2000|2400|2800 1900-2999の制限された範囲で400の仕切りを持つうるう年のセットに一致します
  • 19|2[0-9](0[48]|[2468][048]|[13579][26])) 4 の仕切りがあり、 100 の仕切りがない、すべてのホワイトリストの年の組み合わせに一致します。
  • -02-29 2月2日に一致

一致する日付: 2020-02-29 2024-02-29 2400-02-29

一致しない日付: 2019-02-29 2100-02-29 3200-02-29 2020/02/29 [ X97X]

3.4. 2月のマッチング一般日

うるう年の2月29日と一致するだけでなく、すべての年の2月の他のすべての日(1〜28)と一致する必要があります

^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$

一致する日付: 2018-02-01 2019-02-13 2020-02-25

一致しない日付: 2000-02-30 2400-02-62 2018/02/28

3.5. 31日間の月のマッチング

1月、3月、5月、7月、8月、10月、12月は、1〜31日間一致する必要があります。

^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$

一致する日付: 2018-01-31 2021-07-31 2022-08-31

一致しない日付: 2018-01-32 2019-03-64 2018/01/31

3.6. 30日間の月のマッチング

4月、6月、9月、11月は、1〜30日間一致する必要があります。

^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$

マッチング日: 2018-04-30 2019-06-30 2020-09-30

一致しない日付: 2018-04-31 2019-06-31 2018/04/30

3.7. グレゴリオ暦の日付マッチャー

これで、上記のすべてのパターンを組み合わせて単一のマッチャーにして、すべての制約を満たす完全なGregorianDateMatcherを作成できます。

class GregorianDateMatcher implements DateMatcher {

    private static Pattern DATE_PATTERN = Pattern.compile(
      "^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$" 
      + "|^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$"
      + "|^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$" 
      + "|^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$");

    @Override
    public boolean matches(String date) {
        return DATE_PATTERN.matcher(date).matches();
    }
}

代替文字「|」を使用しました 4つのブランチの少なくとも1つに一致します。 したがって、2月の有効日は、うるう年の2月29日の最初のブランチまたは1から28までの任意の日の2番目のブランチのいずれかに一致します。 残りの月の日付は、3番目と4番目のブランチと一致します。

読みやすさを向上させるためにこのパターンを最適化していないため、自由に長さを試してみてください。

この時点で、すべての制約を満たしました。最初に紹介しました。

3.8. パフォーマンスに関する注意

複雑な正規表現の解析は、実行フローのパフォーマンスに大きな影響を与える可能性があります。この記事の主な目的は、すべての可能な日付のセットで文字列のメンバーシップをテストする効率的な方法を学ぶことではありませんでした。

日付を検証するための信頼性の高い高速なアプローチが必要な場合は、Java8が提供する LocalDate.parse()の使用を検討してください。

4. 結論

この記事では、形式、範囲、および月の長さのルールを提供することにより、グレゴリオ暦の厳密に形式化された日付と一致させるための正規表現の使用方法を学習しました。

この記事で紹介するすべてのコードは、Githubから入手できます。 これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。