Java などでの日付処理について

2017年9月21日 Java 9 リリース🎉

---

ですが今日は Java8 で導入された Date and Time APIについて話します

---

# 歴史
Javaには java.util.Date が @Since 1.0 から存在していた。
Java 1.0 は1996年
DateはEpochからのミリ秒を保持。タイムゾーンに関する情報はもってない。
Mutableなのでよく事故が起こる
関数の引数にDateを渡したら、関数内部で書き換えられる可能性があるので
防御的コピー (Defensive copy)をした上で渡す必要があったりする。

---

## Date
```java
import java.text.SimpleDateFormat;
import java.util.Date;

public class Aaa {
   public static void main(String[] args) throws Exception {
       Date today = new Date();
       System.out.println("Today is: " + new SimpleDateFormat("yyyy-MM-dd").format(today));

       // 昨日の日付を表示したいな
       昨日の日付を表示(today);

       // 今日の日付を表示したいな
       System.out.println("Today is: " + new SimpleDateFormat("yyyy-MM-dd").format(today));
   }

   public static void 昨日の日付を表示(Date date) {
       // 昨日の日付を作る
       date.setTime(date.getTime() - 24L * 60 * 60 * 1000);
       System.out.println("Yesterday is: " + new SimpleDateFormat("yyyy-MM-dd").format(date));
   }
}
```

---

## 結果
```
Today is: 2017-12-19
Yesterday is: 2017-12-18
Today is: 2017-12-18
```
---

## Calendarの時代
Java 1.1 で java.util.Calendar が追加された。
Java 1.1 は1997年
実質的に Calendar == GregorianCalendar
JDK 1.1 における国際化対応の一環として導入された
CalendarはEpochからのミリ秒(Date)とタイムゾーンに関する情報を保持
がAPIが酷い
これまたjava.util.Dateと同じくMutableなのでよく事故が起こる

---

## 日本時間の2001年12月25日の14時ちょうどのCalendarをつくりたいんだけど・・
```java
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class Bbb {
   public static void main(String[] args) throws Exception {
       Calendar cal = Calendar.getInstance();
       cal.set(Calendar.YEAR, 2001);
       cal.set(Calendar.MONTH, 11); // 0 はじまり
       cal.set(Calendar.DAY_OF_MONTH, 25);
       cal.set(Calendar.HOUR_OF_DAY, 14); // Calendar.HOUR を使うとAM/PMがおかしくなる
       cal.set(Calendar.MINUTE, 0);
       cal.set(Calendar.SECOND, 0);
       cal.set(Calendar.MILLISECOND, 0);

       System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").format(cal.getTime()));
   }
}
```

---

# 長い冬の時代
Apache (Jakarta) commons-langのDateUtilsでがんばる日々・・
joda-time という選択肢もあった
2003年頃出てきた .Net Framework1.1 のDateTimeはしっかりImmutableになってた。

余談、Java 6.0 (2006年) で和暦を扱うJapaneseImperialCalendarが追加

---

# 新しい夜明け
Java 8.0 (2014年) で Date and Time API が追加された。
Joda-Time (初出は2002年ごろ？)をベースに JSR-310として策定された
Immutable
java.time パッケージで提供

---

# Date and Time API
中心となるクラス
## LocalDate
  年月日を保持するクラス。ISO8601がベース
  先発グレゴリオ暦で扱う
  java.util.Dateはユリウス暦とグレゴリオ暦の切り替わりがある。
## LocalTime
  時分秒（精度はナノ秒）を保持するクラス。タイムゾーンを持たない
## LocalDateTime
  LocalDate + LocalTime のクラス。
## ZoneId
  Asia/Tokyo などの地理的なタイムゾーン情報を保持
  java.util.TimeZoneがあったが、新たに作成された
  単なるOffset情報だけでなく、夏時間の情報も保持
  単なるOffsetを扱うためにZoneOffsetというサブクラスがある。

## Instant 
  意味：瞬時、瞬間、(特定の)時点
## ZonedDateTime
  LocalDate + LocalTime + ZoneId 

---

# 先発グレゴリオ暦とは・・・
「1582年10月から施行されたグレゴリオ暦の暦法を、1582年以前にも適用したもの」
https://ja.wikipedia.org/wiki/%E5%85%88%E7%99%BA%E3%82%B0%E3%83%AC%E3%82%B4%E3%83%AA%E3%82%AA%E6%9A%A6
ISO8601は先発グレゴリオ暦

## 1582年以前は処理系によって扱いは様々
ISO8601は西暦1年より前の扱いは規定していない
JavaのLocalDateは西暦１年の前年は0年で扱っている
  LocalDate.of(1,1,1).minusDays(1) -> 0000-12-31
Postgresは1年の前年は-1年

---

# データベースでの日付の扱い
DATE型といってもDBMSごとに扱いは様々

標準SQLでは、日付時間関連のデータ型は
* DATE型（日付）
* TIME型（時刻）
* TIMESTAMP型（日付と時刻を足したもの）
TIMEとTIMESTAMPはタイムゾーンは保持しない

---

## Oracleでは・・
日付時間関連のデータ型は四種類
* DATE 年月日と時分秒を保持。タイムゾーンなし
* TIMESTAMP 年月日と時分秒+少数以下(9桁まで指定可能)を保持。タイムゾーンなし
* TIMESTAMP WITH TIME ZONE 上記にタイムゾーン情報を追加したもの
* TIMESTAMP WITH LOCAL TIME ZONE タイムゾーンを持っているけれどもDBのタイムゾーン
http://otndnld.oracle.co.jp/document/products/oracle11g/111/doc_dvd/server.111/E05765-03/datatype.htm#18523

DATEは内部的にユリウス日+秒を保持している。7byte。範囲は-4712/01/01 ～ 9999/12/31
ユリウス暦とグレゴリオ暦を使っている。
西暦1年の前年は0年、0年の前年を「西暦前1年」と言っている。

---

## Postgresでは・・
* DATE型（日付）
* TIME型（時刻）
* TIMESTAMP型（日付と時刻を足したもの）
TIMEとTIMESTAMPはタイムゾーンの有り無しを指定できる

Oracleと同じくユリウス日 紀元前4713/01/01 ～ 294276/12/31
先発グレゴリオ暦を使用
西暦1年の前年は1 BC、1 BCの前年は2 BC 
0001-01-01の前日は '0001-12-31 BC'

---

## MySQLでは
* DATE型(日付)0年月日
* TIME型(時刻) '00:00:00' マイクロ秒まで。タイムゾーンない
* DATETIME型（日付と時刻を足したもの）  タイムゾーンない

DATEは3byteで格納。'1000-01-01' から '9999-12-31'
先発グレゴリオ暦を使用

---

# その他

---

## Git
Epochからの秒数とタイムゾーンを保持

---

## Apache Spark
日付時間関連は2種類
* TimestampType 年月日と時分秒を保持。タイムゾーンの情報はない
* DateType 年月日を保持
https://spark.apache.org/docs/latest/sql-programming-guide.html#data-types

---

## Parquet
日付時間関連は3種類
* DATE 1970-01-01からの経過日数をINT32で保持
* TIME_MILLIS 0:00からの経過ミリ秒をINT32で保持
* TIMESTAMP_MILLIS Epochからの経過ミリ秒をINT64で保持。タイムゾーンの情報はない
https://drill.apache.org/docs/parquet-format/#sql-types-to-parquet-logical-types

---

# まとめ
日時の処理をするときは処理系ごとの動きを把握しよう
型のタイムゾーンの有無を考慮しよう
夏時間があってもちゃんと動きますか？
できれば処理するマシンのタイムゾーンを変更しても問題ないような作りにする

---









実行時点での日本のローカル日時を取得する
LocalDate.now(ZoneId.of("Asia/Tokyo"))
ZoneId.getAvailableZoneIds.asScala.foreach(println)
JapaneseDate.of(1900,1,1).getEra



Oracleでは・・
日付時間関連のデータ型は四種類

内部的にユリウス日を保持している。-4712/01/01 ～ 9999/12/31
ユリウス暦とグレゴリオ暦を使っている。
TO_CHAR(DATE'2001-01-01','J')
2451911

TO_CHAR (DATE '0001-01-01', 'J')
1721424

TO_CHAR(DATE'1582-10-04','J')
2299160
TO_CHAR(DATE'1582-10-15','J')
2299161

Postgresでは・・
Oracleと同じくユリウス日 紀元前4713/01/01 ～ 294276/12/31
先発グレゴリオ暦を使用
postgres=# SELECT TO_CHAR (DATE '2001-01-01', 'J');
 to_char
---------
 2451911
(1 row)

postgres=# SELECT TO_CHAR (DATE '0001-01-01', 'J');
 to_char
---------
 1721426
(1 row)

postgres=# SELECT TO_CHAR (DATE '1582-10-04', 'J');
 to_char
---------
 2299150