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

昨日の続きです.

うるう年の正規表現

とりあえず年は4桁と仮定します.
うるう年とはどんなものか?はてなダイアリーKeywordにありました.

閏年
太陽暦では一年が366日ある年、2月29日のある年のことをいう。
ちなみに次の方法で判定が可能。

  1. 西暦が400で割り切れる年はうるう年である。
  2. 400で割り切れない場合、西暦が100で割り切れる年はうるう年ではない。
  3. 100で割り切れない場合、西暦が4で割り切れる年はうるう年である。
  4. 4で割り切れない場合、うるう年ではない。

100や400は置いておいて,4で割り切れるかに絞って考えることにしましょう.
まず,下2桁が4で割り切れれば,全体も4で割り切れます.これは大丈夫ですよね.上2桁部分は100の倍数になるわけで100は4で割り切れますから.というわけで下2桁のみで考えたとき,以下の法則(というと大げさですが)があるのが分かります.

  • 10の位が偶数のときは,1の位が0,4,8であれば4の倍数
  • 10の位が奇数のときは,1の位が2,6であれば4の倍数

数学的な証明はめんどくさいのでパス.ここまでを正規表現にするとこうなるでしょう.

\d{2}(?:([02468][048])|([13579][26]))

ここまでくると,100と400の場合も自然と導けます.これらは下2桁が00固定と考え,上2桁が4で割り切れればうるう年,割り切れなければうるう年でないってことになります.正規表現にするとこう.

(?:([02468][048])|([13579][26]))00

これをあわせれば,うるう年正規表現の出来上がりです.ただし最初の正規表現のうち,下2桁が00のものは除くことにしましょう.

(?:\d{2}(?:0[48]|[2468][048])|([13579][26]))|(?:(?:[02468][048]|[13579][26])00)

if 〜 then 〜 else 〜 構文

.NET Framework正規表現クラスライブラリには,if 〜 then 〜 else 〜 に相当する構文があります.

(?(expression)yes|no)

なので,expressionの部分にうるう年の正規表現を,yesの部分にうるう年の場合の月日の正規表現を,noの部分にうるう年以外の場合の月日の表現を適用します.

@"
(?(^(?:\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]))
    )
)
"

先読みによる形式チェック

ここまでで十分な気がしますが,少しパフォーマンスが気になります.選択が多いので,あきらかに誤った形式でも大量のトラックバックが発生してしまいそうです.まぁ文字列長が短いのでたかがしれてますが,それでも早めに刈り取れるのであれば刈り取っておきたいところです.
というわけで正規表現の先頭に先読み構文を入れておきます

@"
(?=^\d{4}-\d{2}-\d{2}$) # yyyy-MM-ddという形になっている
(?(^(?:\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]))
    )
)
"

こうしておけば,^\d{4}-\d{2}-\d{2}$ という形式でない時点で失敗してくれます.

さいごに

昨日・今日と書きながら,括弧の入り方が微妙だったりすることに気付いたので,あまり信用しないでくださいね.深読みしなくてよいです.それはたぶんバグですから(^^;;