「Sat Dec 30 1899 10:30:00 GMT+0900」
管理画面に表示されたこの文字列を見て、一瞬思考が止まった。1899年。スプレッドシートに入力したのは今日の10時30分のはずだ。なぜ125年前の日付が表示されているのか。
デバッグの迷子
最初にフロントエンドを疑った。JavaScriptのDateオブジェクトの扱いがまずいのだと思い、表示処理を書き直した。変わらなかった。
次にAPIの形式を疑った。GASからのレスポンスのJSON構造が違うのかと思い、受け取り部分を書き直した。変わらなかった。
1日目が終わった。
2日目の午前中、GASのコンソールにconsole.log()でデータを出力して確認した。GASがスプレッドシートから取得した時刻データが、すでに「Sat Dec 30 1899 10:30:00」という文字列になっていた。原因はGASの中にあった。
問題はここにあった
GoogleスプレッドシートのセルをGASでgetValues()すると、セルの書式に応じて型が変わる。数字は数値として、テキストは文字列として返ってくる。そして日付・時刻として書式設定されたセルは、JavaScriptのDateオブジェクトとして返ってくる。
これ自体は正しい挙動だ。問題は「時刻専用のセル」だった。
スプレッドシートで「10:30」と入力すると、スプレッドシートの内部では「1899年12月30日 10:30:00」という日付として保存される。GoogleスプレッドシートはExcelの時刻の扱いを踏襲しており、時刻だけのデータには「時刻シリアル値の基準日」として1899年12月30日を使っている。GASがこれをDateオブジェクトとして返すと、1899年がついてくる。
日時が複数の列にわたっていて、列によってDate型だったり文字列だったりと混在していた。これが型の不一致を引き起こしていた。
解決策は2行
GASで値を読み出した後、instanceof Dateで型を確認する。
const d = (rows[i][2] instanceof Date) ? rows[i][2] : new Date(rows[i][2]);
Dateオブジェクトであればそのまま使う。そうでなければnew Date()でDateオブジェクトに変換してから扱う。
タイムゾーンの指定も忘れてはいけない。GASのデフォルトタイムゾーンはプロジェクト設定に依存する。明示的にAsia/Tokyoを指定することで時刻がずれる問題を防ぐ。
Utilities.formatDate(d, 'Asia/Tokyo', 'HH:mm')
この2つをセットで書けば、1899年が出てくることはない。
フロント側にも手が必要だった
GASからJSON文字列で時刻を受け取るフロント側でも、同じ問題が潜んでいた。
new Date('Sat Dec 30 1899 10:30:00 GMT+0900')は正しいDateオブジェクトを返す。しかし日付部分「Dec 30 1899」が表示に紛れ込まないよう、時刻だけを取り出す処理が必要だ。フロントのfmtTime()関数でこれを吸収した。
function fmtTime(t) {
if (!t) return '';
const s = String(t);
if (s.includes('T') || s.includes('Dec 30 1899') || s.includes('Sat ')) {
const d = new Date(s);
if (!isNaN(d)) return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
}
return s;
}
見苦しいコードだが、これが現実だ。125年前の日付が含まれているかどうかを確認するif文が必要になった。
2日を1分にする方法
あとから考えれば、最初にGASのコンソールでデータを確認していれば1分で気づいた。フロントとGASを同時に疑い始めると、原因の切り分けが曖昧になる。
GASを使ったシステムで日付・時刻が絡むとき、「スプレッドシートのセルをGASが何型として読んでいるか」を最初に確認する。それだけで、この種の問題は初日の午前中に終わる。
次にGASでスプレッドシートを扱うときは、日付・時刻列にinstanceof DateチェックとUtilities.formatDate()のAsia/Tokyo指定をセットで書くことが私のルールになった。
*次回は「iPad対応:3列グリッドとタブレットUIの設計」*
*シンプルシステム株式会社 代表 伊藤勝彦*