【Kotlin】for 文で LocalDate を Int のように扱う

## TL;DR

* 実用的には `datesUntil（）` を使うのがてっとり早い
* Kotlin の for 文は `iterator()` メソッドで取得したイテレーターでループしている
* 次のようにして `Int` 型と同じようにループできる
    * `iterator()` メソッドを定義した Range クラスを作成
    * `rangeTo()` メソッドをオーバーライドしてその Range クラスを返す

## はじめに

Kotlin で `Int` 型のループは次のように書くことが可能です。

```kotlin:Int 型のループ
val start = 1
val end = 10

for (x in start..end) {
  println(x)
}
```

`LocalDate` 型のループも同じように書けると思いきや、これはコンパイルエラーになります。

```kotlin:LocalDate 型のループ
val startDate = LocalDate.of(2022, 1, 1)
val endDate = LocalDate.of(2022, 1, 31)

for (date in startDate..endDate) {
  println(date)
}
```

```:エラーメッセージ
For-loop range must have an 'iterator()' method 
```

### 本記事の目的

* エラーを読み解き、Kotlin の for 文を理解する
* for 文で `Int` 型と同じように `LocalDate` 型を扱えるようにする

### datesUntil を使う

https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/LocalDate.html#datesUntil(java.time.LocalDate)

`LocalDate` には `datesUntil` というメソッドが存在し、次のような書き方で日付のイテレーションは可能です。ループするだけならこれで済みますが、本記事では for 文をきちんと理解することを目的として実装を行います。

```kotlin
startDate.datesUntil(endDate.plusDays(1)).forEach { println(it) }
```

## for 文の基本

https://kotlinlang.org/docs/control-flow.html#for-loops

公式ドキュメントによると、ループ対象のオブジェクトに必要な条件は次の３つです。

* メンバー関数または拡張関数として `iterator()` を持っていて、`Iterator<>` を返すこと
* その `Iterator<>` は、メンバー関数または拡張関数として `next()` を持っていること
* その `Iterator<>` は、メンバー関数または拡張関数として `hasNext()` を持っていること

つまり、Kotlin の for 文はイテレーターでループしているだけで、以下の for 文のループ と while文のループ は実質的に同じ処理になります。

```kotlin:for文のループ
val list: List<Int> = listOf(1, 2, 3)

for (i in list) {
  println(i)
}
```

```kotlin:while文のループ
val list: List<Int> = listOf(1, 2, 3)
val iterator: Iterator<Int> = list.iterator()

while (iterator.hasNext()) {
  val i = iterator.next()
  println(i)
}
```


つまり `For-loop range must have an 'iterator()' method` というエラーメッセージが言っているのは、`startDate..endDate` で作成したオブジェクトが `iterator()` メソッドを持ってないよ、ということです。

逆に言えば `1..10` のように作成したオブジェクトは `iterator()` メソッドを持っているわけですが、これは何者なのでしょうか？

## Int 型のループを理解する

`Int` 型のループがどういう仕組みで動作しているのか見ていきましょう。

```kotlin:題材にする for ループ
for (x in 1..10) {
  println(x)
}
```

### IntRange

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/range-to.html

`1..10` の `..` は範囲を表現するための演算子で、`rangeTo` メソッドで実装されています。
そのため、明示的にメソッドを呼んでも同じように動作します。

```kotlin
val range: IntRange = 1.rangeTo(10)
for (x in range) {
  println(x)
}
```

`rangeTo` メソッドの戻り値は `IntRange` です。`Int` の「範囲」を表すクラスで、端点の値（`start` と `endInclusive`）をプロパティとして持っています。

このクラスの `iterator()` メソッドでは `start` から `endInclusive` まで数値が 1 ずつ増えるイテレーターが返ります（実際には親クラスの `IntProgression` で定義されています。長くなるので説明は割愛します）。

## LocalDate 型でループする

### ClosedRange

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/range-to.html

`LocalDate` は Java のクラスであり、rangeTo メソッドを持ちませんが、Kotlin 標準ライブラリに `Comparable<T>.rangeTo` メソッドが定義されているため Range を作成可能です。ただしその戻り値は `ClosedRange<T>` 型です。

```kotlin
val start = LocalDate.of(2022, 1, 1)
val end = LocalDate.of(2022, 12, 31)

val range: ClosedRange<LocalDate> = start..end
```

これは「範囲」を表すインターフェースで、端点の値（`start` と `endInclusive`）を持っているだけです。`IntRange` にはあった `iterator()` メソッドが無いのでループできません。

### LocalDateRange を作る

`ClosedRange` に `iterator()` メソッドが存在しないからループできないので、`iterator()` メソッドを持ったクラスを返すように変更してみましょう。

まずは `ClosedRange` を継承した `LocalDateRange` クラスを作成します。

```kotlin
class LocalDateRange(
    override val start: LocalDate,
    override val endInclusive: LocalDate
) : ClosedRange<LocalDate>
```

`LocalDate` の拡張関数として `rangeTo` メソッドを定義して `LocalDateRange` を返すようにします。

```kotlin
operator fun LocalDate.rangeTo(that: LocalDate) = LocalDateRange(this, that)
```

これで `..` 演算子を使ったときに `LocalDateRange` が返されるようになりました。

```kotlin
val start = LocalDate.of(2022, 1, 1)
val end = LocalDate.of(2022, 12, 31)
val range: LocalDateRange = start..end
```

この時点では `ClosedRange` と変わらないので、まだループはできません。

### LocalDateIterator を作る

`LocalDateRange` クラスに `Iterable<LocalDate>` インターフェースをつけて、`iterator()` メソッドを実装します。

```kotlin
class LocalDateRange(
    override val start: LocalDate,
    override val endInclusive: LocalDate
) : ClosedRange<LocalDate>, Iterable<LocalDate> {
    override fun iterator(): Iterator<LocalDate> = LocalDateIterator(start, endInclusive)
}
```

そこで返すイテレーターは次のように定義しています。このクラスはふつうのイテレーターです。内部状態として次に返す値（`next`）を持っていて、要素を取り出すたびに１日日付を進めます。終了日（`endInclusive`）を超えたら `hasNext()` が `false` を返すようになります。

```kotlin
class LocalDateIterator(
    val start: LocalDate,
    val endInclusive: LocalDate
) : Iterator<LocalDate> {
    var next = start

    override fun hasNext(): Boolean {
        return next <= endInclusive
    }

    override fun next(): LocalDate {
        val returnValue = next
        next = next.plusDays(1)
        return returnValue
    }
}
```

これで次のように startDate から endDate まで1日ずつループすることが可能になります。

```kotlin
val startDate = LocalDate.of(2022, 1, 1)
val endDate = LocalDate.of(2022, 1, 31)

for (date in startDate..endDate) {
  println(date)
}
```

### 補足

ここまでで for 文でループ可能になりましたが、2点補足です。

#### 1. Itarable は必須ではない

for 文でループするには `iterator()` メソッドが必須ですが、実は `Iterable` インターフェースは必須ではありません。インターフェースを使わない場合は `iterator()` メソッドに `operator` を付けてやれば for 文でループ可能です。

```kotlin:Iterable を使わない場合
class LocalDateRange(
    override val start: LocalDate,
    override val endInclusive: LocalDate
) : ClosedRange<LocalDate> {
    operator fun iterator(): Iterator<LocalDate> = LocalDateIterator(start, endInclusive)
}
```

#### 2. Iterator クラスも必須ではない

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/iterator.html

実はイテレーターはクラスとして定義する必要もなく、`iterator` 関数を使うと簡単に定義できます。詳細はリファレンスを参照していただきたいのですが、簡単に言えば `next()` を呼ぶたびに `yield()` に渡した値を返すイテレーターが作成されます。

```kotlin
class LocalDateRange(
    override val start: LocalDate,
    override val endInclusive: LocalDate
) : ClosedRange<LocalDate> {
    operator fun iterator(): Iterator<LocalDate> = iterator {
        var date = start
        while (date <= endInclusive) {
            yield(date) // ここで処理は中断。next() が呼ばれると値を返して次の行へ処理が進みます。
            date = date.plusDays(1)
        }
    }
}
```

## 最終的なコード

```kotlin
// LocalDateRange の定義。
class LocalDateRange(
    override val start: LocalDate,
    override val endInclusive: LocalDate
) : ClosedRange<LocalDate> {
    
    // iterator 関数で Iterator<LocalDate> を作成
    operator fun iterator(): Iterator<LocalDate> = iterator {
        var date = start
        while (date <= endInclusive) {
            yield(date)
            date = date.plusDays(1)
        }
    }
}

// .. 演算子で LocalDateRange を返すようにする
operator fun LocalDate.rangeTo(that: LocalDate) = LocalDateRange(this, that)

fun main() {
    val startDate = LocalDate.of(2023, 1, 1)
    val endDate = LocalDate.of(2023, 1, 31)

    // 2023-01-01 から 2023-01-31 まで出力
    for (date in startDate..endDate) {
        println(date)
    }
}
```