この投稿ではJavaScriptの日時ライブラリdate-fnsでタイムゾーンを扱う方法を説明します。
基本知識
date-fnsはJavaScriptのDateを扱うヘルパー関数のセットなので、そもそもDateについてよく知っておく必要があります。
JavaScriptのDateにはタイムゾーンを表すデータが無い
JavaScriptのDateオブジェクトはタイムゾーンを表すデータを持ちません。
実行環境のタイムゾーン設定を変更したとしても、new Date()はUTC時刻になります:
$ TZ=Asia/Tokyo node -e 'console.log(new Date())'
2020-07-29T00:27:44.573Z
$ TZ=UTC node -e 'console.log(new Date())'
2020-07-29T00:27:50.167Z
DateのコンストラクタはISO 8601の表記をパースできますが、タイムゾーン指定子(+09:00など)をつけたとしても、UTC時刻に直されます:
console.log(new Date('2000-01-01T00:00:00+09:00'))
//=> 1999-12-31T15:00:00.000Z
DateはUTC時刻の1970年1月1日0時0分0秒をnumberの0と規定し、その既定値からの相対的な秒数をラップして、日時処理のAPIを生やしたような素朴なオブジェクトなので、タイムゾーンを表すデータを持たないのも理解できるかと思います。
date-fnsでのタイムゾーンの扱い
date-fnsとdate-fns-tz
date-fnsの姉妹パッケージにdate-fns-tzがあります。date-fns-tzはタイムゾーンを扱うためのパッケージですが、これが必要となるケースと不要なケースがあります。個別に見ていきます。
ISO 8601の表記をパースする
タイムゾーン指定子がある場合
タイムゾーン指定子を持ったISO 8601形式の日時のパースは、date-fnsのparseISOでできます。date-fns-tzは不要です。パースされた日時はUTCになります:
import { parseISO } from 'date-fns'
const utcDate: Date = parseISO('2000-01-01T00:00:00+09:00')
console.log(utcDate)
//=> 1999-12-31T15:00:00.000Z
タイムゾーンに関して言うと、Date()コンストラクタと同じ挙動になります:
const utcDate1: Date = parseISO('2000-01-01T00:00:00+09:00')
const utcDate2: Date = new Date('2000-01-01T00:00:00+09:00')
console.log(utcDate1.getTime() === utcDate2.getTime())
//=> true
タイムゾーン指定子が無い場合
タイムゾーン指定子を持たないISO 8601表記を、タイムゾーンを指定しながらパースする場合は、date-fns-tzのzonedTimeToUtcを使います。関数の名が示すとおり、戻り値のDateオブジェクトはUTC時刻になります。
import { zonedTimeToUtc } from 'date-fns-tz'
const utcDate: Date = zonedTimeToUtc('2000-01-01T00:00:00', 'Asia/Tokyo')
//=> 1999-12-31T15:00:00.000Z
もし、この場合に、parseISOを使ってしまうと、この"2000-01-01T00:00:00"はUTCのタイムゾーンを指すものと解釈されるので注意が必要です:
const utcDate: Date = parseISO('2000-01-01T00:00:00')
//=> 2000-01-01T00:00:00.000Z
Dateオブジェクトをフォーマットする
date-fnsのformat()関数とは別に、date-fns-tzにはタイムゾーンを指定できるformat()関数がありますが、この取扱には注意が必要です。
date-fns-tzのformatはタイムゾーン指定による時刻変更はしない
formatはタイムゾーン指定による時刻変更はしません。
import { format } from 'date-fns-tz'
const utcDate = new Date('2000-01-01T00:00:00')
//=> 2000-01-01T00:00:00.000Z
format(utcDate, 'yyyy-MM-dd HH:mm:ss', { timeZone: 'Asia/Tokyo' })
//=> "2000-01-01 00:00:00"
ご覧のとおり、UTCの0時は日本時間で9時のはずですが、フォーマットされた時刻は0時のままです。timeZoneを"UTC"にすると全く同じ文字列にフォーマットされることから、タイムゾーン指定による時間帯の変更はこの関数では行われないことが分かります:
format(utcDate, 'yyyy-MM-dd HH:mm:ss', { timeZone: 'UTC' })
//=> "2000-01-01 00:00:00"
format(utcDate, 'yyyy-MM-dd HH:mm:ss', { timeZone: 'Asia/Tokyo' })
//=> "2000-01-01 00:00:00"
ではdate-fns-tzのformat関数は何をするかというと、zやxのようなタイムゾーン書式に指定されたタイムゾーンを出すことを行ってくれます。
format(utcDate, 'yyyy-MM-dd HH:mm:ss xxx', { timeZone: 'UTC' })
//=> "2000-01-01 00:00:00 +00:00"
format(utcDate, 'yyyy-MM-dd HH:mm:ss xxx', { timeZone: 'Asia/Tokyo' })
//=> "2000-01-01 00:00:00 +09:00"
したがって、タイムゾーンを書式化する必要がない場合は、date-fns-tzのformat関数を使う理由はありません。
UTCなDateをAsia/Tokyoの日時にフォーマットする
UTCなDateオブジェクトを日本時間の日時文字列にフォーマットする場合は、次の手順を踏みます:
- 一旦、date-fns-tzの
utcToZonedTime関数で、UTCなDateオブジェクトを日本時間にずらしたDateオブジェクトに変換する。 - 変換後の
Dateオブジェクトをformat関数でフォーマットする
import { format } from 'date-fns'
import { utcToZonedTime } from 'date-fns-tz'
const utcDate = new Date('2000-01-01T00:00:00')
//=> 2000-01-01T00:00:00.000Z
const jstDate = utcToZonedTime(utcDate, 'Asia/Tokyo')
//=> 2000-01-01T09:00:00.000Z
const jstString = format(jstDate, 'yyyy-MM-dd HH:mm:ss')
//=> "2000-01-01 09:00:00"
このformat関数はdate-fnsのものであることに注意してください。もし、タイムゾーン表記をフォーマット後の文字列に含める場合は、date-fns-tzのほうのformat関数をtimeZoneオプションを指定しつつ使う必要があります。誤って、date-fnsのformat関数を使うとUTCのタイムゾーン表記になってしまいます:
import { format } from 'date-fns'
import { format as formatTZ } from 'date-fns-tz'
format(jstDate, 'yyyy-MM-dd HH:mm:ss xxx')
//=> "2000-01-01 09:00:00 +00:00" ← UTCの表記
formatTZ(jstDate, 'yyyy-MM-dd HH:mm:ss xxx', { timeZone: 'Asia/Tokyo' })
//=> "2000-01-01 09:00:00 +09:00" ← 日本時間の表記