📚

日時はクラス分けして扱うと良い

2021/10/08に公開

日時を扱う際に混乱するのはタイムゾーンの取り扱いである。

例えば、外国の人と時間を待ち合わせて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の実装を少しいじればできるので、読者で考えてみてほしい。

脚注
  1. Template Literal Typesを用いれば、厳密なISO8601の表現はできるので、トライしたい方はどうぞ。 ↩︎

Discussion