JavaScript 正規表現で日付文字列が正しいのか評価し年・月・日を取り出す(うるう年を考慮)

正規表現を使って日付文字列を判定(閏年判定もも含む)

日付文字列が正しいかどうか正規表現で判別することが正しいのかは分からないが、プログラムが簡潔に書ける(?)
そのついでに年・月・日を取り出したい。ググったが見つけられない。
そう言うときは、結局として諦めるか自分で悩まなければならない。
しかも yyyy年mm月dd日 とか、yyyy-mm-dd とか、 yyyy/mm/dd には対応させたかった。
なので対応してみた。
先読み否定を3回も行っているし、無駄も多い感じ。さてどうやって短くしようか?
それにしても閏年の判定も正規表現で出きるとは!
先達の人たちは、すごいな。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
<body>
<script>

let str = '2400-2-29';
let reg = /^(?:(?!(?:[02468][1235679]|[13579][01345789])00[年\-\/]0?2[月\/\-](?:29|30)日?))(?:(?![0-9]{2}(?:[02468][1235679]|[13579][01345789])[年\-\/]0?2[月\/\-](?:29|30)日?))(?:(?![0-9]{4}[年\-\/](?:0?2|0?4|0?6|0?9|11)[月\/\-]31日?))([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/;
let ymd;

let date = (ymd = reg.exec(str)) ? new Date (ymd[1], ymd[2]-1, ymd[3]): null; //parseInt 

</script>

肯定先読みをして年月日の文字を省略したらちょっとだけ短くなった

/^(?=[0-9]{4}[年\-\/](?:0?[1-9]|1[0-2])[月\/\-](?:0?[1-9]|[12][0-9]|3[01])日?$)(?!\d{4}\D(?:(?:0?2|0?4|0?6|0?9|11)\D31|0?2\D30)\D?$)(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?$)(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?$)(\d+)\D(\d+)\D(\d+)\D?$/

さらに短くなった(検証はしていない)

もう全角数字なんて気にしなくても・・・
全部\dでよいのかも

/^(?!\d{4}\D(?:(?:0?2|0?4|0?6|0?9|11)\D31|0?2\D30)\D?$)(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?$)(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?$)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/

さらにさらに2文字短くなった(検証はしていない)

/^(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?$)(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?$)(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?$)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/

さらにさらにさらに、3文字短くなった(検証はしていない)

/^(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?)(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?)(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/

さらに9文字短くなった

/^(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?)(?!(?:(?:[02468][1235679]|[13579][01345789])00|\d{2}(?:[02468][1235679]|[13579][01345789]))\D0?2\D29\D?)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/

年月日の数字の間に空白文字を含んでも可能にする場合

/^\s*(?!\d{4}\D*(?:(?:0?(?:2|4|6|9)|11)\D*31|0?2\D*30)\D*)(?!(?:(?:[02468][1235679]|[13579][01345789])00|\d{2}(?:\s*[02468][1235679]|[13579][01345789]))\D*0?2\D29\D*)([0-9]{4})\s*[年\-\/]\s*(0?[1-9]|1[0-2])\s*[月\/\-]\s*(0?[1-9]|[12][0-9]|3[01])\s*日?\s*$/;

閏年を考慮しない日付文字列チェックの正規表現

あくまでも練習用に書いた。
肯定先読みをして大まかな書式でチェックして、その次に否定(31日のある月を否定、2月は30日も否定)をして残りを抽出。
なので2月の29日を許容する

/^(?=[0-9]{4}[年\-\/](?:0?[1-9]|1[0-2])[月\/\-](?:0?[1-9]|[12][0-9]|3[01])日?$)(?!\d{4}\D(?:(?:0?2|0?4|0?6|0?9|11)\D31|0?2\D30)\D?$)(\d+)\D(\d+)\D(\d+)\D?$/

閏年を考慮しない日付文字列チェックの最小(最短)の正規表現

なんで”最小の正規表現”としたかというと、ググられたとき検索に引っかかりやすいとおもうから(w)

/^(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/

正規表現を文字列の合成として表現するのなら

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
<body>
<script>

{
  const
    delimiter         = '\\-\\/',
    zero              = '0?',
    delimiter_year    = '[' + '年' + delimiter + ']',
    delimiter_month   = '[' + '月' + delimiter + ']',
    delimiter_day     = '日?',

    deny_leap_year    = '(?:[02468][1235679]|[13579][01345789])',//閏年を否定する
    year_long_cycle   = deny_leap_year + '00',
    year_short_cycle  = '[0-9]{2}' + deny_leap_year,

    year              = '[0-9]{4}',
    month             = zero + '[1-9]' + '|' + '1[0-2]',
    day               = zero + '[1-9]' + '|' + '[12][0-9]' + '|' + '3[01]', 
    
    deny_month_31     = '0?2|0?4|0?6|0?9|11',
    
    deny_last_day_31  = zero + '(?:' + deny_month_31 + ')' + delimiter_month + '31',
    deny_last_day_30  = zero + '2' + delimiter_month + '30',
    deny_last_day_29  = zero + '2' + delimiter_month + '29',
    
    
    //(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?)
    deny_29_long_cycle =
      '(?!' + year_long_cycle + delimiter_year + deny_last_day_29 +')',
    
    //(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?)
    deny_29_short_cycle =
      '(?!'+ year_short_cycle + delimiter_year + deny_last_day_29 +')', 
    

    //(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?)
    deny_last_day =
      '(?!'+  year + delimiter_year +
      '(?:'+ deny_last_day_31 +'|'+ deny_last_day_30 +')'+ delimiter_day + ')',
                    
    
    // ([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?
    pickup_date =
      '('+ year  +')'+ delimiter_year +
      '('+ month +')'+ delimiter_month +
      '('+ day   +')'+ delimiter_day;

    this.is_date_reg =
      new RegExp (
        '^' +
        deny_29_long_cycle +
        deny_29_short_cycle +
        deny_last_day +
        pickup_date +
        '$'
      );

}
            
console.log (is_date_reg.exec('2400-2-29'));
</script>

正しい日付文字列なら Date オブジェクトを返す関数

function str2date (str = '') {
  let
    reg = /^(?!\d{4}\D(?:(?:0?(?:2|4|6|9)|11)\D31|0?2\D30)\D?)(?!(?:[02468][1235679]|[13579][01345789])00\D0?2\D29\D?)(?!\d{2}(?:[02468][1235679]|[13579][01345789])\D0?2\D29\D?)([0-9]{4})[年\-\/](0?[1-9]|1[0-2])[月\/\-](0?[1-9]|[12][0-9]|3[01])日?$/;
    a = reg.exec (str);
  return a ? new Date (parseInt (a[1], 10), parseInt (a[2], 10) - 1, parseInt (a[3], 10)): null;
}

ついでに時間文字列も

文字列が時間文字列かどうか判別して、時・分・秒・ミリ秒を取り出す
" 12 : 34 : 56 . 7890123 "
" 12 時 34 分 56 秒 7890123 "
=> [ string , 12, 34, 56, 789 ]を返す
中抜けの数字は許さない
ミリ秒は3桁まで有効
"秒" の後の "." は許さない

/^\s*(?:([01]?[0-9]|2[0-4])(?:\s*[:時]\s*(?:([0-5]?[0-9])(?:\s*[:分]\s*(?:([0-5]?[0-9])(?:\s*[秒\.]\s*(?:(\d{1,3})\d*)?)?)?)?)?)?)?\s*$/

おまけ

/^\s*(?!\d{4}\D*(?:(?:0?(?:2|4|6|9)|11)\D*31|0?2\D*30)\D*)(?!(?:(?:[02468][1235679]|[13579][01345789])00|\d{2}(?:\s*[02468][1235679]|[13579][01345789]))\D*0?2\D29\D*)([0-9]{4})\s*[年\-\/]\s*(0?[1-9]|1[0-2])\s*[月\/\-]\s*(0?[1-9]|[12][0-9]|3[01])\s*日?\s*[ Tt]\s*([01]?[0-9]|2[0-4])(?:\s*[:時]\s*(?:([0-5]?[0-9])(?:\s*[:分]\s*(?:([0-5]?[0-9])(?:\s*[秒\.:]\s*(?:(\d{1,3})\d*\s*(?:ms)?)?)?)?)?)?)?$/