日時の処理は様々なアプリケーションにおいて避けては通れないタスクです。JavaScriptにおいてもそれは例外ではありません。
JavaScriptでは最初期からDate
オブジェクトが日時を表すオブジェクトとして存在していましたが、これは非常に使いにくいAPIで知られています。その結果、momentに代表されるような日時処理ライブラリを使うのが事実上スタンダードとなっています。
この記事では、将来的に日時処理の有力な選択肢になると期待されるモジュールであるTemporalについて解説します。Temporalでは、既存のDate
による日時処理のつらい部分が解消されることが期待されています。
なお、例によってTemporalはまだ策定中の仕様です。現在Stage 2というフェーズにあり、APIを鋭意策定中という状況です。よって、この記事にかかれている内容は確定までにまだ変化するかもしれません。この記事でTemporalの雰囲気を理解し、実際に使う際はご自分での情報収集が必要です。覚えていればこの記事もちゃんとアップデートしますが。
Temporalの特徴
Temporalについて詳説する前に、Temporalの特徴的な部分をまとめておきます。具体的なひとつひとつのAPIがまだ今後変わりそうなことを考えれば、この記事で一番重要なのはここです。特に、Temporalについてどのようなデザイン上の決定が為されているのかを知るのはTemporal、ひいては今後のJavaScriptの展望を理解するのに役立つでしょう。
1. 標準ライブラリに含まれている
Temporalは標準ライブラリに含まれるモジュールです。標準ライブラリについては筆者の別記事で詳しく解説しているのでぜひチェックしてください。
外部ライブラリを使うという選択肢はTemporalの登場後も消えることは無いでしょうが、それらと比べたTemporalの利点はTemporalが標準ライブラリに含まれていることです。特にブラウザ環境では、Temporalで事足りるならば外部ライブラリが不要になり、JavaScriptのダウンロードサイズを削減することができます。
標準ライブラリの概念の登場によってこれまで最低限だったJavaScriptの機能を拡充しようという動きがあり、Temporalもその一環だといえます。
2. オブジェクトがイミュータブル
Temporalが提供するオブジェクトはイミュータブルです。つまり、オブジェクトを作成した後でそれが表す日時情報を書き換えることはできません。別の日時情報を表したい場合は別のオブジェクトを作ることになります。
これと対称的に、既存のDate
オブジェクトはミュータブルでした。つまり、setFullYear
やsetHours
などのメソッドを使ってDate
オブジェクトの日時情報を書き換えることができたのです。
なぜミュータブルなオブジェクトよりもイミュータブルなオブジェクトのほうがよいのかは今さら言うまでもありません。特に今回扱うのは日時情報という「データ」の側面が強いものを表すオブジェクトです。例えば日時データを他の関数に渡すような場合に、その関数の中でオブジェクトが書き換えられてしまわないかどうかをわざわざ心配するのは無駄ですね。
// 日付データを引数で受け取る
function foo(date) {
// 別の関数に渡す
bar(date);
// dateはイミュータブルなのでbarによって書き換えられている心配はない
console.log(date);
}
そもそも単なる日時データとしてもDate
が使いにくかったことがTemporalが登場する動機のひとつとなっています。
3. 意味に応じて異なるオブジェクトを使用
既存のDate
オブジェクトの最大の問題点はタイムゾーンの扱いがとても適当であるという点です。Date
オブジェクトは自分のタイムゾーンはどこかという情報を持っています1。明示していない場合タイムゾーンは環境依存です。また、(特にタイムゾーンが1つしかない日本では)タイムゾーンが特に必要でない日時処理もあり、そのような場合でもDate
の仕様上タイムゾーンを意識しなければならないのはバグの元だし残念です。
そこで、Temporalではタイムゾーンの情報を持つデータとタイムゾーンの情報を持たないデータを別々のオブジェクトで表現します。タイムゾーンの情報を持たない日時情報は**CivilDateTime
、タイムゾーンの情報を持つ日時情報はZonedDateTime
**というオブジェクトで表現します。また、詳しくは後述しますが他にもいくつかの種類のオブジェクトが利用可能です。
4. ナノ秒単位のデータが扱える
読んで字のごとしです。既存のDate
オブジェクトの時刻データはミリ秒単位の精度ですが、Temporalでは精度が6桁増えてナノ秒単位になりました。
Temporalの使い方
では、いよいよTemporalの使い方を解説していきます。
Temporalを読み込む
Temporalは標準ライブラリに加えられる見込みですので、import
文を用いて読み込むことになります。例えばCivilDateTime
というオブジェクトを使用したいときは次のようにします。
import { CivilDateTime } from "std:temporal";
"std:temporal"
の部分は未確定です。"js:temporal"
とかになるかもしれません。
もちろんこれはまだブラウザ等には実装されていません。公式のPolyfillが存在しますが、仕様が変更されるにつれてPolyfillにも変更が加えられることになっていますので安定したものではありません2。また、現状ではこのPolyfillも少し古い情報(2019年3月以前の仕様)に基づいており、最新の情報を反映していません。以降のサンプルでは可能なものはこのPolyfillで動作を確認しています。
オブジェクトの種類
Temporalが提供するオブジェクトは現在のところ以下の種類があります。
- Civil系:
CivilDateTime
,CivilTime
,CivilDate
,CivilYearMonth
,CivilMonthDay
- 絶対時間系3:
Instant
,OffsetDateTime
,ZonedDateTime
Civil系オブジェクト
Civil系というのはタイムゾーン情報を持たないデータで、持っている情報によってオブジェクトの種類が細分化されています。例えばCivilDate
というのは日付の情報、すなわち年・月・日を持つオブジェクトです、CivilTime
は時計、すなわち時間・分・秒及びそれ以下です。CivilDateTime
は日付と時計の両方の情報を持っています。また、CivilYearMonth
というのは年と月のみで日及び時計の情報を持たないデータです。CivilMonthDay
も同様に月と日のみで年の情報を持ちません。
重要なのは、これらのCivil系オブジェクトは絶対的な(全世界的に一意の)ひとつの時刻を指し示すものではないということです。Civil系オブジェクトは例えば「2019年5月1日12時0分0秒」のように暦(こよみ)上の情報、すなわちカレンダーや時計の表示を表すものですが、このデータだけでは全世界的に一意の時刻を表すことはできません。例えば、日本とイギリスではこのデータが表す絶対時刻は9時間4ずれています。
要するに、Civil系オブジェクトはタイムゾーンあるいはオフセットの情報を持たないデータであるということです。これらのオブジェクトはタイムゾーンと関係ないローカルなデータや、カレンダーの処理をしたい場合に適しています。
絶対時間系オブジェクト
Civil系以外の残りの3つのオブジェクトは絶対時間を表すことができます。Instant
オブジェクトはその中でも「生データ」に近いオブジェクトであり、時刻を1970年1月1日0時0分0秒(世界標準時)からの経過時間(ナノ秒単位)で保持しています5。
このように1970年1月1日からの経過時間で絶対時刻を表す方法はコンピュータにおける時刻表現として広く使われており、UNIX時間として知られています。Instant
はこのUNIX時間を表すオブジェクトであるといえます。
さて、Instant
により絶対時刻を表すことができますが、これは不便です。なぜなら、オフセットの情報が無いと実際にはこれが何月何日の何時何分なのか分からないからです。これも先ほどと同じ話で、例えば「1970年1月1日0時0分0秒(世界標準時)から100秒後」という時刻はイギリスでは1970年1月1日の0時1分40秒ですが、日本では1970年1月1日の9時1分40秒を表しており、ひとつに定まりません。
オフセットは、世界標準時からどれだけ時差があるかということを表す分単位の情報です。例えば日本は世界標準時よりも9時間進んでいるのでオフセットは+09:00
です。イギリスは世界標準時と同じなので+00:00
となります。
絶対時刻とオフセットの2つの情報を組み合わせることで日時(カレンダー・時計の値)をひとつに定めることができます。例えば「1970年1月1日0時0分0秒(世界標準時)から100秒後」という時刻は+09:00
というオフセットにおいては「1970年1月1日9時1分40秒」という日時を表すのです。
逆に言えば、日時とオフセットがあれば絶対時刻を特定することができます。つまり、「オフセット+09:00
における1970年1月1日9時1分40秒」というデータは「1970年1月1日0時0分0秒(世界標準時)から100秒後」という絶対時刻を一意に表しています。
前置きが長くなりましたが、このような「日時とオフセット」の情報を持つオブジェクトがOffsetDateTime
です。
最後のZonedDateTime
はOffsetDateTime
と同様に日時の情報を持つオブジェクトですが、オフセットの代わりにタイムゾーンを持っています。
タイムゾーンは地域名により表現されるデータであり、例えば日本のタイムゾーンはAsia/Tokyo
です。そして、Asia/Tokyo
のオフセットは+09:00
であるというデータがtz databaseに保存されているためタイムゾーンからはオフセットを得ることができます。これにより、ZonedDateTime
もやはり絶対時刻を表すことができます。
OffsetDateTime
とZonedDateTime
の大きな違いは、後者はタイムゾーン内でのオフセットの変化に対応可能だということです。日本ではあまり馴染みがありませんが、典型的には夏時間の影響により、時季によって同じタイムゾーン内でもオフセットが変わる可能性があるのです。これにより、ZonedDateTime
に対して日時の操作を行う場合、同じタイムゾーンでもオフセットが勝手に変動することがあります。一方のOffsetDateTime
は常に固定のオフセットを持ちます。一般に、タイムゾーンに寄り添った日時処理を行いたい場合はZonedDateTime
が便利ですね。
まとめると、Civil系オブジェクトは暦により表された情報のみを持ちオフセットやタイムゾーンの情報を持たないオブジェクトで、絶対時刻を表すものではありません。Instant
, OffsetDateTime
, ZonedDateTime
はいずれも絶対時刻を表すもので、さらにOffsetDateTime
はオフセットの情報を、ZonedDateTime
はタイムゾーンの情報を持っています。
なお、Temporalが採用している暦は先発グレゴリオ暦であり、これは我々が慣れ親しんでいる暦(グレゴリオ暦)をグレゴリオ暦が実際に使われるよりも以前にも適用するようにしたものです。この暦は古い日付も統一的に扱えて簡単であるという利点からプログラミング言語での採用例があり、日時の表記の国際規格であるISO 8601にもこれが採用されています。Temporalは基本的にISO 8601に準拠して作られています。
では、ここからは各オブジェクトを見ていきます。とはいえ、TemporalのAPIはまだ変化の途上にあり、何ヶ月もすればAPIが別物になっているということも普通にあります。ですから以下の話はおまけ程度に考えてください。どちらかといえばここまで説明したコンセプトのほうが重要です。
CivilDateTime
つい先程述べたように、CivilDateTime
は暦の上での日時情報を表すオブジェクトです。具体的には、年・月・日・時間・分・秒・そして秒以下の数値で日時を表します。CivilDateTime
オブジェクトを作る方法の1つはCivilDateTime
コンストラクタを使うものです。例えば、過ぎし2019年5月1日の0時0分0秒を表すCivilDateTime
は次のように作ります。
// 年, 月, 日, 時, 分の順でコンストラクタに数値を渡す
const startOfReiwa = new CivilDateTime(2019, 5, 1, 0, 0);
まず最初に気づいていただきたいのは、月は1始まりということです。古いDate
オブジェクトでは月は0〜11の整数で表され、0が1月、1が2月、……11が12月というとても間違えやすい仕様になっていたのですが、Temporal
では1〜12の数値になっています。
上の例ではコンストラクタの引数は5個ですが、最大9個まで数値を指定可能です。意味は順に年、月、日、時、分、秒、ミリ秒、マイクロ秒、ナノ秒です。ミリ秒以下は0から999の数値で指定します。例えば、2019年5月1日の2時34分56.789012345秒という時刻を表したい場合は次のようにします。
const time = new CivilDateTime(2019, 5, 1, 2, 34, 56, 789, 12, 345);
CivilDateTime
コンストラクタの引数は最低5個必要です。つまり、秒以下は省略可能ですが分以上は省略できません。省略したところは当然0になります。
CivilDateTime
のプロパティ
こうして作られたCivilDateTime
オブジェクトからはいくつかのプロパティを通して情報を取得可能です。基本的なプロパティはyear
、month
、date
、hour
、minute
、second
、millisecond
、microsecond
、nanosecond
で、上記のコンストラクタの引数にちょうど対応した情報を持っています。
console.log(time.year); // 2019
console.log(time.day); // 1
console.log(time.millisecond); // 789
Date
ではgetFullYear()
などとする必要があったのに比べると簡潔で嬉しいですね。
また、いくつか追加の情報を取得できるプロパティがあります。dayOfWeek
はその日付の曜日を表す数値であり、1(月曜日)〜7(日曜日)の整数で表されます。dayOfYear
はその日がその年の1月1日から数えて何日目かを表す数値です(1月1日は1
日目です)。そしてweekOfYear
は週番号、すなわちその日を含む週がその年の何週目かを表す数値です。
console.log(time.dayOfWeek); // 3 (2019年5月1日は水曜日)
console.log(time.dayOfYear); // 121
console.log(time.weekOfYear); // 18
なお、週番号についてはISO 8601で規定されているものであり、「年の最初の木曜日を含む週がその年の第1週である」とされています。その結果、次の例のように年始の日付が前の年の最終週に属するという判定になることがあります。
const wah = new CivilDateTime(2010, 1, 3, 0, 0);
console.log(wah.weekOfYear); // 52
2010年最初の木曜日は1月7日ですから、1月7日を含む週が2010年の第1週です。dayOfWeek
で月曜日が1であることからも分かるように週は月曜日〜日曜日が1つの週という数え方になりますから、2010年の第1週は1月4日(月)〜1月10日(日)ということになります。よって、それより前の1月3日は週番号上は2009年の最終週に属することになるのです。
文字列への変換
日時データというのは頻繁にコンピュータ間でやり取りされます。そのためには日時データを表現する共通の方法が必要であり、それはやはりISO 8601によって規定されています。CivilDateTime
はtoString()
によってISO 8601に適合する文字列に変換可能です。toString()
は文字列への暗黙の変換の際も呼ばれます。
const startOfReiwa = new CivilDateTime(2019, 5, 1, 0, 0);
console.log(startOfReiwa.toString()); // "2019-05-01T00:00:00.000000000"
ちなみに、ISO 8601では秒未満(.
以降の部分はオプショナルで桁数の規定はありませんが、Temporalでは9桁、すなわちナノ秒の精度で文字列化することが規定されています。
なお、日時データを機械ではなく人間が読みやすい形で表示できる機能というのも需要がありますが、それは現在議論中でまだ固まっていないようです。
文字列からの変換
上で紹介したメソッドたちとは逆に、文字列からCivilDateTime
オブジェクトを得るための方法も用意されています。それがCivilDateTime.fromString()
です。
console.log(CivilDateTime.fromString("2019-05-01T00:00:00.000000000"));
なお、ISO 8601文字列はもう少しバリエーションがありますが、CivilDateTime.fromString
はそれらの文字列も受け付けてくれそうな雰囲気があります。雰囲気というのはどういうことかというと、正確に何が受け付けられて何が受け付けられないのかは仕様策定者たちの頭の中にしかない(あるいは決まっていないかもしれない)ということです。
plus
: 時刻の加算
CivilDateTime
オブジェクトはplus
メソッドを持ち、ある時刻に対して加算を行うことができます。CivilDateTime
オブジェクトはイミュータブルですから、結果は新しいCivilDateTime
オブジェクトとして得られます。さっそく例を見ましょう。
const startOfReiwa = new CivilDateTime(2019, 5, 1, 0, 0);
// 12時間足して正午にする
const noon = startOfReiwa.plus({ hours: 12 });
console.log(noon.toString()); // "2019-05-01T12:00:00.000000000"
// 1年戻して5000秒進める
const time2 = noon.plus({ years: -1, seconds: 5000 });
console.log(time2.toString()); // "2018-05-01T13:23:20.000000000"
このように、plus
にはオブジェクトを渡します。オブジェクトはyears
, months
, days
, hours
, minutes
, milliseconds
, microseconds
, nanoseconds
を持つことができます。プロパティ名が複数形である点に注意してください。2番目の例のように、負の数を渡すことで時間を戻すこともできます。
with
: 部分的な上書き
with
メソッドは、既存のCivilDateTime
オブジェクトの情報を部分的に書き換えて得られる新しいCivilDateTime
オブジェクトを返します。
const startOfReiwa = new CivilDateTime(2019, 5, 1, 0, 0);
// 年を2100年に、日を31日に変更する
const maybeNotReiwa = startOfReiwa.with({ year: 2100, day: 31 });
console.log(maybeNotReiwa.toString()); // "2100-05-31T00:00:00.000000000"
渡すのはやはりオブジェクトであり、CivilDateTime
と同名のプロパティたちを用いて上書き箇所を指定します。
minus
: 2つの日時の差を得る
2つのCivilDateTime
の間の期間を得ることができるminus
メソッドも用意されています。使い方は多分こんな感じです。
const startOfReiwa = new CivilDateTime(2019, 5, 1, 0, 0);
const startOfHeisei = new CivilDateTime(1989, 1, 8, 0, 0);
const diff = startOfReiwa.minus(startOfHeisei);
console.log(diff);
/*
{
years: 31,
months: 3,
days: 23,
hours: 0,
minutes: 0,
seconds: 0,
(以下略)
}
*/
多分こんな感じというのは、メソッドの存在だけとりあえず決まっていそうな感じがして具体的な計算方法はまだ決まっていなそうな感じがしていることを意味しています。
CivilDate
他のCivil系のオブジェクトは基本的にCivilDateTime
より情報が少ないバージョンです。CivilDate
は年・月・日の情報だけを持ち、コンストラクタの引数は年・月・日の3つです。
const startOfReiwa = new CivilDate(2019, 5, 1);
プロパティとしてはyear
, month
, day
を持ちます。また、dayOfWeek
, dayOfYear
, weekOfYear
も日付だけあれば計算可能なので持っています。持っているメソッドたちも同様で、with
, plus
, minus
があります。
注意すべき点は、plus
では渡されたオブジェクトのyears
, months
, days
のみ見られるということです。次のようにしても日付が1日進んだりはしません。
// hoursは無視されるのでこれはstartOfReiwaと同じ
const tomorrow = startOfReiwa.plus({ hours: 24 });
また、CivilDate
は日付の情報のみを持っているオブジェクトでしたが、次に紹介するCivilTime
(一日の中の時刻情報だけを持つ)と組み合わせることでCivilDateTime
を作ることができます。そのためにはwithTime
メソッドを用います。
const startOfReiwa = new CivilDate(2019, 5, 1);
const clock = new CivilTime(12, 34, 56);
const dateTime = startOfReiwa.withTime(clock);
console.log(dateTime.toString()); // "2019-05-01T12:34:56.000000000"
文字列への変換
CivilDate
で特筆すべき点は文字列への変換メソッドを3種類持っている点です。実はISO 8601では日付の表現方法が3種類規定されています。1つは2019-05-01
のように年-月-日で表現する方法、2つ目は2019-W18-03
のように年-週番号-曜日で表現する方法、そして最後は2019-121
のように年とその年の1月1日からの日数で表す方法です。
この3種類の変換はそれぞれtoDateString()
, toWeekDateString()
, toOrdinalDateString()
で可能です。toString()
はtoDateString()
と同じになります。
fromString()
は3種類の表現の全てを受け付けてくれます。
CivilTime
CivilTime
のコンストラクタは2〜6引数です。最初の2つ、すなわち時と分は省略不可能で、それ以下(秒、ミリ秒、マイクロ秒、ナノ秒)は省略可能です。
また、withDate
にCivilDate
を渡すことでCivilDateTime
に変換することができます。
CivilYearMonth
CivilYearMonth
は年と月だけの情報を持ちます。コンストラクタの引数もその2つです。withDay()
メソッドでCivilDate
に変換できます。
CivilMonthDay
CivilMonthDay
は月と日だけの情報を持ちます。withYear()
メソッドでCivilDate
に変換できます。
なんだか後半駆け足でしたが、以上でCivil系の説明は終わりです。
Instant
Instant
は前述の通り、UNIXエポック(1970年1月1日0時0分0秒(世界標準時))からの経過時間(ナノ秒)によって絶対時刻を表すデータです。よって、Instant
コンストラクタにはUNIXエポックからの経過時間を渡します。例えば、UNIXエポックから1556636400000000000ナノ秒後の絶対時刻を表すInstance
オブジェクトを作りたい場合は次のようにします。
const inst = new Instant(1556636400000000000n);
なお、1556636400000000000n
というのはBigIntのリテラルです。普通の数値型ではナノ秒単位の秒数を扱うには精度が小さいのでBigIntが使われています。数値型の精度とかBigIntの話は筆者の別記事がありますのでよろしければそちらもご覧ください。
Instant
オブジェクトからは、UNIXエポックからの経過時間を秒・ミリ秒・マイクロ秒・ナノ秒単位で得ることができます。
console.log(inst.epochSeconds); // 1556636400
console.log(inst.epochMilliseconds); // 1556636400000
console.log(inst.epochMicroseconds); // 1556636400000000n
console.log(inst.epochNanoseconds); // 1556636400000000000n
マイクロ秒とナノ秒はBigIntになっています。ミリ秒くらいなら普通の数値で行けるだろという判断のようです。ちなみに、ミリ秒をBigIntではなく普通の数値で表した場合、西暦約29万年問題の発生が懸念されます。今からとても心配ですね。
また、Instant
にもtoString
とfromString
がありますが、何に変換されるのかはよく分かりません。多分Z
タイムゾーンのISO 8601表現とかだと思いますが。
Instant
にはオフセットやタイムゾーンの情報がないため具体的な日付や時刻の数値が分からないのでした。そのため、上記のように得られる情報は人間には分かりにくいものとなっています。
そこで、withOffset
やwithZone
メソッドでこれらの情報を付加することでOffsetDateTime
やZonedDateTime
に変換することができます。withOffset
には"+09:00"
のようなオフセットを表す文字列を、withZone
には"Asia/Tokyo"
のようなタイムゾーンを表す文字列を渡します。
OffsetDateTime
OffsetDateTime
はオフセット情報のついた時刻データです。API的にはCivilDateTime
プラスアルファだと思えば間違いありません。
作り方はInstant
のwithOffset
を使う方法やnew OffsetDateTime(instant, offset)
のようにする方法があります。どちらにせよInstant
のインスタンスが必要ですね。CivilDateTime
にもwithOffset
がある気がするのですがはっきりとは分かりませんでした。また、OffsetDateTime.fromString
で文字列から作る方法もあります。
OffsetDateTime
は自分のタイムゾーンの元で自分が何年何月何日の何時何分かということを知っていますから、CivilDateTime
の機能は全て利用可能です。year
, month
などのプロパティで数値を取得したりplus
, with
, minus
メソッドを使用できます。
加えて、instant
プロパティで自分の絶対時刻を表すInstant
オブジェクトを取得可能です。また、offset
プロパティはオフセットを表す文字列です。toString
に関してはISO 8601に従ってオフセット情報が付加された文字列を返します。
const inst = new Instant(1556636400000000000n);
const datetime = new OffsetDateTime(inst, "+09:00");
console.log(datetime.year); // 2019
console.log(datetime.month); // 5
console.log(datetime.day); // 1
console.log(datetime.offset);// "+09:00"
console.log(datetime.toString()); // "2019-05-01T00:00:00.000000000+09:00"
なお、なぜかOffsetDateTime.fromZonedDateTime
が用意されておりZonedDateTime
からOffsetDateTime
への変換ができます。
ZonedDateTime
ZonedDateTime
はタイムゾーンの情報を持ったオブジェクトで、OffsetDateTime
のさらに上位互換です。作り方は先ほど説明したInstant
のwithZone
か、new ZonedDateTime(instant, zone)
です(あとZonedDateTime.fromString
)。
OffsetDateTime
の機能は全て使える上に、timeZone
プロパティでタイムゾーンを取得できます。また、toString()
の結果は下のようにタイムゾーンの情報が付加されます。
const inst = new Instant(1556636400000000000n);
const datetime = new ZonedDateTime(inst, "Asia/Tokyo");
console.log(datetime.year); // 2019
console.log(datetime.month); // 5
console.log(datetime.day); // 1
console.log(datetime.offset);// "+09:00"
console.log(datetime.timeZone); // "Asia/Tokyo"
console.log(datetime.toString()); // "2019-05-01T00:00:00.000000000+09:00[Asia/Tokyo]"
[Asia/Tokyo]
のように角括弧で囲まれたタイムゾーン名があるのが特徴的です。また、オフセットの情報もついています。
補足:現在時刻の取得について
古いDate
は引数なしでDate
コンストラクタを呼ぶと初期値が現在時刻になったり、Date.now()
でミリ秒の形で現在時刻を取得できたりします。Temporalでは同様の方針は採用されませんでした。一時期は「JavaScriptの言語仕様自体に外界の情報を得る手段をこれ以上増やすべきではない」との観点から仕様としてはそのような手段を用意しないべきではという議論もされましたが、さすがに何かあったほうがいいだろうという方向に話が進んでいるように見えます(参考)。一案として以下のようなAPIが出ています。
import { now, timeZone } from "std:temporal/now";
now(); // 現在時刻を表すInstantが返る
timeZone(); // 現在地のタイムゾーンを表す文字列が返る
// この2つの情報から現在時刻を表すZonedDateTimeが作れる
const currentTime = new ZonedDateTime(now(), timeZone();
まとめ
というわけで、現在使用策定中のTemporalについて紹介しました。旧来のDate
に比べると様々な点で改良されています。日付の計算はplus
とminus
くらいしかありませんが、結構それで事足りることもあるでしょう。また、記事中でちらっと触れた通り、人間向けのフォーマッティングについても別個に議論されるようです(それ系はIntl
という別の仕様の範疇となる可能性が高いです)。
後半のAPIの話をよく読んだ方は**CivilDateTime
からZonedDateTime
にどうやって変換すんのこれ**などの鋭い疑問を持ったかもしれません。自分も持ちましたが、まだAPIが整理されていないということで勘弁してあげましょう。実はこの記事がベースとしているのはつい2週間ほど前にドラフトが書き上がったばかりの文書で、実際自分も読んでいる途中にいくつもおかしな箇所を見つけました。仕様化が終わるまでには直っているでしょう。
それよりも、おおよそのTemporalの方向性をこの記事で理解していただけたら幸いです。Civil系、Instant
、OffsetDate
、そしてZonedDateTime
というようにオブジェクトを細分化し、今どの情報を持っていてどの情報を持っていないのかということを明示できるようにしたのはDate
からの大きな進歩です。
個人的には、Temporalが実際に利用可能になるには速く進んでもあと2〜3年はかかりそうだと考えています。いざTemporalがブラウザに搭載されたときに解説記事を書こうとした人がn年前に書かれたこの記事を見て悔しがるのが今から楽しみです。
リンク
- Temporal Proposal - プロポーザルの文書です。
-
What about Temporal in JavaScript - Temporalを紹介する恐らく唯一の既存日本語資料です。2019年2月のスライドなのでAPIが少し古いです(
ZonedTimeZOne
ではなくZonedInstant
になっていたりOffsetDateTime
が存在しないなど)。 - Fixing JavaScript Date - Getting Started - Temporalを作るモチベーションを解説する文書です。
-
厳密には
Date
はオフセット(世界標準時からの時差を数値で表したもの)でタイムゾーンを表しています。Temporalはタイムゾーンをそのまま扱うことができる点で進化しています。 ↩ -
このPolyfillは仕様の策定を担当している方が片手間に作った感じのもので、めちゃくちゃしっかり作られているわけではありません。実際、この記事を書いている間に筆者は1個バグを見つけました。 ↩
-
ここでは「全世界で共通の時間軸」や「その上の時刻」という程度の意味で絶対時間と言っています。この言葉で検索すると哲学とか物理学の話題が出てきて怖いのですが、あまりその方面から突っ込みを入れてくださらないようにお願いします。 ↩
-
イギリスが夏時間の場合は8時間ですが。 ↩
-
なお、この情報は閏秒を考慮していないため、その分だけ実際の経過時間(物理的に1970年1月1日0時0分0秒から経過した時間)とはずれがあります。コンピュータにおける時刻表現は基本的に閏秒を考えていないのでTemporalが特別に劣っているわけではありませんが。閏秒を厳密に処理する必要がある場合は閏秒のデータを用意して自分で処理する(あるいはそういう処理をやってくれるライブラリを使用する)必要があります。 ↩