unixtimeが秒かミリ秒か毎回迷うのに疲れて、桁数で自動判定する日時変換ツールを作った
SQLiteのタイムスタンプ列に秒とミリ秒が混在し、new Date()に秒を渡して1970年を出した。JSのDate.now()はミリ秒でPHPのtime()は秒という単位の不統一、2038年問題の理由、桁数で単位を自動判定するunixtime変換ツールを作った話。
VPSでSQLiteに書き込むcronの自動処理をいくつか回している。デバッグでテーブルを覗くと、タイムスタンプ列にこういう数字が並んでいる。
1784600000
1784600000000
上は秒、下はミリ秒。同じ自分が書いたコードなのに、テーブルによって単位が違う。片方はNodeのDate.now()をそのまま入れていて、もう片方はSQLite側のunixepoch()で入れていたからだ。SELECTするたびに桁を数えて「これはどっちだっけ」とやるのが本気で嫌になったので、貼れば桁数から単位を自動判定して日時にしてくれる変換ツールを作った。それが**UNIXタイム変換**で、この記事はその背景にある「秒とミリ秒の取り違え」の話だ。
取り違えると、1970年か西暦5万年台に飛ぶ
秒とミリ秒の混同が厄介なのは、エラーにならないことだ。数値としてはどちらも正当なので、コードは黙って間違った日付を返す。
JavaScriptのnew Date()はミリ秒を期待する。ここに秒を渡すとこうなる。
new Date(1784600000) // 1970-01-21 — 秒をミリ秒として解釈
new Date(1784600000 * 1000) // 2026-07-21 — 正しい
1784600000ミリ秒は約20日なので、エポック基準の1970年1月1日から3週間後、つまり1970年1月21日が出てくる。逆にミリ秒の値を秒として扱うと、時間が1000倍に膨らんで西暦5万年台の日付になる。画面に「1970/01/21」や「57000年」が出たら、ほぼ確実にこの取り違えだ。逆に言えば、あり得ない日付が出てくれる分だけまだ親切で、本当に怖いのは日付を画面に出さず数値のまま比較しているケースだった(後述する)。
単位は言語・システムごとにバラバラ
そもそもなぜ混在するかというと、エポックからの経過時間の単位が環境ごとに違うからだ。自分が実際に触った範囲だけでもこれだけ割れている。
- JavaScript:
Date.now()もgetTime()もミリ秒 - PHP:
time()は秒 - Python:
time.time()は秒(小数で細かい精度がつく) - JWT:
expやiatのクレームは仕様上秒 - SQLite:
unixepoch()やstrftime('%s','now')は秒
SQLiteには日時型がなく、タイムスタンプ列はただのINTEGERだ。だからNode側からDate.now()で書けばミリ秒が入り、SQL側でunixepoch()を使えば秒が入る。どちらも正常に動いてしまうので、混在に気づくのはJOINや期間比較の結果がおかしくなったときになる。うちのDBの単位が割れたのも、あるテーブルは挿入をJS側で組み立てていて、別のテーブルはINSERT文のデフォルト値に任せていた、というだけの理由だった。
数値のまま比較しているケースが怖いというのはここで、たとえばJWTの有効期限チェックでやりがちなのがこれだ。
if (payload.exp < Date.now()) { /* 期限切れ */ }
expは秒(今なら10桁)、Date.now()はミリ秒(13桁)。この比較は常に真になり、発行した直後のトークンも全部期限切れ扱いになる。日付表示なら一目でおかしいとわかるが、booleanの比較結果だけ見ていると「なぜか全員ログインが切れる」という症状から掘ることになる。Date.now() / 1000にするか、expを1000倍するか、どちらかに揃えるまで気づきにくい。
桁数を見れば、実はほぼ判定できる
で、切り分けのたびに桁を数えていて気づいたのは、現在前後の時刻なら桁数だけで単位がほぼ確定するということだった。
| 単位 | 現在時刻の桁数 | 例 |
|---|---|---|
| 秒 | 10桁 | 1784600000 |
| ミリ秒 | 13桁 | 1784600000000 |
| マイクロ秒 | 16桁 | 1784600000000000 |
きれいに3桁ずつずれる。しかも秒が11桁に到達するのは西暦2280年代なので、現実のログやDBに入っている値なら重なりを心配する必要がない。**UNIXタイム変換ツール**はこのルールをそのまま実装していて、整数部が11桁以下なら秒、12〜14桁ならミリ秒、15桁以上ならマイクロ秒として扱う。どう判定したかは入力欄の下に「自動判定: ミリ秒(整数部 13 桁)」のように明示するので、判定を疑う余地も残らない。境界付近の値を手動で指定したければプルダウンで切り替えられる。
もうひとつ、作っていて必須だと確信したのがJSTとUTCの並列表示だ。unixtime自体はタイムゾーンを持たない世界共通の値だが、変換した瞬間にタイムゾーンの問題が始まる。サーバーのログはUTC、自分の頭はJST、この2つは9時間ズレる。「ログのこの行は日本時間で何時なのか」を確認するとき、片方しか出ないツールだと結局頭の中で9時間足すことになり、そこでまた間違える。なので同じ値をJST・UTCの両方で、ISO 8601・RFC 2822・日本語表記の3形式ずつ並べる作りにした。変換はすべてブラウザ内で完結するので、業務のログの値を貼っても外部には送信されない。
2038年問題が「2038年1月19日」である理由
ツールには早見表も置いていて、その中に2147483647という値がある。これは32bit符号付き整数の最大値(2の31乗マイナス1)で、unixtimeとしては2038年1月19日 03:14:07 UTCにあたる。
古いシステムはunixtimeを32bitのtime_tで持っていて、この秒数を超えた瞬間にオーバーフローして負の値、つまり1901年に巻き戻る。これがいわゆる2038年問題だ。C言語の古いコードやMySQLのTIMESTAMP型などが影響を受ける。2026年のいま現在時刻は1784600000前後なので、上限まで残り3億6千万秒ちょっと、およそ11年半。遠い未来の話でもなくなってきた。
自分がこの値を早見表に入れたのは教養のためではなくて、実務で「有効期限を遠い未来にしたい」ときに雑に9999999999(10桁の上限)や2147483647を入れるコードに出会うからだ。前者は2286年で実質問題ないが、後者は2038年に本当に期限が来る。見かけたらツールに貼って日付を確かめると、その値が安全かどうか一発でわかる。
cronのログとunixtimeはセットで出てくる
最後に運用の話。この手のunixtimeを見る場面は、自分の場合ほぼcronのログとSQLiteの中身だ。cronの設定自体も罠が多くて、書式の順番ミスやPATH問題で丸一日溶かした話はVPSのcronが動かない原因の記事に書いた。スケジュール式を組むときは**cron式ジェネレータ、ログに出てきたタイムスタンプを読むときはUNIXタイム変換**、と自作ツールを2つ往復する運用になっている。
変換ツール自体は世の中にいくらでもある。それでも自分で作ったのは、「秒かミリ秒か」を人間が考える工程を消したかったからで、桁数の自動判定・JST/UTC並列表示・判定根拠の明示はその一点のためにある。テーブルの桁を数える作業は、もうやらなくてよくなった。