Java 8のPeriodとDurationメモ

2つの日時の間隔を求めたい場合に思いつく、なんとなく似ている響きの `java.time.Period` と `java.time.Duration` を比較してみました。

# `java.time.Period`

こちらは「年月日」単位での間隔を表現できます。
例えば、2024-08-01 〜 2024-08-10の間であれば

```kotlin
import java.time.LocalDate
import java.time.Period

fun main() {
    val start = LocalDate.of(2024, 8, 1)
    val end = LocalDate.of(2024, 8, 10)
    
    Period.between(start, end)
    // P9D (9 days)
}
```

終了日 `end` は含まれないので、1,2,3 ... 9 と数えていき、答えは9日間となります。

## Periodの生成方法

### `between`

2つの `LocalDate` の差分から生成します。

SQLのように減算は使えないので、このメソッドを使います。

`start > end` の場合、マイナス値になります。
```kotlin
Period.between(LocalDate.of(2024, 1, 1), LocalDate.of(2023, 11, 16))
// P-1M-15D (-1 month -15 days)
```

`start == end` の場合、0です。
```kotlin
Period.between(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 0, 0))
// P0D (0 days)
```

終了日を含めて「◯日間」と表現したい場合は、endに1日加算します。
もしくは、Period自体に加算します。

```kotlin
import java.time.LocalDate
import java.time.Period

fun main() {
    val start = LocalDate.of(2024, 8, 1)
    val end = LocalDate.of(2024, 8, 3)
    
    Period.between(start, end.plusDays(1))
    // P3D (3 days)

    Period.between(start, end).plusDays(1)
    // P3D (3 days)
}
```

#### `between` を引き算で表現したい

演算子オーバーロードを使います。

```kotlin
operator fun LocalDate.minus(other: LocalDate): Period = Period.between(other, this)

val start = LocalDate.of(2024, 8, 1)
val end = LocalDate.of(2024, 8, 3)

end - start
// P2D (2 days)

start - end
// P-2D (-2 days)
```

必ず正の値を出したい場合

```kotlin
operator fun LocalDate.minus(other: LocalDate): Period = if (this > other) {
    Period.between(other, this)
} else {
    Period.between(this, other)
}

val start = LocalDate.of(2024, 8, 1)
val end = LocalDate.of(2024, 8, 3)

end - start
// P2D (2 days)

start - end
// P2D (2 days)
```

### `from`

他の `java.time.TemporalAmount` から生成します。
例えば `java.time.Duration` や、他ライブラリが実装するパターン（[例](https://javadoc.io/doc/io.r2dbc/r2dbc-postgresql/0.8.8.RELEASE/io/r2dbc/postgresql/codec/Interval.html)）もあります。

### `of(years: Int, months: Int, days: Int)`

差分の「年月日」から生成します。
月が12を超えたり、日が31を超えても表現できます。

```kotlin
Period.of(1, 6, 0)
// P1Y6M (1 year 6 months)

Period.of(0, 18, 90)
// P18M90D (18 months 90 days)
```

### `ofYears`, `ofMonths`, `ofWeeks`, `ofDays`

`of` の省力化バージョン。年のみ、月のみ、週のみ、日のみを指定できます。
月が12を超えたり、日が31を超えても表現できます。

```kotlin
Period.ofMonths(18)
// P18M (18 months)

Period.ofDays(90)
// P90D (90 days)
```

### `parse`

ISO 8601形式の文字列を解釈できます。

## `LocalDate` と `Period` で計算

### 指定期間の加減算

例えば 2024-01-01 の1年6️ヶ月後/前を計算したい場合

```kotlin
val date = LocalDate.of(2024, 1, 1)
val period = Period.of(1, 6, 0)

// -- 加算

date.plus(period)
date + period
// 2025-07-01

// 以下と同等
date.plusYears(1).plusMonths(6)

// -- 減算

date.miuns(period)
date - period
// 2022-07-01

// 以下と同等
date.minusYears(1).minusMonths(6)
```

# `java.time.Duration`

こちらはPeriodより細かい、日〜ナノ秒単位での間隔を表現できます。

```kotlin
import java.time.Duration
import java.time.LocalTime

fun main() {
    val start = LocalTime.of(0, 0)
    val end = LocalTime.of(12, 34, 56, 789000000)
    
    Duration.between(start, end)
    // PT12H34M56.789S
}
```

秒単位での情報を持っていない `LocalDate` などからは算出できません。
(Temporalインターフェイスが引数なので、コンパイルは通ってしまいます）

```kotlin
Duration.between(LocalDate.of(2024, 1, 1), LocalDate.now())
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
```

また、 `start` と `end` で粒度が違う場合もエラーになります。

```kotlin
val start: LocalDateTime = LocalDate.now().atStartOfDay()
val end: LocalTime = LocalTime.of(12, 34, 56, 789000000)

Duration.between(start, end)
// java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 12:34:56.789 of type java.time.LocalTime
```

## Durationの生成方法

Periodとほぼ同じですが、引数が異なるものもあります。

### `of(amount: Long, unit: TemporalUnit)` 

```kotlin
Duration.of(2, ChronoUnit.HOURS)
// PT2H
```

### `ofSeconds` の2つのオーバーロード

`nanoAdjustment` を指定すると、ナノ秒も指定できます。

```kotlin
Duration.ofSeconds(10)
// PT10S

Duration.ofSeconds(10, 200000000)
// PT10.2S
```

### 12:34:56.789 を表現したい

何か他にいい方法があるかもしれませんが…

```kotlin
Duration.ofHours(12).plusMinutes(34).plusSeconds(56).plusMillis(789)
// PT12H34M56.789S
```

## 時刻を持つ型と `Duration` の加減算

`LocalDateTime` を例にします。

```kotlin
val now = LocalDateTime.now()
val duration = Duration.ofDays(3).plusHours(12)

now + duration
// 現在時刻から3日半後
```

## 時刻を持たない型と `Duration` の加減算

`LocalDate` を例にします。

```kotlin
val duration = Duration.ofDays(3)

val date = LocalDate.of(2024, 1, 1)
date + duration
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
```

# PeriodとDurationをあわせたい

```kotlin
val duration = Duration.ofDays(3).plusHours(12)
val period = Period.ofYears(1)

duration + period
// コンパイルエラー

period + duration
// コンパイルは通るが、java.time.DateTimeException: Unit must be Years, Months or Days, but was Seconds
```

PeriodとDurationを一つのTemporal型変数に格納するのは難しそうなので、個別に利用するほうが良さそうです。

```kotlin
val period = Period.ofYears(1).plusMonths(6)
val duration = Duration.ofDays(3).plusHours(12)

val date = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
date + period + duration
// 2025-07-04T12:00 (1年6ヶ月と、3日12時間を加算）
```