記憶力が無い

プログラミングと室内園芸と何か

bashで日付のバリデーション

仕事で書いてるときに思いついたものをメモ。

日付のバリデーション

bashで日付の文字列のバリデーションについて考えます。 日付のフォーマットが yyyy-MM-dd (4桁西暦-2桁月-2桁日) 形式になっていることを確認したいです。

# OK/NGの例

# OK
date1='2020-06-01'

# NG
date2='2020/06/01'

# NG
date3='2020-06-01 20:00:00'

# NG
date4='2020-06-00'

# NG (6月は30日まで)
date5='2020-06-31'

正規表現

安直ですが、次のような正規表現チェックを実装してみました。

while read dt; do
  if [[ $dt =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
    echo "$dt is ...OK"
  else
    echo "$dt is ...NOT OK"
  fi
done <<EOS
2020-06-01
2020/06/01
2020-06-01 20:00:00
2020-06-00
2020-06-31
EOS

これを実行すると次のようになります。

2020-06-01 is ...OK
2020/06/01 is ...NOT OK
2020-06-01 20:00:00 is ...NOT OK
2020-06-00 is ...OK
2020-06-31 is ...OK

3つ目まではうまく判定できてますが、残りの2つはうまくいっていません。 正規表現が、数字の桁数しかチェックしていないからです。

では、次の例ではどうでしょうか。

while read dt; do
  if [[ $dt =~ ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$ ]]; then
    echo "$dt is ...OK"
  else
    echo "$dt is ...NOT OK"
  fi
done <<EOS
2020-06-01
2020/06/01
2020-06-01 20:00:00
2020-06-00
2020-06-31
EOS

結果は次のようになりました。

2020-06-01 is ...OK
2020/06/01 is ...NOT OK
2020-06-01 20:00:00 is ...NOT OK
2020-06-00 is ...NOT OK
2020-06-31 is ...OK

この正規表現では、0日や32日、13月などの明らかにあり得ないようなものは除外できるようになりましたが、 2020-06-31 のように、組み合わせ的にあり得ない日付を除外することができません。

うるう年の問題

月の日数は 28, 29, 30, 31 の4通りしかないので、4つの正規表現を並べることで組み合わせ的にあり得ない日付も除外できそうに思えます。 しかし、うるう年のことを考えると、正規表現だけで実現するのは難しいでしょう。

では、より厳密な日付のバリデーションをするにはどうしたらよいでしょうか。

dateコマンド

日付のことは日付の専門家 date コマンドにまかせればいいじゃない!ということで、日付のバリデーションにdateコマンド*1を使ってみます。

while read dt; do
  if date --date="$dt"; then
    echo "$dt is ...OK"
  else
    echo "$dt is ...NOT OK"
  fi
done <<EOS
2020-06-01
2020/06/01
2020-06-01 20:00:00
2020-06-00
2020-06-31
1900-02-29
EOS

結果は次のようになりました。

2020年 6月  1日 月曜日 00:00:00
2020-06-01 is ...OK
2020年 6月  1日 月曜日 00:00:00
2020/06/01 is ...OK
2020年 6月  1日 月曜日 20:00:00
2020-06-01 20:00:00 is ...OK
date: invalid date ‘2020-06-00’
2020-06-00 is ...NOT OK
date: invalid date ‘2020-06-31’
2020-06-31 is ...NOT OK
date: invalid date ‘1900-02-29’
1900-02-29 is ...NOT OK

※標準エラー出力ものまま表示しています。

2020-06-31 のほかに、一見するとうるう日に見えそうな 1900-02-29 もちゃんと除外できています*2。 一方で、今度は 2020/06/01 のような日付としては問題ないものの、 yyyy-MM-dd のフォーマットを満たしていないものが除外されなくなりました。

フォーマットのチェックに、上で示したような正規表現のチェックを用いても良いのですが、もうちょっとスマートにやってみましょう。

while read dt; do
  if [[ $dt = $(date --date="$dt" +%F) ]]; then
    echo "$dt is ...OK"
  else
    echo "$dt is ...NOT OK"
  fi
done <<EOS
2020-06-01
2020/06/01
2020-06-01 20:00:00
2020-06-00
2020-06-31
1900-02-29
EOS

dateコマンドは +%F オプションで日付を yyyy-MM-dd 形式に変換できるので、これで変換したものが元と一致するかをチェックするという寸法です。

結果は次のようになりました。

2020-06-01 is ...OK
2020/06/01 is ...NOT OK
2020-06-01 20:00:00 is ...NOT OK
date: invalid date ‘2020-06-00’
2020-06-00 is ...NOT OK
date: invalid date ‘2020-06-31’
2020-06-31 is ...NOT OK
date: invalid date ‘1900-02-29’
1900-02-29 is ...NOT OK

2020/06/01 もちゃんと除外できてますね。 もっと良いやり方があればコメントください。

*1:GNU coreutils版を使ってます

*2:1900年は4の倍数だが、うるう年じゃない。

Copyright © 2017 ttk1