📆

Excel, Google Sheet の日時とタイムゾーン

に公開

Google Sheet は Excel との互換性を保つために、 Excel と同じ仕様で日時を扱っている。
以下、 Excel として記述するが Google Sheet も全く同じである。

Excel日時の仕様

Excelの日時は「1899-12-30からの延べ日数」として実数値で表される。
1を足せば翌日、0.5を足せば12時間後を意味する。
例えば 1899-12-30 6:000.25 となる。

タイムゾーン情報は持たない ので、日本で作った Excel ファイルをイギリスで開いても0.250.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 を使うことで上のコードを実行できる。
https://stackblitz.com/edit/excel-date

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