【翻訳】Date and Times in JavaScript

この記事は littledan から依頼を受けて、翻訳しています。広く Date and Times の JS プロポーザルについて意見がほしいとのことです。 意見は以下の場所にポストできます。

docs.google.com

原文: blogs.igalia.com

tl;dr:

Temporal のプロポーザルについてフィードバックを求めています。 Polyfill を試したら、サーベイの回答を送ってください、ただしまだ本番環境では使わないでもらいたいです。

JavaScript の Date クラスは壊れています、しかし Breaking the Web を起こさずに修正する方法はありません。風のうわさでは、 Date クラスは 10 日で作られた JS Engine のhackに含まれたもので、 java.util.Date に基づいたものと言われています。しかも java.util.Date は摩訶不思議なAPIだったため、 1997 年には deprecated になっていて、今ではより良い API のものが使われています。 Java が歩んできた歴史はそのまま JavaScript も同じ歴史を辿ろうとしています。 built-in のDateクラスはとても使いにくいものとして残り続けることでしょう。

2, 3年前から、とある proposal が開発され始めました。その proposal は新しくグローバルに Temporal という JavaScript のオブジェクトを生やすものでした。 Temporal は robust で modern な API 設計で、日付、時刻、タイムスタンプを扱えるものになっています。さらに Date では不可能だったり困難だったこと下記のことが簡単にできるようになっています。

Date では不可能だったり困難だったこと:

  • Timezonesをまたいだ日付変更
  • 夏時間を考慮した上での時刻の加算、減算処理
  • 日付だけ、時刻だけでの処理
  • non-Gregorian カレンダーの時間を扱うこと

さらに Temporal は "just works (うまく動く) " なデフォルトの動作を持っていますが、コーナーケース(オーバーフロー、曖昧な時刻の解釈、などなど)に対応するために opt-in での制御ができるように設計されています。この proposal の歴史と「なぜ Date そのものを直せないか」をさらに知りたければ Maggie Pintのブログを読むことをオススメします。

maggiepint.com

Temporal の能力についていくつか例を挙げます。cookbookを見てみてください。 これらの例の多くは legacy Date クラスには困難なものが多いです。特に timezone の取り扱いに関しては Date では難しいものが多いです。 (ただし、このポストでは例を上げてますが、コードに関しては変更される可能性が大いにあります。)

この proposal は現時点では TC39 のプロセス内で Stage 2 になります。我々 Temporal Interest Group はすぐに Stage 3 に持っていこうとしてます。これまで Temporal の機能セットやAPIに関して長い間活動してきました。そしてついに、我々は 必要十分な機能が揃っており、 API の妥当性を信じる所まで来ています。もちろんホワイトボード上の議論だけで良いAPIの設計はできません。 JavaScript 開発者コミュニティに試してもらい、実際に要望を満たしているかを確認する必要があります。

まだまだ初期フェーズなので、 drastic な変更も必要になったらする可能性があります。ただしそれはフィードバックに基づいたものです。だから、どうか Temporal を試した上で我々にフィードバックを送ってください。

Temporal の試し方

カジュアルに試すのであれば、インタラクティブなpromptを使えば簡単です!ブラウザを開いて、 API ドキュメントページに行ってください。APIドキュメントもしくは cookbook のページのどこでも、ブラウザコンソールから Temporal をロードされた状態で試すことが可能です。 Example のコードを実際に実行して試すことが可能です。もしくは RunKit 上でも試すことが可能です。

もしくは、あなたは Temporal を使った小さなテストプロジェクトを構築するような踏み込んだ評価に興味があるのかもしれません。あなた方の貴重なプロジェクトの時間を奪うようなことになってしまうかもしれませんが、その方法が最も価値のあるフィードバックになるでしょう。踏み込んだ評価をしてくれることに深く感謝します! npm 上に既に Temporal API の polyfill をリリースしています。 npm install --save proposal-temporal をあなたのプロジェクトの中で実行すれば、 Temporal をあなたのプロジェクトの中に取り入れることができます (const { Temporal } = require('proposal-temporal');) 。

しかし、アプリケーションの本番環境では polyfill は使わないでください! proposal はまだ Stage 2 です。さらに polyfill は 0.x version です。それは API が変更される可能性が高いことを示しています。我々はあなたがたからのフィードバックを受けたらそれに基づいてAPIを変更し続けていこうとしています。

フィードバックの送り方

再三申し上げましたが、Temporal を使った上で、あなたからの意見を聞きたいです!一度でもトライしたら、短いサーベイに記入してください。もしよければ、あなたへのコンタクト情報を教えて下さい。教えていただければ、質問にはフォローアップして回答を差し上げます。

我々の issue tracker も気軽に開いてください! 我々はあなたがサーベイに記入したかどうかに関わらず提案を喜んで受け入れます。 issue 内に書かれたフィードバックは閲覧できます、また賛成意見には thumbs-up を送ったり、反対意見には thumbs-down を送ることもできます。

参加していただけるのであれば、謝辞を述べます。私達が受け取った全てのフィードバックは proposal を Stage 3 に上げるため、またブラウザ内で Temporal を利用できるようにするための正しい決定をするのに役に立てていきます。

番外編 (筆者が試したところ)

というわけで、 Temporal を試してみた。まずすごいと思ったのは公式が用意している cookbook が気合が入ってる。

tc39.es

9種類の大きなユースケースを紹介した上で、それぞれのサンプルコードが書かれている。

const future = Temporal.Date.from("2020-08-28")
const today = Temporal.now.date()
const days = future.difference(today).days; // 31

これだけで、日付からの差分が出せるところは嬉しい。あとは細かく、 Calendar形式で1日追加するとかの細かい操作や timezone の変更などの操作が直感的にできる。 <input type=date> な field にも直接入れることができたりと細かいニーズに対応できている。さすが次世代 Date といったところか。

const datePicker = document.getElementById('calendar-input'); // カレンダーのDate Picker
const today = Temporal.now.date(); 
datePicker.max = today; // 現在日付をmaxにして、過去の日付しか選べないようにする。
datePicker.value = today;

若干使いにくそうだな、と思った所も上げる。

使いにくいと思った所その1, epochtime を取る所

// epochsecond を取る時は Date.now() / 1000 だったが、今回からはこうなる。
const now = Temporal.now.absolute();
now.getEpochSeconds();

まず、長い。長いけどAPIはわかりやすい。一方で手軽さがないので、 Date.now() / 1000 を手癖で覚えてるとそっち使ってしまいそう。ただDate.now()/1000 と比較すると、小数点のケアをしなくていいので、楽。これからは Temporal の方法を覚えていきたい。

一方で absolute instance を一度作ってしまうと次から getEpochSeconds() を呼び出しても同じ値になってしまう。一回はこれ絡みの不具合が起きそうな気がする。

const now = Temporal.now.absolute();
setInterval(() => {
  console.log(now.getEpochSeconds()); // 毎回同じ数
}, 1000);

最新の値を更新したければ毎回 absolute から取るしか無さそう。

setInterval(() => {
  const now = Temporal.now.absolute();
  console.log(now.getEpochSeconds()); // 毎回違う数
}, 1000);

epoch seconds を取るときはロギングする時とかが多い気がするが、それを毎回 Temporal instance を作っては捨てるようなことしてもパフォーマンス的に問題ないのかとかは一瞬気になったが、実装次第だろう。個人的にはinstanceを作らなくても epochtime くらいならサクッと取れるようにしてほしい。 Temporal.getEpochSeconds() のように。

Temporal.xxx.from でパースできない文字列を食わせると例外がthrowされる。

from 関数は本当に便利。日付だけで扱いたければ Temporal.Date.from(2020-01-01) で日付のみの Temporal instance を作ってくれる。また、時刻だけで扱いたければ Temporal.Time.from("12:10") で 12時10分 のTimeにしてくれる。また、 日付のオブジェクトを渡せばそれに合わせて instance を作ってくれる。

ただし、 ISODate 形式(YYYY-MM-DDTHH:mm)に反する文字列だと例外が throw される。また、ありえない日付(13月32日のような)ものを食わせると、object形式とstring形式で動きが異なる。

Temporal.DateTime.from("2019-12-10T12:10") // from 関数は文字列を parse して Temporal instance にしてくれる。便利。
Temporal.DateTime.from({ year: 2019, month: 12, day: 1 }) // こっちもわかりやすい、object としてpropertyつけて渡せば良い。
Temporal.DateTime.from("2019-12-10a12:10") // もしも invalid な文字列だと RangeError: invalid ISO 8601 string: 2019-12-10a12:10
Temporal.DateTime.from("2019-13-32T12:10") // 文字列にありえない月や日が入ってても RangeError がthrowされる。
Temporal.DateTime.from({ year: 2019, month: 13, day: 32 }) // object で渡したときは throw されない、 2019-12-31 などの一番近い日付に補正される。
Temporal.DateTime.from({ year: 2019, month: 13, day: 32 }, {disambiguation: "reject"}) // disambiguation を reject にすれば RangeError が throw される。

ここは、若干動きがわかりにくいかも。。。文字列で渡したときは disambiguation が必ず "reject" のようになるが、object で渡したときは disambiguation が "constrain" になる。 polyfill の問題かな。

ちなみに、これまでの Date だと invalid Date という number で言う NaN のような扱いになっており、例外は throw されなかった。

new Date("2012-12-11T12:20") // 2012-12-11T03:20:00.000Z
new Date("2012-12-11a12:20") // Invalid Date

どっちが良いかは諸説あると思うが、いままで手軽に扱っていた Date のマインドセットのままでは使えない。ちなみに moment とかも同様にパースできなくても例外を投げたりしない、 NaN 扱いになる。パースできない時に NaN 扱いだった Date ときちんと例外を処理しないといけない Temporal のどちらが良いかは筆者には判断付かないが、 JavaScript はこれまでの歴史上、例外の取り扱いが難しいので、この辺の議論は是非参加してみたい。

まだまだありそうだけど、翻訳を先に出したかったので、ここまで。後は読者の方々も試してみてほしいです。試してみたら、サーベイに回答しましょう。