HTMLのdate型inputは時差でずれる

# HTMLのdate型inputは時差でずれる
## 環境
+ OS: Ubuntu 20.04LTS, Windows10(20H2)  
+ ブラウザ: Chrome 88.0.4324.146, Firefox 85.0

## 要約
+ `<input type="date">`を使うときには、UTCとの時差修正が必要。日本にいると気づきにくい。
+ `.getTimezoneOffset()`は夏時間と冬時間で変わるので、時差の値を使い回してはならない。(2021/3/20追記)

## 現象
```html
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <title>input dateが正常に日付を扱うか</title>
</head>
<body>
<input type="date" id="today" value="2021-02-07">
</body>
<script>
    let picker = document.getElementById("today");
    console.log(`初期値: ${picker.valueAsDate}`);
    let day = new Date(2021, 2 - 1, 9);
    console.log(`代入値: ${day}`);
    // 代入
    picker.valueAsDate = day;
    let newDay = picker.valueAsDate
    console.log(`取得値: ${newDay}`);
    console.log(`日付: ${newDay.getDate()}`);
</script>
</html>
```

上記のHTMLファイルをブラウザで表示すると、
1. HTMLのbody内の記述により、日付入力欄が設置され、2021/2/7で初期化される。
2. 代入により日付入力欄の値は、2021/2/9になる。
3. 日付入力欄の値を取得すればその値は2021/2/9である。

と考えるところであるが、実際の日付欄とコンソール出力は以下のようになる。
![Screenshot from 2021-02-07 12-46-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602526/18b6b307-8f28-fea8-6383-57aac7c70e25.png)

```
初期値: Sun Feb 07 2021 09:00:00 GMT+0900 (日本標準時)
代入値: Tue Feb 09 2021 00:00:00 GMT+0900 (日本標準時)
取得値: Mon Feb 08 2021 09:00:00 GMT+0900 (日本標準時)
日付: 8
```

代入した値と、画面の表示が1日ずれているが、日付は画面の表示通りに取得される。日本国内であれば日付欄に値を代入せず、取得のみにしていれば問題には一応遭遇しない。

しかし、日付変更線の向こう側(アメリカとか)にタイムゾーンを移すと
![Screenshot from 2021-02-06 22-59-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602526/268ccd5c-b646-a9e2-0cfe-8a6afd36059f.png)

```
初期値: Sat Feb 06 2021 19:00:00 GMT-0500 (アメリカ東部標準時)
代入値: Tue Feb 09 2021 00:00:00 GMT-0500 (アメリカ東部標準時)
取得値: Mon Feb 08 2021 19:00:00 GMT-0500 (アメリカ東部標準時)
日付: 8
````
画面では2021/2/9と表示されているのに、値を取得すると2021/2/8が返ってくる。初期値を見れば、常に表示される日付の前日が返ってきていることがわかる。

## 何が起きているのか
日付入力欄の`input`部品内ではタイムゾーンがUTC(Coordinated Universal Time: 世界標準時~~; CUTでないのは各国が争った結果、どの主要言語とも一致しない文字の並べ方にした妥協の産物らしい~~)になっており、値は日未満が切り捨てられて「yyyy年MM月dd日00時00分00秒」の形式で保管されている。

### 日本の場合
1. 外から2021/2/9 00:00を表すDateオブジェクトの値を代入すると、値は日本標準時を表していると解釈され、UTCに変換(2021/2/8 15:00)した上で`input`に渡される。
2. `input`は受け取った値の端数を切り捨て(2021/2/8 00:00)て保管する。
3. 画面にはUTCでの日付が表示されていることになる。
4. `input`から`valueAsDate`を取得すると、タイムゾーンがUTCとして前述の値がそのまま返ってくる。
5. Dateの`toString()`や`getDate()`などは現地時間を基準にして出力されるので、UTCから日本標準時に変換され2021/2/8 09:00が返ってくる。
6. 日付として扱っている限りは画面の表示通りの値が得られる。
7. 代入する値に`Date()`のような現在時刻を使うと、09:00以降は正常動作に見えるという間違いに気づきにくい事態になる。

### アメリカの場合
重複するので簡潔に書くと、
1. 2021/2/9 00:00を渡すと、2021/2/9 05:00UTCに変換される。
2. `input`内部で2021/2/9 00:00に切り捨てられる。
3. 画面上は2021/2/9と表示される。
4. 値を取得すると現地時間に戻るので2021/2/8 19:00になり、画面表示からも1日ずれている。

## 対応
値を代入する時も、取り出す時もUTCと現地時間の変換を怠らない。あるいは文字列に変換する時に`getUTCDate()`などのUTCで値を取り出すメソッドを使用する。

```html
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <title>input dateが正常に日付を扱うか</title>
</head>
<body>
<input type="date" id="today" value="2021-02-07">
</body>
<script>
    let picker = document.getElementById("today");
    let offset = picker.valueAsDate.getTimezoneOffset()     // 追加
    console.log(`時差(分): ${offset}`)                      // 追加
    console.log(`初期値: ${picker.valueAsDate}`);
    // let day = new Date(2021, 2 - 1, 9);
    let day = new Date(2021, 2 - 1, 9, 0, -offset);
    console.log(`代入値: ${day}`);
    // 代入
    picker.valueAsDate = day;
    let newDay = picker.valueAsDate
    let newOffset = newDay.getTimezoneOffset()              // 追加
    newDay.setMinutes(newDay.getMinutes() +newOffset)         // 追加
    console.log(`取得値: ${newDay}`);
    console.log(`日付: ${newDay.getDate()}`);
    console.log(`日付(修正前UTC): ${picker.valueAsDate.getUTCDate()}`);         // 追加
</script>
</html>
```

**追記**(2021/3/20)
上記でoffsetを使いまわそうとすると、夏時間の境界で1時間ずれてしまうので、offsetは日付変更の度に毎回取り直さなければならない。

## 検証
画像は省略するがいずれも画面上は2021/2/9と表示される。
### 日本標準時
```
時差(分): -540
初期値: Sun Feb 07 2021 09:00:00 GMT+0900 (日本標準時)
代入値: Tue Feb 09 2021 09:00:00 GMT+0900 (日本標準時)
取得値: Tue Feb 09 2021 00:00:00 GMT+0900 (日本標準時)
日付: 9
日付(修正前UTC): 9
```
### アメリカ東部標準時
```
時差(分): 300
初期値: Sat Feb 06 2021 19:00:00 GMT-0500 (アメリカ東部標準時)
代入値: Mon Feb 08 2021 19:00:00 GMT-0500 (アメリカ東部標準時)
取得値: Tue Feb 09 2021 00:00:00 GMT-0500 (アメリカ東部標準時)
日付: 9
日付(修正前UTC): 9
```

## 考察
一番の問題点は、日時を引数に取る`Date(year,...)`のコンストラクタは現地時間でオブジェクト生成し、`getDate()`などのメソッドも現地時間を元にして値を返すのに、`input`だけUTCになっているという点だろう。

[HTML Living Standardにそう書いてある](https://html.spec.whatwg.org/multipage/input.html#date-state-(type%3Ddate))以上、公式な仕様として受け入れる他ない。今更修正したら世界中で大混乱するに違いないからおそらく今後も修正されないだろうと思う。

それにしても、アメリカとかで開発するときにはすごく不便なはずなのだが、仕様策定の時に誰も問題視しなかったのだろうか。

##改訂履歴(表現の修正などは除く)
+ 2021/2/7: 初版
+ 2021/3/20: 夏時間の問題について修正