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
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をつくりたいんだけど・・
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は西暦1年の前年は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