【Javascript】日時を常にJST日本時間で表示したい【IE11対応】

## はじめに
Javascriptの`new Date`関数で生成した日付はタイムゾーンの影響を受けてしまいます。
タイトルの通り「**日時を常にJST日本時間で表示したい**」場合は、タイムゾーンを考慮したコードを書く必要があります。

### 具体的にどういうこと？
下記のようなコードを書いてみるとわかりやすいです。

```.js
const timestamp = 1632322799;
const date = new Date(timestamp * 1000).toLocaleString();
const formattedDate = //【省略】dateをyyyy/mm/dd hh:mm:ssに整形

console.log(formattedDate);
```

**console.log 結果**の日時に注目してください。

| タイムゾーン名称 | タイムゾーンID | 略称 | UTCからのオフセット | console.log 結果 |
|:-:|:-:|:-:|:-:|:-:|
| ハワイ・アリューシャン標準時 | Pacific/Honolulu | HAST | -10:00 | 2021/09/22 4:59:59 |
| ニューファンドランド標準時 | America/St_Johns | NST | -03:30 | 2021/09/22 12:29:59 |
| グリニッジ標準時 | Europe/London | GMT | +00:00 | 2021/09/22 14:59:59 |
| 日本標準時 | Asia/Tokyo | JST | +09:00 | 2021/09/22 23:59:59 |
| ロードハウ標準時 | Australia/Lord_Howe | LHST | +10:30 | 2021/09/23 1:29:59 |

上記のようにPCのタイムゾーン設定により出力される日時が変化してしまっています。

ちなみにPCのタイムゾーン設定はWindows/Mac共に簡単に変更できるので
誰でもブラウザで表示確認できることです:ok_hand:

## 今回やりたいことまとめ
- Unixタイムスタンプを日時に変換して出力
- 日時は常にJST日本時間で表示
- モダンブラウザ＋IE11にも対応
- できればIE11対応のためにポリフィルは入れたくない

今回はタイムスタンプを日時に変換したいので`new Date(timestamp * 1000)`としています。
もちろん`new Date()`や`new Date('2021-09-25')`といった形でも同じ対応が可能です。

## 【結論】date-fns + date-fns-tzで対応

```.js
import { format } from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';

const timestamp = 1632322799;
const formattedDate = format(utcToZonedTime(timestamp * 1000, '+09:00'), 'yyyy/MM/dd HH:mm:ss');

console.log(formattedDate);
```
ポイントは`'+09:00'`の部分。この書き方のおかげでIE11ポリフィルなしで実行できています。
`'Asia/Tokyo'`とも書けますが、IE11で動かすには`'+09:00'`のように、UTCからのオフセットを記述する必要があります。

### 参考
https://github.com/marnusw/date-fns-tz#date-fns-tz
> If you do not wish to use a polyfill the time zone option can still be used, but only with time zone offsets such as '-0200' or '+04:00' and not IANA time zone names.

Google翻訳した日本語はこちら。

> ポリフィルを使用したくない場合でも、タイムゾーンオプションを使用できますが、IANAタイムゾーン名ではなく、「-0200」や「+04:00」などのタイムゾーンオフセットでのみ使用できます。

## 結論に至るまでに失敗した方法を紹介
### JavaScript（timeZone）

ユーザー環境のlocaleを指定＋タイムゾーンを`Asia/Tokyo`と指定するパターンです。

#### ダメだった理由
- `timeZone`プロパティがIE11未対応のためエラーが発生

```.js
const timestamp = 1632322799;
const userLocale = (window.navigator.languages && window.navigator.languages[0]) || window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage;
const date = new Date(timestamp * 1000).toLocaleString(userLocale, { timeZone: "Asia/Tokyo" });
const formattedDate = //【省略】dateをyyyy/mm/dd hh:mm:ssに整形

console.log(formattedDate);
```

### JavaScript（getTimezoneOffset）

`getTimezoneOffset`を使って固定する日時を計算するパターンです。
日本標準時（JST）に固定したいため、UTCからのオフセットを分単位で表すために`(9 * 60)`をしています。

#### ダメだった理由
- サマータイム導入地域の場合、一部の日時がズレて表示される

```.js
const timestamp = 1641308399;
const date = new Date(timestamp * 1000 + ((new Date().getTimezoneOffset() + (9 * 60)) * 60 * 1000));
const formattedDate = //【省略】dateをyyyy/mm/dd hh:mm:ssに整形

console.log(formattedDate);
```

#### サマータイム導入地域の場合、一部の日時がズレて表示される

Unixタイムスタンプが`1632322799`（2021/09/22 23:59:59）では問題ありませんが
サマータイム（夏時間・DST）が導入されている「ニューファンドランド」「ロードハウ」では、例えばタイムスタンプが`1641308399`（2022/01/04 23:59:59）だと時間がズレてしまいます。

| タイムゾーン名称 | タイムゾーンID | 略称 | UTCからのオフセット | console.log 結果 |
|:-:|:-:|:-:|:-:|:-:|
| ハワイ・アリューシャン標準時 | Pacific/Honolulu | HAST | -10:00 | 2022/01/04 23:59:59 |
| ニューファンドランド標準時 | America/St_Johns | NST | -03:30 | **2022/01/04 22:59:59** |
| グリニッジ標準時 | Europe/London | GMT | +00:00 | 2022/01/04 23:59:59 |
| 日本標準時 | Asia/Tokyo | JST | +09:00 | 2022/01/04 23:59:59 |
| ロードハウ標準時 | Australia/Lord_Howe | LHST | +10:30 | **2022/01/05 00:29:59** |

### Day.js

#### ダメだった理由
- IE11でエラーが発生
- 一部Safariでエラーが発生

```.js
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

// タイムゾーンを扱うために必要なプラグインを使用
dayjs.extend(utc);
dayjs.extend(timezone);

// タイムゾーンを日本に設定
dayjs.tz.setDefault('Asia/Tokyo');

const timestamp = 1632322799;
const formattedDate = dayjs.tz(timestamp * 1000).format('YYYY/MM/DD HH:mm:ss');

console.log(formattedDate);
```

`date-fns`よりも軽量かつ`Moment.js`に近い記述ができる`Day.js`。
日時の処理が多いサイトかつ、サポートすべきブラウザで動くのであればこちらが最適解かと思います:muscle:

## ひとこと
IE11対応は大変だなあ:frowning2:（132937289328回目）