Kotlin: How to iterate all dates within a Joda Interval?
Asked Answered
T

4

9

I'd like to iterate all dates within a given Joda interval:

val interval = Interval(DateTime.now().minusDays(42), DateTime.now())

How to do that in Kotlin?

Torino answered 29/8, 2018 at 12:14 Comment(1)
"A time interval represents a period of time between two instants. Intervals are inclusive of the start instant and exclusive of the end."Torino
T
4

The following extension function gives a Sequence of LocalDate objects from the given Interval, which can be used to iterate those dates.

fun Interval.toLocalDates(): Sequence<LocalDate> = generateSequence(start) { d ->
    d.plusDays(1).takeIf { it < end }
}.map(DateTime::toLocalDate)

Usage:

val interval = Interval(DateTime.now().minusDays(42), DateTime.now())
interval.toLocalDates().forEach {
    println(it)
}

In this solution, the last day, DateTime.now() is not included in the Sequence since that's how Interval is implemented as well:

"A time interval represents a period of time between two instants. Intervals are inclusive of the start instant and exclusive of the end."

If, for any reason, you want to make it include the last day, just change the takeIf condition to it <= end.

Torino answered 29/8, 2018 at 12:16 Comment(1)
why you need the Interval to create this sequence? I mean, just have that sequence function alone from 2 instances sounds already solved no?Reface
A
6

Heavily inspired by your current solution:

fun Interval.toDateTimes() = generateSequence(start) { it.plusDays(1) }
                                                 .takeWhile(::contains) 

Usage:

interval.toDateTimes()
        .forEach { println(it) }

If you need the LocalDate you could still do the following instead:

interval.toDateTimes()
        .map(DateTime::toLocalDate)
        .forEach { println(it) }

or as an extension function to Interval again:

fun Interval.toLocalDates() = toDateTimes().map(DateTime::toLocalDate)

If you want the end date to be inclusive instead, use takeWhile { it <= end } instead.

Armorial answered 29/8, 2018 at 12:44 Comment(2)
Yes, I need LocalDate, so the only improvement would maybe be the function reference, which I really prefer, thanksTorino
adapted it... using takeWhile now, which - I think - is even more readable.Armorial
T
4

The following extension function gives a Sequence of LocalDate objects from the given Interval, which can be used to iterate those dates.

fun Interval.toLocalDates(): Sequence<LocalDate> = generateSequence(start) { d ->
    d.plusDays(1).takeIf { it < end }
}.map(DateTime::toLocalDate)

Usage:

val interval = Interval(DateTime.now().minusDays(42), DateTime.now())
interval.toLocalDates().forEach {
    println(it)
}

In this solution, the last day, DateTime.now() is not included in the Sequence since that's how Interval is implemented as well:

"A time interval represents a period of time between two instants. Intervals are inclusive of the start instant and exclusive of the end."

If, for any reason, you want to make it include the last day, just change the takeIf condition to it <= end.

Torino answered 29/8, 2018 at 12:16 Comment(1)
why you need the Interval to create this sequence? I mean, just have that sequence function alone from 2 instances sounds already solved no?Reface
C
4

I guess if you need it more than once, it would be better to overload rangeTo operator to allow this syntax

for (i in LocalDate.now() .. LocalDate.now().plusWeeks(1)) {
    System.out.print(i) // 2018-08-30 2018-08-31 2018-09-01 
}

Here is the code for operator extension:

operator fun LocalDate.rangeTo(other: LocalDate): LocalDateRange {
    return LocalDateRange(this, other)
}

And necessary classes:

class LocalDateRange(override val start: LocalDate, override val endInclusive: LocalDate)
    : ClosedRange<LocalDate>, Iterable<LocalDate> {
    override fun iterator(): Iterator<LocalDate> {
        return DateIterator(start, endInclusive)
    }
}

class DateIterator(start: LocalDate, private val endInclusive: LocalDate)
    : Iterator<LocalDate> {

    private var current = start

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

    override fun next(): LocalDate {
        current = current.plusDays(1)
        return current
    }
}
Clancy answered 29/8, 2018 at 13:17 Comment(1)
If you increment the date like that, you'll never get the start value from the iterator. return current.also { current = current.plusDays(1) } works better.Chap
C
4

LocalDate is preferred nowadays, so we can simply iterate with day as number:

for (day in minDate.toEpochDay()..maxDate.toEpochDay()) {
    // ...
}

or:

(minDate.toEpochDay()..maxDate.toEpochDay()).forEach {
    // ...
}

Iterate with day as date:

generateSequence(minDate) { it.plusDays(1) }.takeWhile { it < maxDate }.forEach {
    // it ...
}

or:

var day = minDate;
while (day < maxDate) {
    day = day.plusDays(1);
    // ...
}
Critique answered 28/3, 2019 at 20:51 Comment(2)
The question is for JodaTime so your first two answers won't work, as toEpochDay is only on java.time LocalDate. I know we should all be on java.time but some of us are stuck with Joda! Your third answer is the same as the other answers in this thread. But your fourth answer works great and is the simplest solution to Kotlin's lack of a 'traditional' for loop.Cozmo
Joda and Local can be perfectly combined and piece by piece moved from Joda to Local. So I would say it's just about preference.Edmon

© 2022 - 2024 — McMap. All rights reserved.