TL;DR
- 実用的には
datesUntil()を使うのがてっとり早い - Kotlin の for 文は
iterator()メソッドで取得したイテレーターでループしている - 次のようにして
Int型と同じようにループできる-
iterator()メソッドを定義した Range クラスを作成 -
rangeTo()メソッドをオーバーライドしてその Range クラスを返す
-
はじめに
Kotlin で Int 型のループは次のように書くことが可能です。
val start = 1
val end = 10
for (x in start..end) {
println(x)
}
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 を使う
LocalDate には datesUntil というメソッドが存在し、次のような書き方で日付のイテレーションは可能です。ループするだけならこれで済みますが、本記事では for 文をきちんと理解することを目的として実装を行います。
startDate.datesUntil(endDate.plusDays(1)).forEach { println(it) }
for 文の基本
公式ドキュメントによると、ループ対象のオブジェクトに必要な条件は次の3つです。
- メンバー関数または拡張関数として
iterator()を持っていて、Iterator<>を返すこと - その
Iterator<>は、メンバー関数または拡張関数としてnext()を持っていること - その
Iterator<>は、メンバー関数または拡張関数としてhasNext()を持っていること
つまり、Kotlin の for 文はイテレーターでループしているだけで、以下の for 文のループ と while文のループ は実質的に同じ処理になります。
val list: List<Int> = listOf(1, 2, 3)
for (i in list) {
println(i)
}
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 型のループがどういう仕組みで動作しているのか見ていきましょう。
for (x in 1..10) {
println(x)
}
IntRange
1..10 の .. は範囲を表現するための演算子で、rangeTo メソッドで実装されています。
そのため、明示的にメソッドを呼んでも同じように動作します。
val range: IntRange = 1.rangeTo(10)
for (x in range) {
println(x)
}
rangeTo メソッドの戻り値は IntRange です。Int の「範囲」を表すクラスで、端点の値(start と endInclusive)をプロパティとして持っています。
このクラスの iterator() メソッドでは start から endInclusive まで数値が 1 ずつ増えるイテレーターが返ります(実際には親クラスの IntProgression で定義されています。長くなるので説明は割愛します)。
LocalDate 型でループする
ClosedRange
LocalDate は Java のクラスであり、rangeTo メソッドを持ちませんが、Kotlin 標準ライブラリに Comparable<T>.rangeTo メソッドが定義されているため Range を作成可能です。ただしその戻り値は ClosedRange<T> 型です。
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 クラスを作成します。
class LocalDateRange(
override val start: LocalDate,
override val endInclusive: LocalDate
) : ClosedRange<LocalDate>
LocalDate の拡張関数として rangeTo メソッドを定義して LocalDateRange を返すようにします。
operator fun LocalDate.rangeTo(that: LocalDate) = LocalDateRange(this, that)
これで .. 演算子を使ったときに LocalDateRange が返されるようになりました。
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() メソッドを実装します。
class LocalDateRange(
override val start: LocalDate,
override val endInclusive: LocalDate
) : ClosedRange<LocalDate>, Iterable<LocalDate> {
override fun iterator(): Iterator<LocalDate> = LocalDateIterator(start, endInclusive)
}
そこで返すイテレーターは次のように定義しています。このクラスはふつうのイテレーターです。内部状態として次に返す値(next)を持っていて、要素を取り出すたびに1日日付を進めます。終了日(endInclusive)を超えたら hasNext() が false を返すようになります。
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日ずつループすることが可能になります。
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 文でループ可能です。
class LocalDateRange(
override val start: LocalDate,
override val endInclusive: LocalDate
) : ClosedRange<LocalDate> {
operator fun iterator(): Iterator<LocalDate> = LocalDateIterator(start, endInclusive)
}
2. Iterator クラスも必須ではない
実はイテレーターはクラスとして定義する必要もなく、iterator 関数を使うと簡単に定義できます。詳細はリファレンスを参照していただきたいのですが、簡単に言えば next() を呼ぶたびに yield() に渡した値を返すイテレーターが作成されます。
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)
}
}
}
最終的なコード
// 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)
}
}