commonMain.io.islandtime.ranges.DateProgressions.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-metadata Show documentation
Show all versions of core-metadata Show documentation
A multiplatform library for working with dates and times
The newest version!
package io.islandtime.ranges
import io.islandtime.Date
import io.islandtime.internal.MONTHS_PER_YEAR
import io.islandtime.measures.*
import kotlin.math.abs
abstract class DateDayProgression internal constructor(): Iterable {
abstract val first: Date
abstract val last: Date
abstract val step: IntDays
/**
* Checks if this progression is empty.
*/
abstract fun isEmpty(): Boolean
override fun iterator(): Iterator = DateDayProgressionIterator(first, last, step)
companion object {
fun fromClosedRange(rangeStart: Date, rangeEnd: Date, step: IntDays): DateDayProgression {
return DefaultDateDayProgression(rangeStart, rangeEnd, step)
}
}
}
private class DefaultDateDayProgression(
start: Date,
endInclusive: Date,
override val step: IntDays
) : DateDayProgression() {
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"
}
}
override val first: Date = start
override val last: Date = getLastDateInProgression(start, endInclusive, step)
override fun isEmpty(): Boolean = if (step.value > 0) first > last else first < last
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()) || (first == other.first && last == other.last && step == other.step))
}
override fun hashCode(): Int {
return if (isEmpty()) {
-1
} else {
31 * (31 * first.hashCode() + last.hashCode()) + step.value
}
}
}
class DateMonthProgression private constructor(
start: Date,
endInclusive: Date,
val step: IntMonths
) : Iterable {
init {
require(step.value != 0) { "Step must be non-zero" }
}
val first = start
val last = getLastDateInProgression(start, endInclusive, step)
/**
* Checks if this progression is empty.
*/
fun isEmpty(): Boolean = if (step.value > 0) first > last else first < last
override fun iterator(): Iterator = 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 {
return other is DateMonthProgression &&
((isEmpty() && other.isEmpty()) || (first == other.first && last == other.last && step == other.step))
}
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)
}
}
}
/**
* Creates a progression of dates in descending order.
*/
infix fun Date.downTo(to: Date) = DateDayProgression.fromClosedRange(this, to, (-1).days)
/**
* Reverses this progression such that it counts down instead of up, or vice versa.
*/
fun DateDayProgression.reversed() = DateDayProgression.fromClosedRange(last, first, -step)
/**
* Creates a progression that steps over the dates in this progression 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)
}
/**
* Creates a progression that steps over the dates in this progression in increments of weeks.
*/
infix fun DateDayProgression.step(step: IntWeeks) = this.step(step.inDays)
/**
* Creates a progression that steps over the dates in this progression 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)
}
/**
* Creates a progression that steps over the dates in this progression in increments of years.
*/
infix fun DateDayProgression.step(step: IntYears) = this.step(step.inMonths)
/**
* Creates a progression that steps over the dates in this progression in increments of decades.
*/
infix fun DateDayProgression.step(step: IntDecades) = this.step(step.inMonths)
/**
* Creates a progression that steps over the dates in this progression in increments of centuries.
*/
infix fun DateDayProgression.step(step: IntCenturies) = this.step(step.inMonths)
/**
* Reverses this progression such that it counts down instead of up, or vice versa.
*/
fun DateMonthProgression.reversed() = DateMonthProgression.fromClosedRange(last, first, -step)
/**
* Assumes step is non-zero
*/
private fun getLastDateInProgression(start: Date, end: Date, step: IntDays): Date {
return when {
step.value > 0L -> if (start >= end) {
end
} else {
val endEpochDay = end.dayOfUnixEpoch
Date.fromDayOfUnixEpoch(endEpochDay - (abs(endEpochDay - start.dayOfUnixEpoch) % step.value))
}
else -> if (start <= end) {
end
} else {
val endEpochDay = end.dayOfUnixEpoch
Date.fromDayOfUnixEpoch(endEpochDay - (abs(start.dayOfUnixEpoch - end.dayOfUnixEpoch) % step.value))
}
}
}
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
}
}
/**
* Gets 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.
*/
private 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