かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript の Date は難しい。日本時間(JST)で有効期限を設定したい!

JavaScript の Date は罠が多い。
local 環境だといい感じに日本時間扱いにしてくれるが、デプロイしたサーバーだと時間がサーバーの locale になってズレてしまうとかある。(それはそう)

Note: While the time value at the heart of a Date object is UTC, the basic methods to fetch the date and time or its components all work in the local (i.e. host system) time zone and offset.
cf. Date - JavaScript | MDN

🏁 Goals

今回は日本時間で時間制限をしたい要望だったので、入力側の日時を日本時間 (JST) として、サーバーが UTC で比較しても問題が起こらないようにしたい

local 環境で new Date したときの例

new Date();
// Sat Jun 22 2024 08:00:00 GMT+0900 (日本標準時)

new Date('2024-06-22');
// Sat Jun 22 2024 09:00:00 GMT+0900 (日本標準時)

new Date('2024-06-22T00:00:00');
// Sat Jun 22 2024 00:00:00 GMT+0900 (日本標準時)
  1. 時間が指定されてないと、UTC 00:00:00 扱いで localの日本時間では +9時間された日時になる
  2. 時間を指定すると local の日本時間の日時になる ※ ただし local 環境なら

一見すると 2 の方法で時間指定すればよいように見えるが、サーバーの locale が日本時間 (JST) でなければ、サーバーの locale の時間になってしまう。(サーバーの locale が GMT なら new Date('2024-06-22T00:00:00')GMT の 0時0分になる)

日付は内部的には単一の数値であるタイムスタンプで表されます。タイムスタンプを操作する際には、タイムスタンプを構造化された日時表現として解釈する必要があります。タイムスタンプを解釈する方法は常に 2 つあります。ローカル時刻として解釈する方法と、世界標準時を定義する協定世界時 (UTC) として解釈する方法です。ローカルタイムゾーンは Date オブジェクトには格納されず、ホスト環境(ユーザー端末)によって決定されます。
cf. 日時の成分とタイムゾーン Date - JavaScript | MDN

タイムゾーンオフセットがない場合、日付のみの形式は UTC 時刻として解釈され、日時形式はローカル時刻として解釈されます。
cf. 日時文字列形式 Date - JavaScript | MDN

日時にタイムゾーンを追加して日本時間で実行環境にかかわらず日本時間 (UTC+9) の日時に固定すれば良い

ISO 8601拡張形式でタイムゾーンを省略しなければ、実行環境の locale に関わらず指定したタイムゾーンの日時として解釈される
日本時間 (UTC+9) を表すには YYYY-MM-DDThh:mm:ss+09:00 又は YYYY-MM-DDYhh:mm:ss+0900 とすればよい (秒が不要なら ss を省略しても問題ない)

指定する有効期限をこの形式にし、getTime()UNIX time (ミリ秒)にして現在の日時 (Date.now()UNIX time (ミリ秒) を返す) と比較させればOK

The Date.now() static method returns the number of milliseconds elapsed since the epoch, which is defined as the midnight at the beginning of January 1, 1970, UTC.
cf. Date.now() - JavaScript | MDN

const isExpired = (expired: {date: string; time: string;}) => {
  // `YYYY-MM-DDThh:mm:ss+09:00` 形式にする
  const limitJTSDateTime = new Date(`${expired.date}T${expired.time}+09:00`);
  const now = Date.now();
  
  return limitJTSDateTime.getTime() <= now; 
};

// 実行日時 2024-06-24 08:00 JST

isExpired({date: '2000-06-24', time: '07:59'});
// -> true

isExpired({date: '2024-06-24', time: '08:00'});
// -> true

isExpired({date: '2024-06-24', time: '08:01'});
// -> false

日付・時間の入力形式をミスったら終わる関数だけど、こんな感じでタイムゾーンを設定してしまえば 日本時間 JST で有効期限を作ることが出来た!

おわり。

用語の整理

UTC (Coordinated Universal Time)

協定世界時
原子時計を用いて算出した時間
現在の世界で標準時として使われる

1982年1月1日、国際電気通信連合(UIT)の決定を受けて、GMTに代わりUTC協定世界時)が採用されました。地球の自転ゆえに、1日の長さは年間を通して一定ではないため、世界時を補正するためにUTCが採用されたのでした。
UTC国際原子時(TAI)によって導き出されており、その差はわずかな整数秒、現在は32秒のみです。このようなうるう秒が国際地球回転事業の主導によって追加され、ここ何年にもわたって概ね、太陽はUTC12時に必ずグリニッジ子午線を超えるようになっており、両者の差は10分の9秒以内とされています。

cf. FH - GMT、UTC(協定世界時)、 TAI (国際原子時)

Coordinated Universal Time協定世界時の略称です。
原子時計の時刻(国際原子時)と、地球の自転に基づく時刻(世界時)のずれを補正するために、うるう秒で調整されています。

GMTは、ロンドンのグリニッジ天文台を経度0度と定め、太陽の南中時刻を午後0時と定めた仮想的な時間です。これに対して、UTC原子時計と精密な天体観測に基づくはるかに精度の高い時間です。
両者は、目的も概念も算出方法もまったく異なりますが、日常生活に不都合のない精度の時間測定(整数の秒)であれば、ほぼ差がないと考えて問題ありません。

cf. UTC|時計の知識 | シチズンウオッチ オフィシャルサイト [CITIZEN-シチズン]

JavaScript では toUTCString() で取得で UTC 時間に変換できる

const event = new Date('14 Jun 2017 00:00:00 PDT');
// Wed Jun 14 2017 16:00:00 GMT+0900 (日本標準時)

event.toUTCString()
// 'Wed, 14 Jun 2017 07:00:00 GMT'

Date.prototype.toUTCString() - JavaScript | MDN

GMT (Greenwich Mean Time)

グリニッジ標準時グリニッジ平均時
グリニッジ天文台を通る経度0度(本初子午線)を基準に作られた時間。1日24時間、経度が-180〜180度なので、15度あたり1時間の時差になる
現在の国際的な基準時刻は概念を修正した協定世界時 (UTC) を用いている

1884年10月のワシントン国際会議は、とりわけフランスと英国との間の激しい議論を経たうえで、経度計測のための本初子午線をグリニッジ子午線とすると決定しました。
この子午線は、タイムゾーンや、標準時、すなわちGMTグリニッジ標準時)を決定するためにも用いられました。1日は24時間ですので、経度が15°(1時間)ずつ異なる24の区域に地球を「分割」し、各区域の中央の時間をその区域の時間とします。GMTは、正午に計算された平均太陽時のことです。GMTシステムは1885年1月1日に全世界で採用されました。

cf. FH - GMT、UTC(協定世界時)、 TAI (国際原子時)

昔、世界で基準とされていた、天体観測(地球の自転)に基づいて決められた時間のことです。英国にあるグリニッジ天文台が基準とされたことから、GMTGreenwich mean time)と呼ばれています。
cf. GMTとUTCの違いについて教えてください。 | シチズンウオッチ オフィシャルサイト [CITIZEN-シチズン]

UTCGMT の違い

国際協定により、UTCGMTと等しいことになっていますが、その測定方法は異なります。GMTは正午から測定されており、一方、UTCは午前0時から測定されています。UTCは世界の法的な時刻の基礎となっています。
cf. FH - GMT、UTC(協定世界時)、 TAI (国際原子時)


UTCGMTが近似的に同一視される事もある。航法や通信の分野で UTC と一般的に同義語として認められる[3][4]。 また、GMT は時刻の最大精度が整数秒である法令、通信、常用その他の目的では UTC の意味で使用される。一方、GMT は天測航法及び測量における暦の独立引数としては世界時の UT1 の意味で引き続き使用される。
cf. グリニッジ標準時 - Wikipedia

UTCGMT は厳密には異なるが、JavaScript を用いる際には同じものとして捉えても大きな問題はなさそう
現に toGMTString() メソッドは . toUTCString()エイリアスとなっている

The toUTCString() method of Date instances returns a string representing this date in the RFC 7231 format, with negative years allowed. The timezone is always UTC. toGMTString() is an alias of this method.
cf. Date.prototype.toUTCString() - JavaScript | MDN

const d = new Date('2024-06-22T00:00:00+0900');
d.toUTCString();
// 'Fri, 21 Jun 2024 15:00:00 GMT'

d.toGMTString();
// 'Fri, 21 Jun 2024 15:00:00 GMT'

.toUTCString() したら 00:00:00 GMT って返ってきた。locale やマシンにもよって結果が異なる可能性はある

JST (Japan Standard Time)

日本標準時
協定世界時 (UTC) を東経135度分の時差 9時間 進めた時刻 → UTC+9 UTCとの差を示す場合は UTC+9UTC+0900 なと表記される

日本国の法令上では「中央標準時」とされている。
中央標準時 (Japan Central Standard Time) は国立天文台が運用している原子時計で決定されているので、厳密には国立研究開発法人情報通信研究機構(NICT)が生成する標準時とは異なるが、日本標準時 (JST) も 中央標準時 (JCST) も UTC+9 として問題ない

NICTが通報する標準時と、国立天文台が決定・現示する中央標準時との関係については、どちらの機関も国際原子時の作成に寄与する原子時計を運転し、それらの時計で決定する協定世界時UTC)+9時間をそれぞれ標準時、中央標準時としているが、いかに不確かさが小さい(正確度と精度に優れた)時計であっても、同一の時計ではないので完全に時刻が一致することはない。これについて、NICTを所管する総務省国立天文台を所管する文部科学省は、共同告示により、NICTが通報する標準時については国立天文台の決定する中央標準時により、その偏差を算出し、これをNICTにおいて公表するとしている。
cf. 日本標準時 - Wikipedia

歴史的経緯・管轄の違いで原子時計を別々に運用して時間を生成してるっぽい…

ISO8601

日付と時刻の表記に関する国際規格
日付と時刻は T で区切り、末尾に UTC なら Z, それ以外のタイムゾーンUTCからの時差を + - に続けて書く
日本時間なら +0900+09:00

基本形は YYYYMMDDThhmmss.sssZ (e.g. 20240622T04:30:00.000+0900)
拡張形式は YYYY-MM-DDTHH:mm:ss.sssZ (e.g. 2024-06-22T04:30:00.000+09:00)

JavaScript では日付は - を含んだ拡張形式を使う (視認性的にも拡張形式のほうが良さそうだし)

new Date('20240622T04:30:00.000+0900')
// Invalid Date

new Date('2024-06-22T04:30:00.000+0900')
// Sat Jun 22 2024 04:30:00 GMT+0900 (日本標準時)

ECMAScript defines a string interchange format for date-times based upon a simplification of the ISO 8601 calendar date extended format. The format is as follows: YYYY-MM-DDTHH:mm:ss.sssZ
cf. ECMAScript® 2025 Language Specification

ミリ秒や秒は省略しても問題ない

new Date('2024-06-22T04:30+0900')
// Sat Jun 22 2024 04:30:00 GMT+0900 (日本標準時)

日付のみ UTC Z 時刻として解釈される。時間がある場合はタイムゾーンを省略してもローカル時間として解釈される

⚠ 時間を指定するとタイムゾーンがなくてもローカル時間になる
UTCの時間にしたい場合は必ずタイムゾーン Z を追加する必要がある

new Date('2024-06-22');
// Sat Jun 22 2024 09:00:00 GMT+0900 (日本標準時)

new Date('2024-06-22T04:30');
// Sat Jun 22 2024 04:30:00 GMT+0900 (日本標準時)

new Date('2024-06-22T04:30Z');
// Sat Jun 22 2024 13:30:00 GMT+0900 (日本標準時)

JavaScript では toISOString()UTC (タイムゾーン Z)の ISO8601 形式の文字列が取得できる

const d = new Date('05 October 2011 14:48 UTC');
d.toISOString();
// '2011-10-05T14:48:00.000Z'

const d = new Date('2024-06-22T04:30:00+09:00');
d.toISOString();
// '2024-06-21T19:30:00.000Z'

Date.prototype.toISOString() - JavaScript | MDN

Unix time (UNIX時間)

UNIX 時間はタイムスタンプを表す方法の一つで、通常、 UNIX 元期の開始(1970 年 1 月 1 日午前 0 時 (UTC))からの秒数で定義されます。うるう秒は無視されます。
cf. Unix time (UNIX 時間) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN


A JavaScript date is fundamentally specified as the time in milliseconds that has elapsed since the epoch, which is defined as the midnight at the beginning of January 1, 1970, UTC (equivalent to the UNIX epoch). This timestamp is timezone-agnostic and uniquely defines an instant in history.
The epoch, timestamps, and invalid date Date - JavaScript | MDN

JavaScript では UNIX time の起点 1970 年 1 月 1 日午前 0 時 (UTC) を epoch (元期) と呼んでいる。UNIX元期 (UNIX epoch) と同じ
UNIX time は UNIX 元期からの経過時間(数値)なので、タイムゾーンに依存しない時間を指すことができるので、タイムスタンプとして利用できる

JavaScript では getTime()UNIX time がミリ秒として取得できる

const jstDate = new Date('2024-06-22T04:30+0900');
jstDate.getTime();
// -> 1718998200000

const utcDate = new Date('2024-06-21T19:30Z');
utcDate.getTime();
// -> 1718998200000

jstDate === utcDate;
// -> false
jstDate.getTime() === utcDate.getTime();
// -> true
jstDate.toISOString() === utcDate.toISOString();
// -> true

[参考]

`