Apache POI Excel の日付で時差を取り扱う


## 概要

[Apache POI](https://poi.apache.org/)  は Microsoft Word とか Excel とかのファイルを Java で生成したりパースして必要な情報を取り出したり出来るライブラリ。ここでは特に Excel のファイルを使う際の日付、特に時差について考える。

## 内容

### 日付の取り扱いについて


Apache POI の Excel ではセルについて下記のような TYPE がある。POI には日付というタイプはない。これらは各セルに対して設定される。

* CELL_TYPE_NUMERIC
* CELL_TYPE_STRING
* CELL_TYPE_FORMULA
* CELL_TYPE_BLANK
* CELL_TYPE_BOOLEAN
* CELL_TYPE_ERROR

Excel としては内部的には「日付」は実数で保存されている。そのセルの中身が日付かどうかは CELL_TYPE_NUMERIC であり、 DateUtils.isCellDateFormatted() が true になるかで確認する。なお、時差情報は含まれていない。

詳しい仕様については ECMA-376[\[1\]](http://www.ecma-international.org/publications/standards/Ecma-376.htm) の 18.17.4 あたりで確認できる。（この中では d としてISO8601 形式で日付が扱えるように書かれているが Excel ではよくわからんので割愛）


### 内部的な日付の数値について

前述の通り 「日付」は実数で保存されている。ECMA-376 を見ると serial date-time と呼ぶらしい。この数字は unix 時間とかそういうものじゃなくて特定の日から何日経過したかという数値である。  整数部分が日付で小数部分が時間となる。

特定の日というのは Excel のバージョンなどにもよるが 1900年1月1日または1904年1月1日のいずれかである。[\[2\]](https://support.microsoft.com/ja-jp/kb/214330)
この開始日時の設定はワークブック単位で設定されているらしい。 ECMA-376 の 18.17.4.1 参照。（workbookPr の date1904 属性。 1 で 1904, 0 で 1900 。デフォルトは 0。）なお、時差情報は含まれていない。

下記は ECMA-376 に書かれている例。

```
The serial date-time 0.0000000... represents 00:00:00
The serial date-time 0.0000115... represents 00:00:01
The serial date-time 0.4207639... represents 10:05:54
The serial date-time 0.5000000... represents 12:00:00
The serial date-time 0.9999884... represents 23:59:59
```

過去を表す場合はマイナスらしいが Excel では見当たらない。

```
The serial date-time -2337.999989... represents 1893-08-05T00:00:01
The serial date-time 3687.4207639... represents 1910-02-03T10:05:54
The serial date-time 2.5000000... represents 1900-01-01T12:00:00
The serial date-time 2958465.9999884... represents 9999-12-31T23:59:59
```

### 時差を扱う場合について

再三言っているが Excel のデータ構造上で時差情報は含まれていない。国をまたいで Excel ファイルをやりとりする場合は何らかの方法で入力された時刻がどのタイムゾーンかを共有する必要がある。

getDateCellValue()[\[4\]](https://poi.apache.org/apidocs/org/apache/poi/ss/usermodel/Cell.html#getDateCellValue()) は一体何を取得してくるのか？ これは Java 側で設定されてるデフォルトのタイムゾーンである。もしかしたらサーバが海外にあれば Asia/Tokyo 以外なのかもしれない。 Apache POI 内部では DateUtil.toJavaDate() を使っていて、タイムゾーンも指定できるようだが getDateCellValue() を使う限りは指定する方法がないためそれはかなわない。


コード上ではどうするか。 2015-09-25 にリリースされた 3.13 で LocalUtil[\[3\]](https://poi.apache.org/apidocs/org/apache/poi/util/LocaleUtil.html) というクラスが追加されているのでこれを使うのが手っ取り早い。これは ThreadLocal を用いて処理時のタイムゾーンを設定することができる。マルチスレッドで処理する場合はスレッドごとに設定する必要があるので注意。

```java:ロサンゼルスのタイムゾーンで入力された日付を取得したい場合
LocaleUtil.setUserTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
Date date = cell.getDateCellValue();
```

それ以前のバーションの場合は TimeZone.setDefault() など全体の設定を変えてしまうなどがあるがコンテナサーバとかだと厳しい気はする。直接実数を取得して toJavaDate() を使うという手は基準日が 1900 年か 1904 年かを POI 利用側でみることが難しいのでわりとがんばる必要がある。 getDateCellValue() で取得した後 Date クラスの数値をいじるのはシステムのタイムゾーンが保証されてて、時差計算をきちんとできるならやってもいいが意味不明なバグを埋め込む可能性が高すぎるため個人的にはおすすめしない（穏やかな表現）。まあ素直に 3.14 に上げて setUserTimeZone() 使うのがよい。

## まとめ

* Excel ファイルでは日付はよくわからん数値で保存されてる
* Excel ファイルでは時差情報は含まれない
* Apache POI 3.14 の LocaleUtil.setUserTimeZone() を使え
* 自前で時差計算するな

# 参考文献

[1] http://www.ecma-international.org/publications/standards/Ecma-376.htm
[2] https://support.microsoft.com/ja-jp/kb/214330
[3] https://poi.apache.org/apidocs/org/apache/poi/ss/usermodel/Cell.html#getDateCellValue()
[4] https://poi.apache.org/apidocs/org/apache/poi/util/LocaleUtil.html