commonMain.io.islandtime.ranges.DateProgressions.kt Maven / Gradle / Ivy
package io.islandtime.ranges
import io.islandtime.Date
import io.islandtime.internal.MONTHS_PER_YEAR
import io.islandtime.measures.*
import kotlin.math.abs
open class DateDayProgression protected constructor(
first: Date,
endInclusive: Date,
val step: IntDays
) : Iterable {
init {
require(step.value != 0) { "Step must be non-zero" }
require(step.value != Int.MIN_VALUE) {
"Step must be greater than Int.MIN_VALUE to avoid overflow on negation"
}
}
protected val firstUnixEpochDay: LongDays = first.daysSinceUnixEpoch
protected val lastUnixEpochDay: LongDays = getLastDayInProgression(firstUnixEpochDay, endInclusive, step)
val first: Date get() = Date.fromDaysSinceUnixEpoch(firstUnixEpochDay)
val last: Date get() = Date.fromDaysSinceUnixEpoch(lastUnixEpochDay)
/** Is the progression empty? */
open fun isEmpty(): Boolean = if (step.value > 0) first > last else first < last
override fun iterator(): DateIterator = DateDayProgressionIterator(firstUnixEpochDay, lastUnixEpochDay, step)
override fun toString() = if (step.value > 0) "$first..$last step $step" else "$first downTo $last step ${-step}"
override fun equals(other: Any?): Boolean {
return other is DateDayProgression &&
(isEmpty() && other.isEmpty() ||
firstUnixEpochDay == other.firstUnixEpochDay &&
lastUnixEpochDay == other.lastUnixEpochDay &&
step == other.step)
}
override fun hashCode(): Int {
return if (isEmpty()) {
-1
} else {
31 * (31 * firstUnixEpochDay.hashCode() + lastUnixEpochDay.hashCode()) + step.value
}
}
companion object {
fun fromClosedRange(rangeStart: Date, rangeEnd: Date, step: IntDays): DateDayProgression {
return DateDayProgression(rangeStart, rangeEnd, step)
}
}
}
class DateMonthProgression private constructor(
val first: Date,
endInclusive: Date,
val step: IntMonths
) : Iterable {
init {
require(step.value != 0) { "Step must be non-zero" }
}
val last = getLastDateInProgression(first, endInclusive, step)
/** Is the progression empty? */
fun isEmpty(): Boolean = if (step.value > 0) first > last else first < last
override fun iterator(): DateIterator = DateMonthProgressionIterator(first, last, step)
override fun toString() = if (step.value > 0) "$first..$last step $step" else "$first downTo $last step ${-step}"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as DateMonthProgression
if (first != other.first) return false
if (step != other.step) return false
if (last != other.last) return false
return true
}
override fun hashCode(): Int {
return if (isEmpty()) -1 else 31 * (31 * first.hashCode() + last.hashCode()) + step.value
}
companion object {
fun fromClosedRange(rangeStart: Date, rangeEnd: Date, step: IntMonths): DateMonthProgression {
return DateMonthProgression(rangeStart, rangeEnd, step)
}
}
}
/**
* A get progression of dates in descending order
*/
infix fun Date.downTo(to: Date) = DateDayProgression.fromClosedRange(this, to, (-1).days)
/**
* Reverse a progression such that it counts down instead of up, or vice versa
*/
fun DateDayProgression.reversed() = DateDayProgression.fromClosedRange(last, first, -step)
/**
* Step over dates in increments of days
*/
infix fun DateDayProgression.step(step: IntDays): DateDayProgression {
require(step.value > 0) { "step must be positive" }
return DateDayProgression.fromClosedRange(first, last, if (this.step.value > 0) step else -step)
}
infix fun DateDayProgression.step(step: IntWeeks) = this.step(step.inDays)
/**
* Step over dates in increments of months
*/
infix fun DateDayProgression.step(step: IntMonths): DateMonthProgression {
require(step > 0.months) { "step must be positive" }
return DateMonthProgression.fromClosedRange(first, last, if (this.step.value > 0) step else -step)
}
infix fun DateDayProgression.step(step: IntYears) = this.step(step.inMonths)
infix fun DateDayProgression.step(step: IntDecades) = this.step(step.inMonths)
infix fun DateDayProgression.step(step: IntCenturies) = this.step(step.inMonths)
fun DateMonthProgression.reversed() = DateMonthProgression.fromClosedRange(last, first, -step)
/**
* Assumes step is non-zero
*/
private fun getLastDayInProgression(startInDays: LongDays, endDate: Date, step: IntDays): LongDays {
val endInDays = endDate.daysSinceUnixEpoch
return when {
step.value > 0L -> if (startInDays >= endInDays) {
endInDays
} else {
endInDays - (abs(endInDays.value - startInDays.value) % step.value).days
}
else -> if (startInDays <= endInDays) {
endInDays
} else {
endInDays - (abs(startInDays.value - endInDays.value) % step.value).days
}
}
}
private fun getLastDateInProgression(start: Date, end: Date, step: IntMonths): Date {
return if ((step.value > 0 && start >= end) ||
(step.value < 0 && start <= end)
) {
end
} else {
val monthsBetween = progressionMonthsBetween(start, end)
val steppedMonths = monthsBetween - (monthsBetween % step.value)
start + steppedMonths
}
}
/**
* Get the number of months between two dates for the purposes of a progression. This works a little differently than
* the usual [monthsBetween] since it tries to use the same day as the start date while stepping months, coercing that
* day as needed to fit the number of days in the current month.
*/
internal fun progressionMonthsBetween(start: Date, endInclusive: Date): IntMonths {
val yearsBetween = endInclusive.year - start.year
val monthsBetween = yearsBetween * MONTHS_PER_YEAR + (endInclusive.month.ordinal - start.month.ordinal)
// Deal with variable month lengths
val coercedStartDay = start.dayOfMonth.coerceAtMost(endInclusive.month.lastDayIn(endInclusive.year))
val monthAdjustment = when {
start > endInclusive && endInclusive.dayOfMonth > coercedStartDay -> 1
endInclusive > start && endInclusive.dayOfMonth < coercedStartDay -> -1
else -> 0
}
return (monthsBetween + monthAdjustment).months
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy