日時はクラス分けして扱うと良い
日時を扱う際に混乱するのはタイムゾーンの取り扱いである。
例えば、外国の人と時間を待ち合わせてZoomしたいのなら、相手のタイムゾーンに合わせて日時を変換する必要がある。
つまり、「時間軸上のある一点」を共有する必要がある。
一方で、グローバル企業における就業時間帯(例:9-18時)というデータは、働いている人の住んでいるタイムゾーンに関係なく同じ時間が表示されるべきかもしれない。
こういうのはクラスを分けた方が良い。
どう分けるか(一例)
例えば、
- Moment(時間軸上のある一点)
- Datetime(Date+Time、タイムゾーン無視)
- Date
- Time
というように分けておくと便利だ。
要件に応じて、値に対するクラス(または型)を割り振ると良い。
名前は一例なので、プロジェクトにとってわかりやすい名前を採用すると良い。
シリアライズとデシリアライズ
簡単なシリアライズの方法として、ISO8601に変換してしまう方法がある。
サーバーサイドはタイムゾーンや要件について関知せず、ただDBに突っ込めばよくなる。
そして、デシリアライズ時にクラスに必要な情報だけを抜き出すようにする。
例)
※ 太字はデシリアライズの際に必要な情報。
Moment型の場合、
2021-10-08T03:30:30.002Z ⇔ 2021年10月8日 12:30:30.002(JST)
Datetime型の場合
2021-10-08T12:30:30.002Z ⇔ 2021年10月8日 12時30分30秒2ミリ秒
Date型の場合
2021-10-08T00:00:00.000Z ⇔ 2021年10月8日
Time型の場合
1970-01-01T12:30:30.002Z ⇔ 12時30分30秒2ミリ秒
というようにする。
実装例(TypeScript)
実際に4種類のクラスを定義してみる。
シリアライズとデシリアライズは、それぞれコンストラクタと toISOString
メソッドを実装することで表現する。
Momentクラスは実装が一番楽で、プリミティブなDate型と挙動は同じだ。
ただし、ISO8601しか受け付けないという制約[1]がコンストラクタで表現される。
export class Moment {
date: Date;
constructor(iso8601: string) {
this.date = new Date(iso8601);
}
toISOString() {
return this.date.toISOString();
}
}
では次に、Datetime型も作ってみよう。
Datetime型はタイムゾーン無視だが、今回はUTC(~Z)の形式を前提とすることで実装を簡易化する。
export class Datetime {
date: Date;
constructor(iso8601UtcOnly: string) {
const moment = new Date(iso8601UtcOnly);
const year = moment.getUTCFullYear();
const month = moment.getUTCMonth();
const date = moment.getUTCDate();
const hours = moment.getUTCHours();
const minutes = moment.getUTCMinutes();
const seconds = moment.getUTCSeconds();
const ms = moment.getUTCMilliseconds();
this.date = new Date(year, month, date, hours, minutes, seconds, ms);
}
toISOString() {
const date = new Date(this.date.toISOString());
date.setHours(date.getHours() - date.getTimezoneOffset());
return date.toISOString();
}
}
まずコンストラクタから説明しよう。
2021-10-08T12:30:30.002Z
というISO8601文字列が引数に来た場合、普通にDateコンストラクタに読ませるとUTCの12:30で認識してしまうが、DateTimeはタイムゾーン無視でローカルなタイムゾーンとして認識したいので、筆者の場合はJSTの12:30に変換したい。
まず、JSのDate型にはgetUTC~というメソッドが用意されていて、2021-10-08T12:30:30.002Z
をDateコンストラクタに渡した場合、
- getHours -> 21(12+9) ローカルな時間を返す
- getUTCHours -> 12 UTCの時間を返す
となる。
そして、JSのDateコンストラクタは年~ミリ秒までを数値で指定すると、ローカルなタイムゾーンのとして認識してくれるので、
new Date(2021, 10, 8, 12, 30, 30, 2)
とすると、JSTの12:30になってくれる。
よって、getUTC~を使って年~ミリ秒までの各数値を取得し、Dateコンストラクタに指定することで、意図した日時情報を持ったDateオブジェクトを作成できるわけだ。
次に、toISOString を説明する。
先ほどの例でいうと、現状JSTの12:30がDateの情報として入っているので、これを基底クラスのtoISOStringに任せると、UTC換算されてしまう。
(基底のtoISOStringの場合)
JSTの12:30 → 2021-10-08T03:30:30.002Z
欲しい文字列は2021-10-08T12:30:30.002Z
なので、JSTの場合は9時間足す必要がある。
まずはJSTの12:30のDateオブジェクトをクローンする。
const date = new Date(this.date.toISOString());
クローンしたオブジェクトに対して、+9時間する必要があるが、そのための値はJSのDate.getTimezoneOffset メソッドで取得できる。
例えば、ローカルがJST(+9)の場合、以下の出力が出る
new Date().getTimezoneOffset()
-> -540
(分換算)
今回は540を足したいので、マイナスを付けて符号を反転させる
date.setHours(date.getHours() - date.getTimezoneOffset());
これで、JSTの21:30のDateオブジェクトができたので、これをtoISOStringすれば下記が得られる。
2021-10-08T12:30:30.002Z
これで、Datetimeクラスの実装の説明は終わりだ。
DateとTimeはDatetimeの実装を少しいじればできるので、読者で考えてみてほしい。
-
Template Literal Typesを用いれば、厳密なISO8601の表現はできるので、トライしたい方はどうぞ。 ↩︎
Discussion