ClosedXMLで時間値を取得したときの値とEXCELの仕様(特に24時間以上の場合)

ClosedXMLで「15:00」のようなセルから、データ値をDateTime型で取得した際の戻り値について。

この記事の内容はClosedXML Ver0.95を対象に書いている。しかしVer0.100で結果が変わったため、誤解の無いように結果のみ先に書いておく。

・Ver 0.100.0 より前
1899/12/30 HH:mm:ss

・Ver0.100.0 以降
1899/12/31 HH:mm:ss

本文

Excelの時間が入力されたセルから、ClosedXMLで値を取得したとき、TimeSpan型となる場合とDateTime型となる場合がある。これはこれで面倒ではある※が、今回の問題はDateTime型のときだ。

※ 例えばセルのフォーマットが"[h]:mm:ss"だとTimeSpan型、"[h]:mm"だとDateTime型になったりする

Excelでは時刻も日時も内部では同じシリアル値で管理されている。時刻を入力したセルのフォーマットを変更して日付を表示してみると「1900/1/0 ~」と表示される。日付としては不正だが、1日未満という意味で0日なんだろう。

C#のDateTime型では当然0日という日付は表現できない。ではどのような値になるかというと

1899/12/30 ~

そう1899/12/31ではないのだ。これはExcelの仕様バグに起因する。

1900年2月29日が存在するExcelのバグ

1900年は100の倍数であり400の倍数でないため、本来うるう年ではないのだが、Excelでは仕様バグとして2月29日が存在している。「仕様バグ」と書いたのは、正しくはないが意図的に組み込まれた動作だからだ。

詳しくは以下に公式のアナウンスがある
Excel では、1900 年が閏年であると誤って想定されています

Windowsが存在する前のソフトとの互換性が、2023年現在でも尾を引いてるというのは面白い。

ちなみにGoogleスプレッドシートでは日時をExcelと同様シリアル値で管理しているが、上記は切り捨てられていて1900年2月29日は存在しない。

ClosedXMLのGetValue

ClosedXMLのGetValueでは、DateTime型を生成する際にこの補正などを特にしていないため、1900年3月1日より前の日時に関してはシリアル値が1日分ずれることになる。

この1900年のうるう年問題と、時刻を1900年1月0日で扱うという仕様の組み合わせにより、時刻の値が1899年12月30日という微妙な日になる。

バージョンによる違い

冒頭に書いたが、ClosedXMLのVer0.100でコードが変更され、シリアル値60以下の場合は日付を一日ずらす処理が加えられた。これにより正しい日付にはなるものの、それ以前のバージョンと差異が発生する。なおVer0.100は他にもセル値の扱いが色々変わっている。

どういう場合に問題が起きるか

しかし時刻として使う場合は、日付部分は読み捨てるだろうから影響は少ないだろう。問題となるのは時間間隔として「25:00」などの表現をする場合だ。この場合、24時間以上を読み取ろうと思うと多少の処理が必要となる。

方法1)1899年12月30日との差分をとる

現状の実装に迎合して、1899/12/30 00:00との時間差を計算する。24:00以上の場合はセルの取得値は1899/12/31~となるので、24時間以上も計算できる。

この方法の難点はExcelとClosedXMLという二つの外部仕様に影響を受けるところだ。Excelはここまで来たら今更変更はしなさそうだが、オープンソースであるClosedXMLは流れ次第で突然修正してくるかもしれない。(と思ってるうちにされた)

方法2)シリアル値で計算する

シリアル値から日数を計算すれば、少なくともClosedXML側の日付変換仕様には左右されなくなる。シリアル値の1につき24時間加算すればいい。

しかしClosedXMLでは日時型のセルからシリアル値を取る方法は提供されていない。なのでセルのDataTypeをNumberに変更してからシリアル値を小数で取得することになる。この方法の難点はDataTypeの変更で副作用が発生するかもしれないことだ。

どちらにせよ、Excelシートから時刻値を読み込む場合は、修正できるように処理をまとめておいた方がいいだろう。

コメント