1. 概要

このクイックチュートリアルでは、Javaで2つの日付の差を計算する複数の可能性を探ります。

2. コアJava

2.1. java.util.Dateを使用して日数の違いを見つける

コアJavaAPIを使用して計算を行い、2つの日付の間の日数を決定することから始めましょう。

@Test
public void givenTwoDatesBeforeJava8_whenDifferentiating_thenWeGetSix()
  throws ParseException {
 
    SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH);
    Date firstDate = sdf.parse("06/24/2017");
    Date secondDate = sdf.parse("06/30/2017");

    long diffInMillies = Math.abs(secondDate.getTime() - firstDate.getTime());
    long diff = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS);

    assertEquals(6, diff);
}

 

2.2. java.time.temporal.ChronoUnitを使用して違いを見つける

Java8のTimeAPIは、日時の単位を表します。 TemporalUnitインターフェースを使用した秒または日。

各ユニットは、 between という名前のメソッドの実装を提供し、その特定のユニットに関して2つの時間オブジェクト間の時間を計算します。

たとえば、2つの LocalDateTime 間の秒数を計算するには、次のようにします。

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSeconds_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = ChronoUnit.SECONDS.between(now, tenSecondsLater);

    assertEquals(10, diff);
}

ChronoUnit は、 TemporalUnit インターフェースを実装することにより、具体的な時間単位のセットを提供します。 読みやすさを向上させるために、ChronoUnitenum値を静的にインポートすることを強くお勧めします

import static java.time.temporal.ChronoUnit.SECONDS;

// omitted
long diff = SECONDS.between(now, tenSecondsLater);

また、 ZonedDateTime であっても、互換性のある2つの時間オブジェクトをbetweenメソッドに渡すことができます。

ZonedDateTime の優れている点は、異なるタイムゾーンに設定されている場合でも計算が機能することです。

@Test
public void givenTwoZonedDateTimesInJava8_whenDifferentiating_thenWeGetSix() {
    LocalDateTime ldt = LocalDateTime.now();
    ZonedDateTime now = ldt.atZone(ZoneId.of("America/Montreal"));
    ZonedDateTime sixMinutesBehind = now
      .withZoneSameInstant(ZoneId.of("Asia/Singapore"))
      .minusMinutes(6);
    
    long diff = ChronoUnit.MINUTES.between(sixMinutesBehind, now);
    
    assertEquals(6, diff);
}

2.3. Temporal#until()を使用する

LocalDateやZonedDateTimeなどの Temporal オブジェクトは、指定された単位に関して別のTemporalまでの時間を計算するuntilメソッドを提供します。

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSecondsUsingUntil_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = now.until(tenSecondsLater, ChronoUnit.SECONDS);

    assertEquals(10, diff);
}

Temporal#untilTemporalUnit#between は、同じ機能のための2つの異なるAPIです。

2.4. java.time.Durationおよびjava.time.Periodを使用する

Java 8では、TimeAPIは2つの新しいクラスDurationとPeriodを導入しました。

時間ベース(時間、分、または秒)の時間で2つの日時の差を計算する場合は、Durationクラスを使用できます。

@Test
public void givenTwoDateTimesInJava8_whenDifferentiating_thenWeGetSix() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime sixMinutesBehind = now.minusMinutes(6);

    Duration duration = Duration.between(now, sixMinutesBehind);
    long diff = Math.abs(duration.toMinutes());

    assertEquals(6, diff);
}

ただし、 2つの日付の違いを表すためにPeriodクラスを使用しようとする場合は、落とし穴に注意する必要があります。

例では、この落とし穴を簡単に説明します。

Period クラスを使用して、2つの日付の間の日数を計算してみましょう。

@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenWorks()  {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixDaysBehind = aDate.minusDays(6);

    Period period = Period.between(aDate, sixDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(6, diff);
}

上記のテストを実行すると、合格します。 Periodクラスは問題を解決するのに便利だと思うかもしれません。 ここまでは順調ですね。

この方法が6日間の差で機能する場合、60日間も機能することは間違いありません。

それでは、上記のテストの660に変更して、何が起こるかを見てみましょう。

@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenDoesNotWork() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);

    Period period = Period.between(aDate, sixtyDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(60, diff);
}

ここで、テストを再度実行すると、次のように表示されます。

java.lang.AssertionError: 
Expected :60
Actual   :29

おっとっと! Periodクラスが違いを29日として報告したのはなぜですか?

これは、 Period クラスが、「x年、yか月、z日」の形式で日付ベースの時間を表すためです。  getDays()メソッドを呼び出すと、「z日」の部分のみが返されます。

したがって、上記のテストの period オブジェクトは、「0年、1か月、29日」という値を保持します。

@Test
public void givenTwoDatesInJava8_whenUsingPeriod_thenWeGet0Year1Month29Days() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);
    Period period = Period.between(aDate, sixtyDaysBehind);
    int years = Math.abs(period.getYears());
    int months = Math.abs(period.getMonths());
    int days = Math.abs(period.getDays());
    assertArrayEquals(new int[] { 0, 1, 29 }, new int[] { years, months, days });
}

Java8のTimeAPIを使用して日数の差を計算する場合は、 ChronoUnit.DAYS.between()メソッドが最も簡単な方法です。

3. 外部ライブラリ

3.1. JodaTime

JodaTimeを使用して比較的簡単な実装を行うこともできます。

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.9</version>
</dependency>

Joda-timeの最新バージョンはMavenCentralから入手できます。

LocalDate の場合:

@Test
public void givenTwoDatesInJodaTime_whenDifferentiating_thenWeGetSix() {
    org.joda.time.LocalDate now = org.joda.time.LocalDate.now();
    org.joda.time.LocalDate sixDaysBehind = now.minusDays(6);

    long diff = Math.abs(Days.daysBetween(now, sixDaysBehind).getDays());
    assertEquals(6, diff);
}

同様に、 LocalDateTime の場合:

@Test
public void givenTwoDateTimesInJodaTime_whenDifferentiating_thenWeGetSix() {
    org.joda.time.LocalDateTime now = org.joda.time.LocalDateTime.now();
    org.joda.time.LocalDateTime sixMinutesBehind = now.minusMinutes(6);

    long diff = Math.abs(Minutes.minutesBetween(now, sixMinutesBehind).getMinutes());
    assertEquals(6, diff);
}

3.2. Date4J

Date4j は、単純で単純な実装も提供します。この場合、TimeZoneを明示的に提供する必要があることに注意してください。

Mavenの依存関係から始めましょう:

<dependency>
    <groupId>com.darwinsys</groupId>
    <artifactId>hirondelle-date4j</artifactId>
    <version>1.5.1</version>
</dependency>

標準のDateTimeを使用した簡単なテストは次のとおりです。

@Test
public void givenTwoDatesInDate4j_whenDifferentiating_thenWeGetSix() {
    DateTime now = DateTime.now(TimeZone.getDefault());
    DateTime sixDaysBehind = now.minusDays(6);
 
    long diff = Math.abs(now.numDaysFrom(sixDaysBehind));

    assertEquals(6, diff);
}

4. 結論

この記事では、プレーンJavaと外部ライブラリの両方で、日付(時間ありとなし)の差を計算するいくつかの方法を説明しました。

記事の完全なソースコードは、GitHubから入手できます。