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)
}
}