日付書式にマッチさせる(1)

『詳説 正規表現』は一通り読みきれた(一部端折った章あり:「3章 正規表現の種類と機能」「7章 Perl」「8章 Java」)かな?ってところで,

正規表現ハンドブック (Technical Handbook Series)

正規表現ハンドブック (Technical Handbook Series)

を読んでみました.これは『詳説 正規表現』を読んだ後だとあっという間に読めます.逆に読んでなかったら,この本だけではマスターできなかったでしょう.どうも自分はハンドブック系は向いてないみたいです.
読んでて,ちょっと気になる例がありました.

日付書式にマッチさせる
^(19|[2-9][0-9])[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$

こんな表現も,いまの自分にはへっちゃらです.せっかくなので,逐語的文字列リテラルを使い,コメントもいれてみましょう.

@"
^  # 文字列の先頭
(19|[2-9][0-9])[0-9]{2}  # 年の部分.19xx または 2000 以降 9999 まで
/
(0[1-9]|1[0-2])  # 月の部分.01 から 12
/
(0[1-9]|[12][0-9]|3[01])  # 日の部分.01 から 31
$  # 文字列の終端
"

(この文字列を使ってRegexオブジェクトを作成するときは,

Regex r = new Regex(@"(上記コメント付き文字列)",RegexOptions.IgnorePatternWhitespace);

でOK)


気になるのは日の指定です.これだと04/31や02/30もマッチしてしまいます.もちろんそういったこと細かいチェックは正規表現とは別の形で行なう(正規表現を入力チェックに使うとした場合,その後の論理チェックとして業務ロジック内で行なう)というのも1つの考え方ですが,もうちょっと頑張れないかな?と思いました.
月日については簡単になんとかなりそうですが,うるう年のことを考えると途端にめんどくさくなりますよね.でもなんとかクリアできたっぽいので,ちょっとさらしてみようかと.(都合により区切り文字を-に変更)

@"(?=^\d{4}-\d{2}-\d{2}$)(?(^(?:\d{2}(?:0[48]|[2468][048])|(?:[13579][26]))|(?:(?:[02468][048]|[13579][26])00)-)(?:\d{4}-(?:(?:1[02]|0[13578])-(?:3[01]|[12][0-9]|0[1-9]))|\d{4}-(?:(?:11|0[469])-(?:30|[12][0-9]|0[1-9]))|\d{4}-(?:02-(?:2[0-9]|1[0-9]|0[1-9])))|(?:\d{4}-(?:(?:1[02]|0[13578])-(?:3[01]|[12][0-9]|0[1-9]))|\d{4}-(?:(?:11|0[469])-(?:30|[12][0-9]|0[1-9]))|\d{4}-(?:02-(?:2[0-8]|1[0-9]|0[1-9]))))"

月日の正規表現

月日の部分は書籍の形から若干アレンジすれば出来ます.要は月の部分を大の月と小の月で場合分けし,さらに2月で場合分けすればうまくいきます.
うるう年の場合:

(?: 
  \d{4}-(?:(?:1[02]|0[13578])-(?:3[01]|[12][0-9]|0[1-9]))
|
  \d{4}-(?:(?:11|0[469])-(?:30|[12][0-9]|0[1-9]))
|
  \d{4}-(?:02-(?:2[0-9]|1[0-9]|0[1-9]))
)

うるう年以外の場合:

(?: 
  \d{4}-(?:(?:1[02]|0[13578])-(?:3[01]|[12][0-9]|0[1-9]))
|
  \d{4}-(?:(?:11|0[469])-(?:30|[12][0-9]|0[1-9]))
|
  \d{4}-(?:02-(?:2[0-8]|1[0-9]|0[1-9]))
)

?:はカッコの中をキャプチャしない指定.今回の場合キャプチャはどうでもいいんですが,若干性能がよくなるという噂があるので付けてみました.ここまでは問題ありませんよね?
問題なのは,年の部分でうるう年判定をどうするかと,それぞれの場合で正規表現を使い分ける方法(if 〜 then 〜 else 〜)でしょう.ここは次回説明します.