JS/TSでタイムゾーン情報付きの`Date.toISOString()`を作成する

## 問題点

JavaScript/TypeScriptで日時情報を扱う場合、よく`Date`が用いられます。

そして`Date`を一定の書式（ここではISO 8601拡張形式）で文字列に変換するには、`Date.toISOString()`を用います。

しかしながら、この`Date.toISOString()`は「**日時を常にUTCで吐き出す**」という仕様[^1]になっており、例えば

```typescript
console.log(new Date('2021-01-30T00:00:00+09:00').toISOString());
```

を実行しても

```console
2021-01-29T21:00:00Z
```

のように、元々のタイムゾーン情報が失われてしまいます。これはUTC以外のタイムゾーン環境下[^2]においては下記のような点で問題を抱えています。

- 出力された時刻が直感と一致せず、いちいち脳内やプログラムで変換しなければならない
- タイミングによっては日付がずれてしまうこともあり、何かと不便
- etc...

そこで、`Date.toISOString()`と同じフォーマット（ISO 8601拡張形式）でタイムゾーン情報付きの文字列を出力させるためのメモです。

[^1]: ECMAスクリプトの[仕様書](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-date.prototype.toisostring)に曰く、"The time zone is always UTC, denoted by the suffix Z. "だそうで。

[^2]: e.g. 普段JSTの下で生活している我々

## 解決法

タイムゾーン情報付きのISO 8601拡張形式で出力する関数を自分で書くことで解決します。
`Date`には日時を任意のフォーマットで文字列に変換する関数がないため、下記のような泥臭い方法になっています。

### TypeScript

```typescript
function toISOStringWithTimezone(date: Date): string {
    const pad = function (str: string): string {
        return ('0' + str).slice(-2);
    }
    const year = (date.getFullYear()).toString();
    const month = pad((date.getMonth() + 1).toString());
    const day = pad(date.getDate().toString());
    const hour = pad(date.getHours().toString());
    const min = pad(date.getMinutes().toString());
    const sec = pad(date.getSeconds().toString());
    const tz = -date.getTimezoneOffset();
    const sign = tz >= 0 ? '+' : '-';
    const tzHour = pad((tz / 60).toString());
    const tzMin = pad((tz % 60).toString());
​
    return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
}
```

### JavaScript

```js
function toISOStringWithTimezone(date) {
    const pad = function (str) {
        return ('0' + str).slice(-2);
    };
    const year = (date.getFullYear()).toString();
    const month = pad((date.getMonth() + 1).toString());
    const day = pad(date.getDate().toString());
    const hour = pad(date.getHours().toString());
    const min = pad(date.getMinutes().toString());
    const sec = pad(date.getSeconds().toString());
    const tz = -date.getTimezoneOffset();
    const sign = tz >= 0 ? '+' : '-';
    const tzHour = pad((tz / 60).toString());
    const tzMin = pad((tz % 60).toString());

    return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
}
```

### 実行結果

```typescript
console.log(toISOStringWithTimezone(new Date('2021-01-30T00:00:00+09:00')));
```

を実行すると

```console
2021-01-30T00:00:00+09:00
```

と、きちんとタイムゾーン情報を保持したまま出力できました。

## 非推奨のおまけ

下記のように`Date.prototype.toISOString`をオーバライドする方法をとることで、`Date.toISOString()`の結果にタイムゾーン情報を含めることが可能です。

しかし「元の関数の定義から乖離してしまうという」という点で保守性の低下やバグの原因になりかねないので、この方法を取るか否か慎重に検討すべきでしょう。

### TypeScript

```typescript
Date.prototype.toISOString =  function(): string {
    const pad = function (str: string): string {
        return ('0' + str).slice(-2);
    }
    const year = (this.getFullYear()).toString();
    const month = pad((this.getMonth() + 1).toString());
    const day = pad(this.getDate().toString());
    const hour = pad(this.getHours().toString());
    const min = pad(this.getMinutes().toString());
    const sec = pad(this.getSeconds().toString());
    const tz = -this.getTimezoneOffset();
    const sign = tz >= 0 ? '+' : '-';
    const tzHour = pad((tz / 60).toString());
    const tzMin = pad((tz % 60).toString());
​
    return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
};
```

### JavaScript

```js
Date.prototype.toISOString = function () {
    const pad = function (str) {
        return ('0' + str).slice(-2);
    };
    const year = (this.getFullYear()).toString();
    const month = pad((this.getMonth() + 1).toString());
    const day = pad(this.getDate().toString());
    const hour = pad(this.getHours().toString());
    const min = pad(this.getMinutes().toString());
    const sec = pad(this.getSeconds().toString());
    const tz = -this.getTimezoneOffset();
    const sign = tz >= 0 ? '+' : '-';
    const tzHour = pad((tz / 60).toString());
    const tzMin = pad((tz % 60).toString());

    return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
};
```