jQueryを使用して 閏年判定付き 生年月日フォーム をつくってみた

# 1. はじめに

個人的に、何らかサイトの会員登録画面で生年月日を入力するとき、
存在しない月日（例えば 9月31日、2月30日、4月31日 など) が選択できてしまうことが 割とよくあります。

システム的にも、ユーザー的にも、存在しない月日は入力できない方が良いはずです。

さらに、閏年は2月29日までありますし、
今回は、その点も含めて 存在しない年月日を入力できないように セレクトボックスの選択肢を制御する方法を調べてみました。


# 2. 準備

作成にあたって、以下のツールを準備します。

- テキストエディター
- ブラウザ （ここではChromeを使用）

また、ライブラリーに jQueryを使用します。
jQueryのサイトからファイルの読み込み方を確認します。

- jQuery　
 https://code.jquery.com/

# 3. 作成

作成した生年月日フォームのhtml・jsファイルの全体像とブラウザ画面を示します。

## 3-1. コーディング

以下に作成した html(main.html) と javascript(form.js) を示します。

main.htmlでは、jQueryライブラリーと"js/form.js"を呼び出しています。

```main.html
<!DOCTYPE html>
<html lang="ja">
<head>
    
    <title>Form</title>
    
    <script
    src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
    integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
    crossorigin="anonymous"></script>
    <script type="text/javascript" src="js/form.js"></script>
    
</head>
<body>
        <li>
            <label for="birth">年月日： </label>
            <select id="year">
                <option value="0">----</option>
            </select> 年
            <select id="month">
                <option value="0">--</option>
            </select> 月
            <select id="date">
                <option value="0">--</option>
            </select> 日
        </li>
    
</body>
    

```

main.htmlで呼び出している、form.jsの内容は以下の通りです。

```form.js
$(function() {
    // 現在の年月日を取得
    var time = new Date();
    var year = time.getFullYear();
    var month = time.getMonth() + 1;
    var date = time.getDate();

    // 選択された年月日を取得
    var selected_year = document.getElementById("year").value;
    var selected_month = document.getElementById("month").value;

    // 年(初期): 1900〜現在の年 の値を設定
    for (var i = year; i >= 1900 ; i--) {
        $('#year').append('<option value="' + i + '">' + i + '</option>');
    }

    // 月(初期): 1~12 の値を設定
    for (var j = 1; j <= 12; j++) {
        $('#month').append('<option value="' + j + '">' + j + '</option>');
    }

    // 日(初期): 1~31 の値を設定
    for (var k = 1; k <= 31; k++) {
        $('#date').append('<option value="' + k + '">' + k + '</option>');
    }

    // 月(変更)：選択された年に合わせて、適した月の値を選択肢にセットする
    $('#year').change(function() {
        selected_year = $('#year').val();

        // 現在の年が選択された場合、月の選択肢は 1~現在の月 に設定
        // それ以外の場合、1~12 に設定
        var last_month = 12;
        if (selected_year == year) {
            last_month = month;
        }
        $('#month').children('option').remove();
        $('#month').append('<option value="' + 0 + '">--</option>');
        for (var n = 1; n <= last_month; n++) {
            $('#month').append('<option value="' + n + '">' + n + '</option>');
        }
    });

    // 日(変更)：選択された年・月に合わせて、適した日の値を選択肢にセットする
    $('#year,#month').change(function() {
        selected_year = $('#year').val();
        selected_month = $('#month').val();
        
        // 現在の年・月が選択された場合、日の選択肢は 1~現在の日付 に設定
        // それ以外の場合、各月ごとの最終日を判定し、1~最終日 に設定
        if (selected_year == year && selected_month == month ) {
            var last_date = date;
        }else{
            // 2月：日の選択肢は1~28日に設定
            // ※ ただし、閏年の場合は29日に設定
            if (selected_month == 2) {
                if((Math.floor(selected_year%4 == 0)) && (Math.floor(selected_year%100 != 0)) || (Math.floor(selected_year%400 == 0))){
                    last_date = 29;
                }else{
                    last_date = 28;
                }

            // 4, 6, 9, 11月：日の選択肢は1~30日に設定
            }else if(selected_month == 4 || selected_month == 6 || selected_month == 9 || selected_month == 11 ){
                last_date = 30;

            // 1, 3, 5, 7, 8, 10, 12月：日の選択肢は1~31日に設定
            }else{
                last_date = 31;
            }
        }
        
        $('#date').children('option').remove();
        $('#date').append('<option value="' + 0 + '">--</option>');
        for (var m = 1; m <= last_date; m++) {
            $('#date').append('<option value="' + m + '">' + m + '</option>');
        }
    });

});

```


## 3-2. 画面

ブラウザで表示される画面を示します。

![スクリーンショット 2019-11-20 17.43.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174250/041f8a93-d268-817c-cfe7-89f313040bfa.png)


**例1）** 現在の年月日が 2019年11月20日として、ユーザーが 「年」に"2019"、「月」に"11" を選択した場合、日には選択肢として*"1~20"* が表示されます。
(2019年11月21日~2019年11月30日 は選択できない。）

![スクリーンショット 2019-11-20 17.45.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174250/f7821e3d-c809-81b5-ecc6-9ba096058702.png)


**例2)** ユーザーが、「年」に"2016"、「月」に"2" を選択した場合、「日」には選択肢として*"1~29"* が設定されます。


![スクリーンショット 2019-11-20 17.43.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174250/f9dd5aee-8a4d-7d07-327e-2c1729014255.png)






<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="haruka0121" data-slug-hash="ZEEwZpB" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Form">
  <span>See the Pen <a href="https://codepen.io/haruka0121/pen/ZEEwZpB">
  Form</a> by Haruka Ogawa (<a href="https://codepen.io/haruka0121">@haruka0121</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

# 4. 解説

form.js の処理内容を解説します。

## 4-1. 現在の年月日取得

セレクトボックスで 未来の年月日を選択できないように処理するため、
まず *現在の年月日*を取得しておく必要があります。

**new Date()**で現在の日付オブジェクトを生成し、
そこから *現在の年月日*の値をそれぞれ取得して、変数 *year*、*month*、*date*に入力します。

```javascript

    // 現在の年月日を取得
    var time = new Date();
    var year = time.getFullYear();
    var month = time.getMonth() + 1;
    var date = time.getDate();
```

### ・日付オブジェクト 生成

**new Date()**を使用し、日付オブジェクトを生成します。
引数に何も指定しない場合、現在の日付オブジェクトが生成されます。

```javascript
new Date();
```
今回、生成した日付オブジェクトは、変数*time*に入力します。

### ・現在の年 取得

**getFullYear()** を使用し、指定した日付オブジェクトの「年」を取得します。

構文は以下の通りです。

```javascript
日付オブジェクト.getFullYear();
```

今回は、*日付オブジェクト*として 変数*time* を指定します。

### ・現在の月 取得

**getMonth()** を使用し、指定した日付オブジェクトの「月」を取得します。
※ **getMonth()** は *0~11*で返されるため、値に +1 して使用します。

構文は以下の通りです。

```javascript
// 0から11の値で返される
日付オブジェクト.getMonth() 
```
今回は、*日付オブジェクト*として 変数*time* を指定します。

### ・現在の日 取得

**getDate()** を使用し、指定した日付オブジェクトの「日」を取得します。

構文は以下の通りです。

```javascript
日付オブジェクト.getDate()
```

今回は、*日付オブジェクト*として 変数*time* を指定します。

## 4-2. 選択された年月日取得

セレクトボックスで 存在しない日 や 未来の年月日 を選択できないようにするため、
「年」・「月」セレクトボックスで ユーザーが選択した値によって、「月」・「日」セレクトボックスの選択肢を設定します。

まず ここでは、「年」・「月」セレクトボックスで ユーザーが選択した値を取得します。

**getElementById**を使用し、ユーザーが選択した「年」・「月」の値を取得し、
それぞれ変数 *selected_year*、*selected_month*に代入します。

``` javascript
    // 選択された年月日を取得
    var selected_year = document.getElementById("year").value;
    var selected_month = document.getElementById("month").value;
```

引数には、htmlで設定した「年」・「月」セレクトボックスのid名(*"year"・"month"*)を指定します。

構文は以下の通りです。

```javascript
document.getElementById(id).value;
```

## 4-3. 年月日の選択肢設定（初期）

フォーム画面が表示された時点（初期）での、「年」「月」「日」セレクトボックスの選択肢を設定します。

### ・「年」の選択肢設定(初期)

「年」セレクトボックスは、*1900~現在の年* の値を選択肢とします。
*現在の年* は、変数 *year* に代入してあります。

```javascript

    // 年(初期): 1900〜現在の年 を設定
    for (var i = year; i >= 1900 ; i--) {
        $('#year').append('<option value="' + i + '">' + i + '</option>');
    }
```

ループ処理を用いて、*1900 ~ year* の値を「年」の選択肢として生成します。

生成した値は、**append()**を用いて、「年」セレクトボックスに挿入します。

```javascript
$(セレクタ).append(挿入コンテンツ);
```
ここでは、*セレクタ*として htmlで指定した「年」セレクトボックスの id名(*"year"*)を指定します

### ・「月」の選択肢設定(初期)

「月」セレクトボックスは、*1~12* の値を選択肢とします。

```javascript

    // 月(初期): 1~12 の値を設定
    for (var j = 1; j <= 12; j++) {
        $('#month').append('<option value="' + j + '">' + j + '</option>');
    }
```

ループ処理を用いて、*1~12* の値を「月」の選択肢として生成します。

生成した値は、**append()**を用いて、「月」セレクトボックスに挿入します。
*セレクタ*として htmlで指定した「月」セレクトボックスの id名(*"month"*)を指定します


### ・「日」の選択肢設定(初期)

「日」セレクトボックスは、*1~31* の値を選択肢とします。

```javascript
    // 日(初期): 1~31 の値を設定
    for (var k = 1; k <= 31; k++) {
        $('#date').append('<option value="' + k + '">' + k + '</option>');
    }
```

ループ処理を用いて、*1~31* の値を「日」の選択肢として生成します。

生成した値は、**append()**を用いて、「日」セレクトボックスに挿入します。
*セレクタ*として htmlで指定した「日」セレクトボックスの id名(*"date"*)を指定します


## 4-4. 月日の選択肢 変更

### ・「月」の選択肢 変更

未来の年月日が選択されないように、月の選択肢を変更します。

例えば、現在が2019年11月21日の場合、
ユーザーが「年」セレクトボックスで*2019*を選択したとき、「月」セレクトボックスの選択肢に *12* が表示されないようにします。

```javascript
    // 月(変更)：選択された年に合わせて、適した月の値を選択肢にセットする
    $('#year').change(function() {
        // 選択された年の値を取得する
        selected_year = $('#year').val();

        // 現在の年が選択された場合、月の選択肢は 1~現在の月 に設定
        // それ以外の場合、1~12 に設定
        var last_month = 12;
        if (selected_year == year) {
            last_month = month;
        }

        $('#month').children('option').remove();
        $('#month').append('<option value="' + 0 + '"></option>');

        for (var n = 1; n <= last_month; n++) {
            $('#month').append('<option value="' + n + '">' + n + '</option>');
        }
    });

```

- **change()**：「月」セレクトボックス 選択肢 変更処理 実行

**change()**を用いて、
「年」セレクトボックスで値が選択された時に、
「月」セレクトボックス 選択肢 変更処理が実行されるように設定します。

```javascript
$(セレクタ).change(ファンクション)
```
ここでは、*セレクタ*として htmlで指定した「年」セレクトボックスの id名(*"year"*)を指定します。

*ファンクション*には、「月」セレクトボックスの選択肢を変更する処理を記述します。



- **val()** ：「年」セレクトボックスの選択された値取得

**val()**を用いて、
選択された「年」の値を取得します。

```javascript
$(セレクタ).val() 
```

ここでは、*セレクタ*として htmlで指定した「年」セレクトボックスの id名(*"year"*)を指定します。


- 「月」選択肢 の値範囲 決定

条件処理を用いて、
生成する「月」セレクトボックスの選択肢の値の範囲を決定します。

「年」セレクトボックスで 
*現在の年* が選択された場合、「月」セレクトボックスの選択肢は *1 ~ 現在の月* 、
それ以外の場合、「月」セレクトボックスの選択肢は *1~12* 
に設定します。

- **remove()** ：「月」セレクトボックスの選択肢 削除

**remove()**を用いて、
設定されている「月」セレクトボックスの選択肢の値を削除し、初期化します。

```javascript
$(セレクタ).remove();
```

- **append()** ：「月」セレクトボックス 未選択時 の値 挿入

**append()**を用いて、
「月」セレクトボックス 未選択時 の値 を挿入します。

```javascript
$(セレクタ).append(挿入コンテンツ);
```
ここでは、*セレクタ*として htmlで指定した「月」セレクトボックスの id名(*"month"*)を指定します。

*挿入コンテンツ*には、「月」セレクトボックス未選択時 の値として*"--"*が 表示されるように設定します。

- 「月」セレクトボックス の選択肢の値 生成・変更

ループ処理を用いて、「月」の選択肢の値を生成します。
生成した値は**append()**を用いて、「月」セレクトボックスに挿入します。

### ・「日」の選択肢 変更

存在しない年月日が選択されないように、「日」セレクトボックスの選択肢を変更します。

具体的には、
現在の年月日を確認し、未来の年月日が選択できないよう、「日」セレクトボックスの選択肢を変更します。
さらに、閏年や各月の正しい最終日を考慮し、存在しない日(*9月31日*、*2月30日*、*4月31日* など )も選択できないように処理します。

```javascript
    // 日(変更)：選択された年・月に合わせて、適した日の値を選択肢にセットする
    $('#year,#month').change(function() {
        selected_year = $('#year').val();
        selected_month = $('#month').val();
        
        // 現在の年・月が選択された場合、日の選択肢は 1~現在の日付 に設定
        // それ以外の場合、各月ごとの最終日を判定し、1~最終日 に設定
        if (selected_year == year && selected_month == month ) {
            var last_date = date;
        }else{
            // 2月：日の選択肢は1~28日に設定
            // ※ ただし、閏年の場合は29日に設定
            if (selected_month == 2) {
                if((Math.floor(selected_year%4 == 0)) && (Math.floor(selected_year%100 != 0)) || (Math.floor(selected_year%400 == 0))){
                    last_date = 29;
                }else{
                    last_date = 28;
                }

            // 4, 6, 9, 11月：日の選択肢は1~30日に設定
            }else if(selected_month == 4 || selected_month == 6 || selected_month == 9 || selected_month == 11 ){
                last_date = 30;

            // 1, 3, 5, 7, 8, 10, 12月：日の選択肢は1~31日に設定
            }else{
                last_date = 31;
            }
        }
        
        $('#date').children('option').remove();
        $('#date').append('<option value="' + 0 + '">--</option>');

        for (var m = 1; m <= last_date; m++) {
            $('#date').append('<option value="' + m + '">' + m + '</option>');
        }
    });
```


- **change()**：「日」セレクトボックス 選択肢 変更処理 実行

**change()**を用いて、
「年」・「月」セレクトボックスで値が選択された時に、
選択肢 変更処理が実行されるように設定します。

```javascript
$(セレクタ).change(ファンクション)
```
ここでは、*セレクタ*として htmlで指定した「年」・「月」セレクトボックスの id名（"year", "month")を指定します。

*ファンクション*には、「日」セレクトボックスの選択肢を変更する処理を記述します。


- **val()** ：「年」・「月」セレクトボックスの選択された値取得

**val()**を用いて、
選択された「年」・「月」の値を取得します。

```javascript
$(セレクタ).val() 
```

ここでは、*セレクタ*として htmlで指定した「年」・「月」セレクトボックスの id名（"year", "month")を指定します。


- 「日」選択肢 の値範囲 決定

条件処理を用いて、
生成する「日」セレクトボックスの選択肢の値の範囲を決定します。

ここでは、「年」・「月」セレクトボックスで、
それぞれ *現在の年・月* が選択された場合、「日」セレクトボックスの選択肢は *1~現在の日付* 、
それ以外の場合、「月」セレクトボックスの選択肢は *1~月の最終日* 
に設定します。

*月の最終日*は、閏年や月で異なるため、
さらに細かく条件を分岐させて値の範囲を設定します。

選択された「月」の値が*2*月の場合、
「日」セレクトボックスの選択肢の範囲は 
閏年であれば*1~29*日、
閏年ではなければ*1~28*日に設定します。

※ 以下の場合、閏年と判定する。
・西暦を4で割り切れ
て、かつ100で割り切れない
・西暦を400で割り切れる

選択された「月」の値が*4, 6, 9, 11*月の場合、
「日」セレクトボックスの選択肢の範囲は *1~30*日に設定します。

選択された「月」の値が*1, 3, 5, 7, 8, 10, 12*月の場合、
「日」セレクトボックスの選択肢の範囲は *1~31*日に設定します。


- **remove()** ：「日」セレクトボックスの選択肢 削除

**remove()**を用いて、
設定されている「日」セレクトボックスの選択肢の値を削除し、初期化します。

```javascript
$(セレクタ).remove();
```
- **append()** ：「日」セレクトボックス 未選択時 の値 挿入

**append()**を用いて、
「日」セレクトボックス 未選択時 の値 を挿入します。

```javascript
$(セレクタ).append(挿入コンテンツ);
```
ここでは、*セレクタ*として htmlで指定した「日」セレクトボックスの id名(*"date"*)を指定します。

*挿入コンテンツ*には、「日」セレクトボックス未選択時 の値として*"--"*が 表示されるように設定します。

- 「日」セレクトボックス の選択肢の値 生成・変更

ループ処理を用いて、「日」の選択肢の値を生成します。
生成した値は**append()**を用いて、「日」セレクトボックスに挿入します。



# 5. おわりに

今回は、存在しない年月日が入力できない 閏年判定付き 生年月日フォームを作成しました。

jQueryを使用すると、簡単にhtml要素を変更できるので、生年月日フォーム以外でも活用できそうです。


## 追記

「年」の値が選択されたら「月」、
「年」・「月」の値が選択されたら「日」 の選択肢が再設定される処理の流れになっていますが、
ユーザーが「年」より先に「月」or「日」、または「月」より先に「日」の値を選択していた場合、
選択肢の再設定処理で、ユーザーの選択していた値が初期化されてしまいます。

もっと理想を言えば、再設定処理がされても、ユーザーが選択していた値を保持できれば 良いなと思います。


# 参考情報

・JavaScript 日本語リファレンス
http://js.studio-kingdom.com/

・nkmrkisk.com:【js】jQuery or jsで生年月日入力formを自動生成するサンプルコード
https://nkmrkisk.com/archives/1153

・yuya4の備忘録：【JavaScript】【jquery】セレクトボックスでのうるう年計算
http://yu-ya4.hatenablog.com/entry/2015/07/03/195409

・ITの隊長のブログ：【jQuery】年と月からうるう年も計算した日を出力する
https://www.aipacommander.com/entry/2015/08/11/155210

・jQuery CDN – Latest Stable Versions
https://code.jquery.com/