JavaScript である程度プログラムを書いていると1度はハマることになるタイムゾーンの扱いについての自分メモです。
travel_world_jisa



【タイムゾーンとは?】
地球は丸くて、1日は24時間です。ある地点で朝を迎えた時、別のある地点では昼になっていて、更に別のある地点では夜だったりします。同じ瞬間でも時間や日付が異なるわけです。普通はあまり気にしなくていいのですが、「いま何時?」の質問に答えるには「どの地点での話か?」を明確にする必要があります。

この「どの地点での日付の話か?」を地域ごとに定義したものが「タイムゾーン」です。世界標準では GMT と呼ばれる、イギリスのグリニッジ天文台を通過する経線を使います。日本にはタイムゾーンは1つしか存在せず、兵庫県の明石市を通過する東経 135 度の経線のもの(世界標準時よりも9時間早いもの)を「日本標準時」として使います。つまり厳密には日本の東と西では日没時間などは異なりますが、全て「時計は明石市を基準に計測されるものを使う」ことになっています。横に広い国の場合はタイムゾーンが複数存在します。例えばアメリカの場合は「アメリカ東部標準時」、「アメリカ中部標準時」、「アメリカ山地標準時」、「アメリカ太平洋標準時」、「アラスカ標準時」、「ハワイ・アリューシャン標準時」と6つの標準時が存在します(国によってサマータイムとかもありますが、話が更にややこしくなるのでここでは無視します)。例えばあるイベントなども「東部標準時だと午後8時から、太平洋標準時だと午後5時からスタート」のようになります。


【タイムスタンプとは?】
タイムゾーンと少し似た言葉ですが、時間に関係するという以外は全く意味の異なる用語です。タイムスタンプ(UNIX タイムスタンプ)は「ある瞬間を整数で表したもの」です。より正確にはある瞬間が「世界標準時 1970 年 1 月 1 日午前零時から何ミリ秒経過しているか、を整数で表したもの」です。ちなみに日本時間での 2021 年 1 月 22 日午前10時40分40秒ごろに一度計測してみたところ、タイムスタンプの値は 1611279640693 という数値でした。単位はミリ秒なので、ここから1秒経過するごとに 1000 ずつ増えて、1秒さかのぼるごとに 1000 ずつ減っていくことになります。

このタイムスタンプ自体はタイムゾーンの影響を受けません。つまり上述の 1611279640693 という数字は「日本では 2021 年 1 月 22 日午前10時40分40秒(と 0.693 秒)」を示す値であって、別のタイムゾーンだと別の日時(例えばロンドンだと 2021 年 1 月 22 日午前1時40分40秒ごろ)になります。

ソフトウェア・アプリケーションを開発する際に、データの入力日時や更新日時を記録することがありますが、一般的には '2021-01-22 10:40:40' のような日時の文字列では記録せず、タイムスタンプ値を使って記録することが多いです(特にこのシステムが世界中で使われる場合、日時は日本時間として扱われない可能性があるので、後からどのタイムゾーンにでも変換できるようタイムスタンプとして保存するのがよい、とされています)。


【タイムスタンプのタイムゾーン問題】
さて、このようにシステム上において日時はタイムスタンプとして記録されることが多いのですが、一方で画面に表示する際にはタイムスタンプを日時に変換した上で表示されることがほとんどです( 1611279640693 という数字だけを見ても、何年何月何日の何時何分何秒なのか全くわからないので)。

このタイムスタンプから日時文字列への変換を JavaScript で行う場合、何通りかの方法がありますが、例えば以下のような方法が考えられます:
var timestamp = 1611279640693;      //. タイムスタンプ値
var dt = new Date( timestamp );     //. Date オブジェクトに変換
var h = dt.getHours();              //. 時
var m = dt.getMinutes();            //. 分
var s = dt.getSeconds();            //. 秒

var hms = h + ':' + m + ':' + s;    //. 時刻を H:M:S 形式の文字列に変換
   :

タイムスタンプの値を指定して Date オブジェクトを作成し、Date オブジェクトのメソッドを使って時、分、秒の情報をそれぞれ取り出してから H:M:S とコロン区切りの文字列に変換する、という方法です。この例でのタイムスタンプ値は上述のものなので、この結果 hms 変数には 10:40:40 という文字列が代入される、、、 と思われます。

ところが実際に実行してみると、そのようになる場合とならない場合があります。実行環境によって結果が異なってしまうのでした。このならない場合の原因こそがタイムゾーンの問題です。

上述のように 1611279640693 は日本時間で「2021 年 1 月 22 日午前10時40分40秒ごろ」を示すタイムスタンプでした。なので日本時間で動くシステムであれば、この JavaScript の実行結果は "10:40:40" という文字列になるはずです。問題はそのシステムが日本時間で動いているとは限らないことにあります。つまり「日本から使う」想定はできたとしても、そのシステムそのものが「日本にある」とは限らず、日本時間とは別のタイムゾーンが設定されて動いている可能性があるのです。例えばロンドンにあるサーバーで、ロンドンのタイムゾーンが設定されて動いている場合のこの処理結果は "01:40:40" と表示されてしまうことになります(ロンドンタイムゾーンの時間になってしまうので)。同じ日であればともかく、条件によっては別の日に入力されたデータとして扱われる可能性もあります。特に昨今はクラウドサーバーを利用する機会が多く、そのサーバーは日本にはないどころか、「サーバーインスタンスがどこで動いているのかよく分からない」状態で使っていることもあるでしょう。PaaS やコンテナ環境によっては(アプリケーション側からは)サーバーのタイムゾーン設定を変えることができないことも考えられます。


【JavaScript でのタイムゾーン問題】
ここまでは JavaScript に限らず、タイムスタンプを記録している以上はどのプログラミング言語でも起こりうる問題です。JavaScript だと話がもう少しややこしくなります。

アプリケーションが動いているサーバーのタイムゾーン設定が日本とは限らないとわかったとして、ではどのようにして日付時刻を日本時間で表示すればよいでしょうか?

例えばプログラム内でサーバーのタイムゾーン設定を異なる設定(例えば「日本」)に変更することができるのであれば、システム内のタイムスタンプ値を Date オブジェクトに変更し、「日本時間での」日時を取得することができるようになります。また現在のサーバーのタイムゾーン設定を調べることができれば、その値から日本との時差を計算できるので、タイムスタンプ値に時差時間ぶんの数値を加えたり、引いたりすることで、関数実行結果が日本時間のものになるよう調整することもできます。

一般的には前者の方法が使われます。例えばタイムゾーン設定を変更するための setTimezoneOffset() のような関数を使って強制的に日本時間で計算できるようにできればいいのです。が、JavaScript にはこのような関数が用意されていません。つまり JavaScript では前者の方法は使えないのです。

したがって JavaScript では後者の方法を使う必要があります。JavaScript には setTimezoneOffset() 関数は存在していないのですが、設定値を取得するための getTimezoneOffset() 関数は存在しています。例えばこのような感じで使います:
var timestamp = 1611279640693;      //. タイムスタンプ値
var dt = new Date();                //. Date オブジェクトを作成

var tz = dt.getTimezoneOffset();    //. サーバーで設定されているタイムゾーンの世界標準時からの時差(分)
tz = ( tz + 540 ) * 60 * 1000;      //. 日本時間との時差(9時間=540分)を計算し、ミリ秒単位に変換

dt = new Date( timestamp + tz );    //. 時差を調整した上でタイムスタンプ値を Date オブジェクトに変換

var h = dt.getHours();              //. 時
var m = dt.getMinutes();            //. 分
var s = dt.getSeconds();            //. 秒

var hms = h + ':' + m + ':' + s;    //. 時刻を H:M:S 形式の文字列に変換
   :


これでサーバーがどこでどのようなタイムゾーン設定で動いていても、常に日本時間での文字列に変換することができるようになります。


もちろん、このような不便で面倒な方法を使わなければならないというわけではなく、moment などの外部ライブラリを使うことでもう少し楽に扱うことができるようになります。

とはいえ、この事情を理解してないと原因調査も回避するのも難しいと思ってます。というわけでの自分メモでした。