Excel, Google Sheet の日時とタイムゾーン
Google Sheet は Excel との互換性を保つために、 Excel と同じ仕様で日時を扱っている。
以下、 Excel として記述するが Google Sheet も全く同じである。
Excel日時の仕様
Excelの日時は「1899-12-30からの延べ日数」として実数値で表される。
1を足せば翌日、0.5を足せば12時間後を意味する。
例えば 1899-12-30 6:00
は 0.25
となる。
タイムゾーン情報は持たない ので、日本で作った Excel ファイルをイギリスで開いても0.25
は 0.25
のままだし、日時形式で表示すれば「1899-12-30 6:00」である。
Google Sheet のタイムゾーン設定
Google Sheet には「ファイル>設定」にタイムゾーン設定がある。これを変更しても、シート内の日時値が勝手に変わることはないし、日時形式の表示にも影響はない。
ただし =NOW()
の結果に影響を及ぼす。
タイムゾーン設定が日本 (+9:00) の時とロンドン (+0) では 0.375 (=9/24) だけ結果がズレる。設定を変えて保存した直後にシート内の NOW()
が再計算される。
Excel には同等の設定が見当たらないので、 NOW()
を含むドキュメントを異なるタイムゾーンのユーザーがやりとりすると何やら面倒なことになるのかもしれない。
Javascript での変換
Javascript 標準の Date
型は内部値は epoc millsec である。 Date.now()
は現在時刻の内部値を返すが、これはシステムのタイムゾーン設定の影響を受けない。この点が根本的にExcel日時の概念と異なっている。
Excel日時の概念にマッチするのは Temporal.PlainDateTime
であるので、まずはこれを使って相互変換の関数を実装する。
const SheetDate = {
origin: Temporal.PlainDateTime.from({ year: 1899, month: 12, day: 30, }),
dayToMs: 24 * 60 * 60 * 1000,
};
export const ExcelDate = {
from(plain: Temporal.PlainDateTime): number {
return SheetDate.origin.until(plain).total({ unit: 'milliseconds' }) / SheetDate.dayToMs;
},
toPlain(serial: number): Temporal.PlainDateTime {
return SheetDate.origin.add({ milliseconds: Math.floor(serial * SheetDate.dayToMs) });
}
}
Deno.test({
name: 'ExcelDate',
fn: () => {
const now = Temporal.Now.zonedDateTimeISO("Asia/Tokyo").toPlainDateTime();
const serial = ExcelDate.from(now);
const now2 = ExcelDate.toPlain(serial);
console.log({ now, serial, now2 });
}
})
精度がどれくらい出るのかは詳しくわからないが、とりあえず 2025年付近の日時値であれば1/10秒までは一致しそう。
残念ながら2025年4月時点ではほとんどのモダンブラウザと node.js で Temporal が使用できないが、 @js-temporal/polyfill
を使うことで上のコードを実行できる。
Deno であれば unstable temporal
フラグを on にすれば polyfill なしで実行可能。
Date
を、 Timezone を意識して正しく使えているならば、以下のコードを使用できる。
const tz = Deno.env.get("TZ")
fromDate(date: Date, timeZone: Temporal.TimeZoneLike = tz): number {
const plain = date.toTemporalInstant().toZonedDateTimeISO(timeZone).toPlainDateTime();
return ExcelDate.from(plain);
},
toDate(serial: number, timeZone: Temporal.TimeZoneLike = tz): Date {
const plainDateTime = this.toPlain(serial);
const ms = plainDateTime.toZonedDateTime(timeZone).toInstant().epochMilliseconds;
return new Date(ms);
}
Discussion