RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

Day.jsのタイムゾーンを扱う関数 tz のドキュメントにない注意点を整理

こんにちは! RevComm のフロントエンドエンジニアの小山功二です。

私が RevComm に入社する前に担当した開発案件は、どれも国内のユーザーにしか使われていないものばかりでした。一方で、RevComm の提供する MiiTel は、日本はもちろんインドネシアやアメリカでも使われています。 私の担当する MiiTel CallCenter というプロダクトは今年リリースしたのですが、こちらもリリース当初から海外で利用できることが求められていました。

開発時からタイムゾーンを扱うのは大変そうだよねというのは感じていたのですが、想定よりも大変でした。

そこで今回はタイムゾーン周りの理解を深めるために、Day.js のタイムゾーンを変更する関数である tz という関数について整理していきたいと思います。

似たような4種の書き方ができる Day.js の tz 関数

まず tz 関数を使えるようにする準備をしましょう。

Day.js でtz関数を使えるようにするには timezone パッケージをインストールする必要があります。また、場合によって customParseFormat パッケージが必要になるケースもあります。

ここでは yarn を使っています。

$ yarn add dayjs timezone
$ yarn add customParseFormat

dayjsを拡張します。

import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import timezone from 'dayjs/plugin/customParseFormat';

dayjs.extend(timezone);
dayjs.extend(customParseFormat)

これで tz 関数を使えるようになりました。

この tz 関数は公式ドキュメントの中では3つページに記載があり、それぞれ別の使い方があることが示されています。

私はこの公式ドキュメントにない書き方をしてしまったのですが、それが Parsing in Zone に近い書き方で第一引数にDate型の値を入れてしまった書き方です。

Parsing in Zone には以下のように書いてあるので、第一引数がstring でくる前提のように見えます。

Parse date-time string in the given timezone and return a Day.js object instance.

一方で、tzの型をVSCode上で見てみると、以下のようになっていました。

const tz: dayjs.DayjsTimezone
(date: string | number | dayjs.Dayjs | Date | null | undefined, timezone?: string | undefined) => dayjs.Dayjs (+1 overload)

第一引数はstring以外も許容しています。

この点、具体的なコードをみた方が比較しやすいと思うので、 いくつかの記法を並べてみましょう。

// [記法1] ローカルタイムゾーンで '2023-12-25 00:00:00'を'Pacific/Honolulu'のタイムゾーンに変換
dayjs("2023-12-25 00:00:00").tz("Pacific/Honolulu")

// [記法2] '2023-12-25 00:00:00'を'Pacific/Honolulu'のタイムゾーンでパース
dayjs.tz("2023-12-25 00:00:00", "Pacific/Honolulu")

// [記法3] 記法2と同じ日時をdate型で指定したもの
dayjs.tz(new Date('2023-12-25 00:00:00'), "Pacific/Honolulu")

// [記法4] 文字列のフォーマットを解析して'Pacific/Honolulu'のタイムゾーンを設定(customParseFormatが必要)
dayjs.tz("12-25-2023 00:00:00", "MM-DD-YYYY ss:mm:HH", "Pacific/Honolulu")

記法2が Parsing in Zone に記載がある使い方で、記法3 が私が書いてしまったコードと同様の書き方です。

試しに、実際にどんな値が返ってくるか format 関数を使って見てみましょう。

なお、日本のタイムゾーンであるAsia/TokyoはUTC+09:00であり、以下のコードの中に出てくるPacific/Honoluluの中で設定しているPacific/HonoluluはUTC-10:00です。2つのタイムゾーンの時差は19時間です。

// [記法1] 日本時間2023-12-25 00:00:00のPacific/Honoluluでの時間を返す。
dayjs("2023-12-25 00:00:00").tz("Pacific/Honolulu").format("YYYY-MM-DDTHH:mm:ssZ")
  => "2023-12-24T05:00:00-10:00"

// [記法2] Pacific/Honoluluのタイムゾーンの2023-12-25 00:00:00を返す。
dayjs.tz("2023-12-25 00:00:00", "Pacific/Honolulu").format("YYYY-MM-DDTHH:mm:ssZ")
  => "2023-12-25T00:00:00-10:00"

// [記法3] Pacific/Honoluluのタイムゾーンの2023-12-25 00:00:00を返して欲しかったのですが、そうなっていない...
dayjs.tz(new Date('2023-12-25 00:00:00'), "Pacific/Honolulu").format("YYYY-MM-DDTHH:mm:ssZ")
  => "2023-12-24T05:00:00-10:00"

// [記法4] Pacific/Honoluluのタイムゾーンにて、“12-25-2023” という文字列が "MM-DD-YYYY" というフォーマットになっていると解釈した値を返す。
dayjs.tz("12-25-2023", "MM-DD-YYYY", "Pacific/Honolulu").format("YYYY-MM-DDTHH:mm:ssZ")
  => "2023-12-25T00:00:00-10:00"

記法2と3の結果が一致しませんでした。

tz 関数の実処理をコードから確認

なぜこうなるかわからなかったので、Day.jsのコードをみてみました。

https://github.com/iamkun/dayjs/blob/dev/src/plugin/timezone/index.js

以下は tz 関数の該当コードの抜粋です。

d.tz = function (input, arg1, arg2) {
    const parseFormat = arg2 && arg1
    const timezone = arg2 || arg1 || defaultTimezone
    const previousOffset = tzOffset(+d(), timezone)
    if (typeof input !== 'string') {
      // timestamp number || js Date || Day.js
      return d(input).tz(timezone)
    }
    const localTs = d.utc(input, parseFormat).valueOf()
    const [targetTs, targetOffset] = fixOffset(localTs, previousOffset, timezone)
    const ins = d(targetTs).utcOffset(targetOffset)
    ins.$x.$timezone = timezone
    return ins
  }

typeof input !== 'string' のとき、たとえば date 型を引数として設定した場合、d(input).tz(timezone) を返す処理になっています。これは記法1のタイムゾーンを変換する処理と同じ結果になります。

ドキュメントにはこの点の記載がないので、Day.jsのリポジトリに改善提案の issue を立てました。

終わりに

まとめると以下のようになります。

// [記法1] dateTimeString を timezone に変換
dayjs(dateTimeString).tz(timezone)

// [記法2] dateTimeString を timezone でパース
dayjs.tz(dateTimeString, timezone)

// [記法3] dateObject を timezone に変換(記法2に似てますが、記法1と同じ結果なので注意が必要)
dayjs.tz(dateObject, timezone)

// [記法4] dateTimeStringをcustomParseFormatで解析して、timezoneでパース
dayjs.tz(dateTimeString, customParseFormat, timezone)

今回は、Day.js の tz 関数について整理をすることができました。 これからもタイムゾーンとしっかり向き合い、日本でも国外でも多くの方々に使われるプロダクトに成長させられるように向き合っていきたいです。