Ch.7 日付・時刻API

日付時刻クラス

import java.time.*;

public class DataTimeSample {
    public static void main(String[] args){
        System.out.println("LocalDate :" + LocalDate.now()); 
        // 'LocalDate :2021-03-28'
        System.out.println("LocalTime :" + LocalTime.now()); 
        // 'LocalTime :09:07:54.907153500'
        System.out.println("LocalDateTime :" + LocalDateTime.now());   
        // 'LocalDateTime :2021-03-28T09:07:54.908154500'
        System.out.println("OffsetTime : " + OffsetTime.now());        
        // 'OffsetTime : 09:07:54.909152500+09:00'
        System.out.println("OffsetDateTime : " + OffsetDateTime.now());
        // 'OffsetDateTime : 2021-03-28T09:07:54.910151400+09:00'
        System.out.println("ZoneOffset.systemDefault() : " + ZoneOffset.systemDefault()); 
        //'ZoneOffset.systemDefault() : Asia/Tokyo'
        System.out.println("ZonedId :" + ZoneId.systemDefault()); 
        // 'ZonedId :Asia/Tokyo'
    }
}

LocalDateTime

LocalDateTimeオブジェクトの生成で指定する月には【Month】クラスの列挙型が利用得できる。 また、LocalDateクラスとLocalTimeクラスのオブジェクトを組み合わせて【LocalDateTime】クラスのインスタンスを作成できる。

    public static void createInstance(){
        //System.out.println(LocalDate.of(2018, Month.FEBRUARY, 29));
        // 'Invalid date 'February 29' as '2018' is not a leap year'
        System.out.println(LocalDate.of(2018, Month.FEBRUARY, 28));
        //'2018-02-28'
        System.out.println(LocalDateTime.of(2019, Month.MARCH, 12, 23, 59));
        //'2019-03-12T23:59'
        System.out.println(LocalDateTime.of(2019, Month.MARCH, 12, 23, 59).getMonth());
        //'MARCH'
        System.out.println(LocalDateTime.of(2019, Month.MARCH, 12, 23, 59).getMonthValue());
        //'3'
        System.out.println(LocalDateTime.of(LocalDate.of(2020,11,12), LocalTime.of(20,23)));
        //'2020-11-12T20:23'
    }

getMonthメソッドでは列挙型で月を取得できる。

フォーマッタ

日付・時刻のフォーマッタは日付・時刻オブジェクトに渡して文字列化することも、フォーマッタオブジェクトに日付・時刻オブジェクトを渡して文字列化することも出来る。

    public static void  formatter(){
        LocalDateTime myTime = LocalDateTime.of(2019, Month.MARCH, 12, 23, 59);
        System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(myTime)); //'2019-03-12T23:59:00'
        System.out.println(myTime.format(DateTimeFormatter.ISO_DATE_TIME)); //'2019-03-12T23:59:00'
    }

後者の方がオブジェクト指向的で良いかな、と思ったりする。

フォーマッタは手作り出来る。 第二引数にLocale定数を渡すと様々な言語に翻訳して表示できる。

    public static void myFormatter(){
        LocalDateTime thisTime = LocalDateTime.now();

        System.out.println(thisTime.format(DateTimeFormatter.ofPattern("MMMMdd")));
        //'3月28'
        System.out.println(thisTime.format(DateTimeFormatter.ofPattern("MMMMdd", Locale.KOREA)));
        //'3월28'
        System.out.println(thisTime.format(DateTimeFormatter.ofPattern("今年始まってから: D日")));
        //'今年始まってから: 87日'
        System.out.println(thisTime.format(DateTimeFormatter.ofPattern("今月 W週目の E曜日")));
        //'今月 5回目の 日曜日'
        System.out.println(LocalDate.parse("2020ねん04がつ20にち10じ05ふん", DateTimeFormatter.ofPattern("yyyyねんMMがつddにちHHじmmふん")));
        //'2020-04-20'
    }

フォーマッタに利用できるパターンは下記に記載されている。

DateTimeFormatter (Java Platform SE 8)

日付・時刻の計算

日付と時刻のオブジェクトはイミュータブルオブジェクトであるため、計算してもそのオブジェクトの値が変わることは無い。

    public static void calculate(){
        LocalDate thisDate = LocalDate.now();
        System.out.println(thisDate.plusMonths(1).plusDays(3));//'2021-05-01'
        System.out.println(thisDate);//'2021-03-28'

        LocalDateTime thisDateTime = LocalDateTime.now();
        System.out.println(thisDateTime.minusHours(45).plusSeconds(200));//'2021-03-26T14:41:12.627712400'
        System.out.println(thisDateTime);//'2021-03-28T11:37:52.627712400'
    }

ZonedDateTimeとOffsetDateTime

ZonedDateTimeはDateTimeオブジェクトにタイムゾーン情報を付与できる。 タイムゾーン情報を付与するだけで、渡したDateTimeオブジェクトの日付・時刻情報自体を変更することは無い。

    public static void zoned(){
        LocalDateTime sampleTime = LocalDateTime.of(2020,1,1,00,01);
        ZonedDateTime melbourneDateTime = ZonedDateTime.of(sampleTime, ZoneId.of("Australia/Melbourne"));
        ZonedDateTime japanDateTime     = ZonedDateTime.of(sampleTime, ZoneId.systemDefault());
        ZonedDateTime perthDateTime     = ZonedDateTime.of(sampleTime, ZoneId.of("Australia/Perth"));

        System.out.println(melbourneDateTime); 
        //'2020-01-01T00:01+11:00[Australia/Melbourne]'
        System.out.println(japanDateTime);     
        //'2020-01-01T00:01+09:00[Asia/Tokyo]'
        System.out.println(perthDateTime);     
        //'2020-01-01T00:01+08:00[Australia/Perth]'

        DateTimeFormatter austraiaFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL);
        System.out.println(austraiaFormat.format(melbourneDateTime));
        //'2020年1月1日水曜日 0時01分00秒 オーストラリア東部夏時間'
        System.out.println(austraiaFormat.format(perthDateTime));    
        //'2020年1月1日水曜日 0時01分00秒 オーストラリア西部標準時'

        DateTimeFormatter shortFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
        System.out.println(shortFormat.format(sampleTime));          
        //'2020/01/01 0:01'
        System.out.println(austraiaFormat.format(sampleTime));       
        //'Exception in thread "main" java.time.DateTimeException Unable to extract ZoneId from temporal 2020-01-01T00:01'
    }

DateTimeFormatter.ofLocalizedDateTimeを利用してフォーマットにFormatStyle.FULLFormatStyle.LONGを指定することで、ゾーン情報を付与した文字列に変換してくれる。

FormatStyle.shortFormatStyle.MEDIUMではゾーン情報が付与されないので渡す時刻情報にゾーン情報が存在しなくても例外にはならない。

    public static void offset(){
        LocalDateTime sampleTime = LocalDateTime.of(2020,1,1,00,01);
        OffsetDateTime originalTime  = OffsetDateTime.of(sampleTime, ZoneOffset.of("+02:00"));
        System.out.println(originalTime);

        DateTimeFormatter fullFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL);
        System.out.println(fullFormat.format(originalTime));//'2020-01-01T00:01+02:00'
        //'Exception in thread "main" java.time.DateTimeException: Unable to extract ZoneId from temporal 2020-01-01T00:01+02:00'
    }

夏時間

夏時間によってゾーン情報の時差が変更になっている。

    public static void summerTime(){
        LocalDate startDate = LocalDate.of(2018, 10, 7);
        LocalTime startTime = LocalTime.of(1,59);
        ZonedDateTime melbourneTime = ZonedDateTime.of(startDate, startTime, ZoneId.of("Australia/Melbourne"));
        System.out.println(melbourneTime + " 1.0h 後 ->" + melbourneTime.plusHours(1));
        //'2018-10-07T01:59+10:00[Australia/Melbourne] 1.0h 後 ->2018-10-07T03:59+11:00[Australia/Melbourne]'

        LocalDate endDate = LocalDate.of(2019,4,7);
        LocalTime endTime = LocalTime.of(2,59);
        ZonedDateTime melbourneTimeEnd = ZonedDateTime.of(endDate, endTime, ZoneId.of("Australia/Melbourne"));
        System.out.println(melbourneTimeEnd + " 1.0h 後 ->" + melbourneTimeEnd.plusHours(1));
        //'2019-04-07T02:59+11:00[Australia/Melbourne] 1.0h 後 ->2019-04-07T02:59+10:00[Australia/Melbourne]'
    }

基本的な事項であるが、夏時間開始時は「睡眠時間が短くなり(時間のスキップが発生し)」、夏時間終了時は「睡眠時間が長くなる(時間の重複が発生する)」。

Periodクラス

時間の間隔を年・月・日の単位で保持する。 オブジェクトは年・月・日の間隔をそれぞれ別に取り出すことが出来る。

    public static void period(){
        LocalDate startDate = LocalDate.of(2018, 10, 7);
        LocalTime startTime = LocalTime.of(1,59);

        LocalDate endDate = LocalDate.of(2020, 5, 5);
        LocalTime endTime = LocalTime.of(1,3);
        Period  myPeriod = Period.between(startDate, endDate);

        System.out.println(myPeriod);//'P1Y6M28D'
        System.out.println(myPeriod.getYears()); //'1'
        System.out.println(myPeriod.getMonths());//'6'
        System.out.println(myPeriod.getDays());  //'28'
        //Period  myPeriod = Period.between(LocalDateTime.of(startDate, startTime), LocalDateTime.of(endDate, endTime));

        ZonedDateTime melbourneTime = ZonedDateTime.of(startDate, startTime, ZoneId.of("Australia/Melbourne"));
        ZonedDateTime japanDateTime = ZonedDateTime.of(startDate,startTime, ZoneId.systemDefault());
        //Period period = Period.between(melbourneTime, japanDateTime);
    }

Periodは日・月・年単位の期間しか持てないため、LocalDateTimeZonedDateTime同士の期間は計算できない。

Period同士の計算が可能。計算に時間や秒単位の処理が発生しないため、PeriodとLocalDateTmeの計算も可能。

    public static void periodCalc(){
        LocalDate start = LocalDate.now();
        LocalDate end   = LocalDate.of(1999,8,12);
        Period thisPeriod = Period.between(end, start);

        System.out.println(thisPeriod);
        //'P21Y7M16D'
        System.out.println(thisPeriod.plus(Period.ofYears(10)));
        //'P31Y7M16D'
        System.out.println(thisPeriod.plus(Period.ofYears(10).plus(Period.ofMonths(3))));
        //'P31Y10M16D'
        System.out.println(thisPeriod.plus(Period.ofYears(10).plus(Period.ofMonths(3))).plus(Period.ofDays(10)));
        //'P31Y10M26D'

        System.out.println(Period.between(start, end));
        //'P-21Y-7M-16D'
        System.out.println(end.plus(thisPeriod));
        //'2021-03-28'
        System.out.println(LocalTime.now().plus(thisPeriod));
        //'Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Months'
    }

最後の例では時間の情報しか持たないLocalTimeオブジェクトに対して、【年・月・日】の足し算をしようとしているため、実行時例外となっている。 下記の場合でも同様である。

        System.out.println(LocalTime.now().plus(Duration.ofHours(24)));
        //'20:48:26.033646'
        System.out.println(LocalTime.now().plus(Period.ofDays(1)));
        //'Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Days'

Durationクラス

Periodクラスが【年・月・日】の概念を持つのに対して、Durationでは【時間】の概念を扱う。Periodオブジェクトの標準出力が【P】で表されたのに対してDurationは【PT】となる。

ナノ秒の細かさまで保持することが出来、ofSecondsメソッドでは第二引数でナノ秒を直接して出来る。

        System.out.println(Duration.ofSeconds(9, 300000));  //'PT9.0003S'

        System.out.println(Duration.of(10, ChronoUnit.DAYS));  //'PT240H'
        System.out.println(Duration.of(10, ChronoUnit.HOURS));  //'PT10H'
        System.out.println(Duration.of(10, ChronoUnit.SECONDS));  //'PT10S'

Durationクラスも当然LocalDateTimeLocalTimeオブジェクトに対する加算・減算に利用できる。但し、時刻情報を持たないLocalDateオブジェクトに対しての加算・減算は実行時例外となる。

        LocalDateTime nowTime = LocalDateTime.now();
        System.out.println(nowTime.plus(Duration.of(10, ChronoUnit.DAYS)));
        //'2021-04-07T21:10:37.799255900'

        LocalDate     noeDate = LocalDate.now();
        System.out.println(noeDate.plus(Duration.of(10, ChronoUnit.DAYS)));
        //'Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds'

Instantクラス

LocalDateTimeは「ある一時点の時刻」を持てるが「どの場所での時刻か」は考慮されていない。同じ1999-11-20 14:20でもメルボルンの11-20 14:20と東京の11-20 14:20では2時間の差が生まれる。

ZonedLocalDateTimeも「ある時間ゾーンの一時点の時刻」を持てるが、下記の例のように「重複して発生する時間」が存在するため、一回目の2:59と二回目の2:59を見分けることは出来ない。 

        System.out.println(melbourneTimeEnd + " 1.0h 後 ->" + melbourneTimeEnd.plusHours(1));
        //'2019-04-07T02:59+11:00[Australia/Melbourne] 1.0h 後 ->2019-04-07T02:59+10:00[Australia/Melbourne]'

そこで【UTCの1970年1月1日0:00】から何秒経過した時間か、という情報の持ち方をすることで【単一時点】を表現できるようにしたのがInstantクラスである。

    public static void epoch(){
        System.out.println(Instant.now());//'2021-03-28T12:49:32.458080400Z'
        Arrays.asList(1,2,3,4,5).stream().forEach(System.out::println);
        System.out.println(Instant.now());//'2021-03-28T12:49:32.460081200Z'
        System.out.println(Instant.ofEpochSecond(1000000000));//'2001-09-09T01:46:40Z'
    }

エポック秒(とナノ秒)の2つの情報を持つオブジェクトだが、(そのまま表記されるとわかりにくいため)標準出力で表示されるのは「UTCのLocalDateTime」になっている。

ZonedDateTimeをInstantクラスのオブジェクトに変換することも出来る。

        LocalDate endDate = LocalDate.of(2019,4,7);
        LocalTime endTime = LocalTime.of(2,59);
        ZonedDateTime melbourneTimeEnd = ZonedDateTime.of(endDate, endTime, ZoneId.of("Australia/Melbourne"));
        System.out.println(melbourneTimeEnd + " 1.0h 後 ->" + melbourneTimeEnd.plusHours(1));

        System.out.println("Before the end of Daylight Saving Time: " + melbourneTimeEnd.toEpochSecond());//'1554566340'
        System.out.println("After the end of Daylight Saving Time: " + melbourneTimeEnd.plusHours(1).toEpochSecond());//'1554569940'
        System.out.println("Before the end of Daylight Saving Time: " + melbourneTimeEnd.toInstant());//'2019-04-06T15:59:00Z'
        System.out.println("After the end of Daylight Saving Time: " + melbourneTimeEnd.plusHours(1).toInstant());//'2019-04-06T16:59:00Z'

ZonedLocalDateTimeオブジェクトでは、夏時間終了時重複した時間を表現できなかったがInstantオブジェクトに変換することで時間の重複を含めた【一時点の表現】に成功している。

LocalDateTimeオブジェクトはゾーン情報や時差情報を持たないため、当然直接Instantクラスのオブジェクトに変換することは出来ない。toInstantメソッドを呼ぶときにゾーン情報(地域または時差)を指定して変換する必要がある。

        LocalDateTime myTime = LocalDateTime.of(LocalDate.of(2020,12,20), LocalTime.of(10,20));
        Instant myinstant = myTime.toInstant(ZoneOffset.of("+02:00"));
        System.out.println(myinstant.getEpochSecond());//'1608452400'
        System.out.println(myinstant);//'2020-12-20T08:20:00Z'

        ZonedDateTime myDarwinTime = ZonedDateTime.of(myTime,ZoneId.of("Australia/Darwin"));
        System.out.println(myDarwinTime.toInstant().getEpochSecond());//'1608425400'
        System.out.println(myDarwinTime.toInstant());//'2020-12-20T00:50:00Z'

instantオブジェクトに対する足し算にはDurationと同じChronoUnitが利用できる。 ただしYEARSはサポートされていない。