日付時刻まわりの処理の話

javascript からのネタだけど一般論の話でもある。

Cytoscape.js お試しシリーズで作ってた実例の「声優関連図作り子ちゃん」なんだけれど、日付時刻の扱いで実は結構な「結果論的には正しいけど豪快なインチキ」をやっていて、もしかすると気付く人もいるのかもしらんのだけれど、例えばこんなデータにして使っている:

 1 {
 2   "#": "h6d", 
 3   "dtl": {
 4     "bld": "O", 
 5     "brth": "1996-04-09T15:00:00.000Z"
 6   }, 
 7   "j": "黒沢 ともよ", 
 8   "n": "Kurosawa, Tomoyo", 
 9   "p": "/images/voiceactors/3/30873.jpg"
10 }

この brth が誕生日なわけだけれど、この ISO8601 形式は、もとはといえば people/11661/Tomoyo_Kurosawa に表示されている「Birthday: Apr 10, 1996」を javascript の Date に直接パースさせただけのもの:

1 var rbif = new RegExp("(?:<span>)?(.*):(?:</span>)? (.*)");
2 var m = rbif.exec(e);
3 var v = m[2];
4 result["dtl"]["brth"] = new Date(v);

最初ちょっとビックリしたわよ。Python などのほかのスクリプティング環境でも「ここまで素でよきに計らってくれる日付時刻パーザ」がすぐに使えるのは結構珍しい。昔 C のライブラリでまさに「getdate」だったかそんな名前の「とにかくどんな形式だろうと頑張る」パーザが出回ってた(なんと、YACC で書かれてた)が、なんかそれを思い出した。あるいはそれ使って実現してたりする? かなーり緩い書き方でも受け容れてくれんのね、この子。”10-07-17″ もちゃんと 2017年10月7日だとわかってくれる。いい子だ。(ほかの言語の場合大抵ベースになってるのが C 標準ライブラリの strptime とかなので、「フォーマットが既知」ならそう大変ではないけれど、こういったフリーフォーマットに近いものは結構大変。)

さて。インチキ? そう、Apr, 10。無論これは「合ってる」。標準時に直せば “1996-04-09T15:00:00.000Z” だろ?

こういうインチキがどうして通用するかと言えば、MyAnimeList.net というのが「アニメのサイト」(日本のカートゥーン)だから。つまりは、原則として日付の表現が「日本時間」だからなのね。なので、ワタシのこのインチキ WEB アプリケーションを「日本時間以外で動作してる PC」で動かすと、つまりたとえばブラジルで動かすと、滅茶苦茶な情報になってしまうということ。(海外のサイトなのに日本時間表記というのはまぁ、相当珍しいであろうね。) (なお、このパーズ、ブラウザに結構依存するそうだ。なので、あとで真面目にやり直そうとは思う。)

なので、これを真面目にやりたければ、「MyAnimeList.net が日本標準時基準で記述されている」という知識を実装に埋め込まなければならない。最も安直なのはこんなよね:

1 var rbif = new RegExp("(?:<span>)?(.*):(?:</span>)? (.*)");
2 var m = rbif.exec(e);
3 var v = m[2];
4 result["dtl"]["brth"] = new Date(v + " (JST)");
5 // ex. new Date("Oct 7, 2017 (JST)") => 2017-10-06T15:00:00.000Z 

暗黙で JST で書いてるわけだから、それを読む方も「暗黙で JST であることを知っている」実装にすればいい。

というわけで、「パーズはあとでちゃんとしとこーっと」て話なんだけれど、それとともに。一番上に引用した「JSON」はこれは、まったくもって今時点で「本当にこの形で書き出してる」のね。つまり javascript で動作中はまさしく Date オブジェクトのまんまメモリに保持してて、それを「そのまんま JSON.stringify で JSON 化」すると、引用したそのまんま JSON になる、というわけ。つまり JSON.stringify は Date オブジェクトについての知識を持っていて、toISOString してくれている、ということ。そうなんだけれど、この ISO8601 形式(のフル形式)ってまぁ「かさばる」わけじゃん。特に元データが日付までしか意味がないものなら特に。JSON のサイズが問題になるほど大量のノードを扱うのでね、切り詰めたいよなぁ、と思ってな。なので、「new Date」でちゃんと復元出来る形で、なおかつちゃんとタイムゾーンの情報を含んだ形で書き出したいぞと言うことなら、どうするべかな、と思って。

今気付いた「自覚的インチキではないチョンボ」:

 1 var dateFormatter = function(cell, formatterParams) {
 2     // to get component of cell: cell.getElement()
 3     // to get value of cell: cell.getValue()
 4     var v = cell.getValue();
 5     if (v) {
 6         try {
 7             return v.toISOString().slice(0, 10);
 8         } catch (error) {
 9         }
10     }
11     return "-";
12 }

これは一日ズレちゃうんで、これの真似して書き出すのはダメ。そうではなくて、さっきの例と同じく、この形式で書き出したい:

8601形式を加工するだけで作れそうだし
1 new Date("2017-10-07 (JST)") // => 2017-10-06T15:00:00.000Z 

けどこの「JST」ってどうやって出力できんのかね? あ、この手があるか:

この形式ならすぐに作れる
1 new Date("1996-04-10 GMT+0900") // => 1996-04-09T15:00:00.000Z
2 new Date("1996-4-10 GMT+0900") // => 1996-04-09T15:00:00.000Z
3 new Date("1996-4-10 Z+0900") // => 1996-04-09T15:00:00.000Z
4 new Date("1996-4-10 Z+09") // => 1996-04-09T15:00:00.000Z
5 new Date("1996-4-10 Z+9") // => 1996-04-09T15:00:00.000Z
6 new Date("1996-4-10Z+9") // => 1996-04-09T15:00:00.000Z

ゼロパディングすべきかしてはならないかが結構ブラウザ依存みたいなので、真剣にやるなら気をつけないといけないけれど、

1 var s = "Apr 10, 1996";
2 var dt = new Date(s + " (JST)");  // 情報サイトが前提としている知識は埋め込まざるをえないでしょ
3 dt.getFullYear() + "-"
4     + (dt.getMonth() + 1)
5     + "-" + dt.getDate()
6     + " Z+" + (-dt.getTimezoneOffset() / 60)  // 東半球でしか通用しないコード

おおよそこれに近い感じか。正負の扱いさえちゃんと出来れば西半球でも通用するコードにはなりそうね。


とまぁ「javascript の処理の話」として考えてきたけれど、今からしたいのはそれとは全然関係のない「リアル世界」の話。

先の「黒沢 ともよ」の誕生日をさ、「日本時間の 4月10日だ!」と厳密に言う意味って、実際のところなんなんだろう、という話。無論、「生誕日付時刻」としての話なら別。けど今は「日付」の話をしている。この場合ね、「日本時間の 4月10日だ!」と主張することは要するに「標準時で 4月9日だ!」と主張するのと「完全に等価」でしょう? 時刻の情報を失ってしまっているわけだから、「実際は標準時の 4月9日15時だ!」と完全に等価、となると思う? けど冷静に考えたら違うよね? 「標準時の 4月9日15時」に厳密に一致するのは数ある日本時間4月10日の中で唯一「日本時間の 4月10日0時」だけなんだよね。なので「時刻の情報を失ってしまったあとのタイムゾーンの意味って、ほんとのところは何?」て話。

たとえば「生誕をカウントダウンして祝おうぜ」てことを考える場合、そして「日本人の生誕を海外の人が祝おうと思う場合」、仮に「生誕時刻に祝おうぜ」てことなら厳密な「同一時刻」に祝えばよろしい。そういうことなら(黒沢ともよが何時生まれなのかなんかわからんが仮に深夜1時だとして)「日本時間の10日1時」そのものズバリでなければ意味がない。それがたとえビジネスタイム真っ最中でクソ忙しい10時だろうがその瞬間に祝わねばならない。けれども、「ふーん、4月10日が誕生日なんだぁ、心の中でお祝いしとくねっ」くらいのごく普通の市民ならば、「自分の生きている日常タイムゾーン」の文脈での4月10日に「祝ったとしても」、普通は別に気にもならないだろう。どう? ハリウッドスターの誕生日を祝おう、てときにさ、どうしてる? いちいち現地時間で祝ってますかぁ? (「キリスト生誕際」であるところのクリスマスなんぞその典型だろ? ローカル時間の 12月24日が「違う!」と騒ぐバカはおらんであろ?)


いらん話をした。

話戻すけど、「日付の形式がかさばるので」ということを考える場合、情報としては無論「日付 + UTCオフセット」の組で記録することは必要だけれども、「Date オブジェクトが厳密な意味での解釈が可能なような形式で」にこだわる理由って、「楽だから」以上の意味って実はないんだよね。つまり、「日本時間だよーっ」というグローバルデータを外に一個だけ持って、「誕生日フィールド」には最もコンパクトな例えば「19960410」みたいにしちゃっておいて、解釈するほうが頑張ればいい。

というわけで、ワタシのヤツではそうしとこうかしらね。当然だけど、「サイトとかシステムでタイムゾーンが一貫している」場合にのみ通用する話。ISO8601 はこれが一貫していないもののためにこそある。


追記:
ブラウザ依存の件は W3CSchools に書いてある。これによれば、「YYYY-mm-ddZ+09」なら良さそうだね。(スラッシュはダメ、ゼロパディングしないのはダメ、dd-mm-YYYY はダメ、の3つが書いてある。)ので例えば(ダルいけど)こんなだね:

 1 function toCompactDateString(dt) {
 2     function _pad(n, add_sign) {
 3         var sign = "";
 4         if (add_sign) {
 5             sign = (n < 0) ? "-" : "+";
 6         }
 7         ns = "" + Math.abs(n);
 8         if (ns.length == 1) {
 9             ns = "0" + ns;
10         }
11         return sign + ns;
12     }
13     return dt.getFullYear()
14         + "-" + _pad(dt.getMonth() + 1)
15         + "-" + _pad(dt.getDate())
16         + "Z" + _pad(-dt.getTimezoneOffset() / 60, true);
17 }
18 // toCompactDateString(new Date("Apr 10, 1996 (JST)")); → '1996-04-10Z+09'
19 // new Date(toCompactDateString(new Date("Apr 10, 1996 (JST)")));
20 // → 1996-04-09T15:00:00.000Z

これって 8601 準拠だっけ? 忘れた。まぁ Date が解釈出来るならそれでいい。

ちなみに「いらん話」のなかで「日本時間の、をことさらに主張するのってそんなに意味ないよね」と書いたけど、これはもちろん言い過ぎで、ちゃんと意味がある。日付しか持っていないなら「日本時間で 4月10日00:00~23:59」と解釈出来るが、「どの現地時間での言い方か」の情報を失ってしまうとこれは困る。


さらに追記:
行儀悪いな、Date。不正な形式を渡して「まともな文字列」返してくれやんの…(「Invalid Date」という文字列が返る)。

ロケールに振り回されるわけにもいかんので、せめてこうなのかな:

 1 function toCompactDateString(dt) {
 2     function _pad(n, add_sign) {
 3         var sign = "";
 4         if (add_sign) {
 5             sign = (n < 0) ? "-" : "+";
 6         }
 7         ns = "" + Math.abs(n);
 8         if (ns.length == 1) {
 9             ns = "0" + ns;
10         }
11         return sign + ns;
12     }
13     if (!isNaN(dt.getFullYear())) {
14         return dt.getFullYear()
15             + "-" + _pad(dt.getMonth() + 1)
16             + "-" + _pad(dt.getDate())
17             + "Z" + _pad(-dt.getTimezoneOffset() / 60, true);
18     }
19 }
20 // toCompactDateString(new Date("Apr 10, 1996 (JST)")); → '1996-04-10Z+09'
21 // new Date(toCompactDateString(new Date("Apr 10, 1996 (JST)")));
22 // → 1996-04-09T15:00:00.000Z
23 // toCompactDateString(new Date("hoge")); → undefined


Related Posts