Skip to content

Commit

Permalink
Partial revamp of ranges/intervals (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikc5000 committed Aug 17, 2020
1 parent 37163e9 commit 0e51988
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 179 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ fun ZonedDateTimeInterval.toInstantInterval(): InstantInterval {
return (this as TimePointInterval<*>).toInstantInterval()
}

private inline fun <T> TimeInterval<T>.toDateRange(toDateTime: T.() -> DateTime): DateRange {
private inline fun <T> Interval<T>.toDateRange(toDateTime: T.() -> DateTime): DateRange {
return when {
isEmpty() -> DateRange.EMPTY
isUnbounded() -> DateRange.UNBOUNDED
Expand Down
23 changes: 9 additions & 14 deletions core/src/commonMain/kotlin/io/islandtime/ranges/DateIterators.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
package io.islandtime.ranges

import io.islandtime.Date
import io.islandtime.measures.*

abstract class DateIterator : Iterator<Date> {
override fun next() = nextDate()

abstract fun nextDate(): Date
}
import io.islandtime.measures.IntDays
import io.islandtime.measures.IntMonths

internal class DateDayProgressionIterator(
first: LongDays,
last: LongDays,
first: Date,
last: Date,
private val step: IntDays
) : DateIterator() {
) : Iterator<Date> {

private val finalElement = last
private var hasNext = if (step.value > 0) first <= last else first >= last
private var next = if (hasNext) first else finalElement

override fun hasNext() = hasNext

override fun nextDate(): Date {
override fun next(): Date {
val value = next

if (value == finalElement) {
Expand All @@ -34,23 +29,23 @@ internal class DateDayProgressionIterator(
next += step
}

return Date.fromDaysSinceUnixEpoch(value)
return value
}
}

internal class DateMonthProgressionIterator(
first: Date,
last: Date,
private val step: IntMonths
) : DateIterator() {
) : Iterator<Date> {

private val finalElement = last
private var hasNext = if (step.value > 0) first <= last else first >= last
private var next = if (hasNext) first else finalElement

override fun hasNext() = hasNext

override fun nextDate(): Date {
override fun next(): Date {
val value = next

if (value == finalElement) {
Expand Down
104 changes: 51 additions & 53 deletions core/src/commonMain/kotlin/io/islandtime/ranges/DateProgressions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,30 @@ import io.islandtime.internal.MONTHS_PER_YEAR
import io.islandtime.measures.*
import kotlin.math.abs

open class DateDayProgression protected constructor(
first: Date,
abstract class DateDayProgression internal constructor(): Iterable<Date> {
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<Date> = 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,
val step: IntDays
) : Iterable<Date> {
override val step: IntDays
) : DateDayProgression() {

init {
require(step.value != 0) { "Step must be non-zero" }
Expand All @@ -18,45 +37,29 @@ open class DateDayProgression protected constructor(
}
}

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)

/**
* Checks if this progression is empty.
*/
open fun isEmpty(): Boolean = if (step.value > 0) first > last else first < last
override val first: Date = start
override val last: Date = getLastDateInProgression(start, endInclusive, step)

override fun iterator(): DateIterator = DateDayProgressionIterator(firstUnixEpochDay, lastUnixEpochDay, 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() ||
firstUnixEpochDay == other.firstUnixEpochDay &&
lastUnixEpochDay == other.lastUnixEpochDay &&
step == other.step)
((isEmpty() && other.isEmpty()) || (first == other.first && last == other.last && 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)
31 * (31 * first.hashCode() + last.hashCode()) + step.value
}
}
}

class DateMonthProgression private constructor(
val first: Date,
start: Date,
endInclusive: Date,
val step: IntMonths
) : Iterable<Date> {
Expand All @@ -65,32 +68,29 @@ class DateMonthProgression private constructor(
require(step.value != 0) { "Step must be non-zero" }
}

val last = getLastDateInProgression(first, endInclusive, step)
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(): DateIterator = DateMonthProgressionIterator(first, last, step)
override fun iterator(): Iterator<Date> = 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
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
return if (isEmpty()) {
-1
} else {
31 * (31 * first.hashCode() + last.hashCode()) + step.value
}
}

companion object {
Expand Down Expand Up @@ -154,27 +154,25 @@ fun DateMonthProgression.reversed() = DateMonthProgression.fromClosedRange(last,
/**
* Assumes step is non-zero
*/
private fun getLastDayInProgression(startInDays: LongDays, endDate: Date, step: IntDays): LongDays {
val endInDays = endDate.daysSinceUnixEpoch

private fun getLastDateInProgression(start: Date, end: Date, step: IntDays): Date {
return when {
step.value > 0L -> if (startInDays >= endInDays) {
endInDays
step.value > 0L -> if (start >= end) {
end
} else {
endInDays - (abs(endInDays.value - startInDays.value) % step.value).days
val endEpochDay = end.dayOfUnixEpoch
Date.fromDayOfUnixEpoch(endEpochDay - (abs(endEpochDay - start.dayOfUnixEpoch) % step.value))
}
else -> if (startInDays <= endInDays) {
endInDays
else -> if (start <= end) {
end
} else {
endInDays - (abs(startInDays.value - endInDays.value) % step.value).days
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)
) {
return if ((step.value > 0 && start >= end) || (step.value < 0 && start <= end)) {
end
} else {
val monthsBetween = progressionMonthsBetween(start, end)
Expand All @@ -184,11 +182,11 @@ private fun getLastDateInProgression(start: Date, end: Date, step: IntMonths): D
}

/**
* Gets the number of months between two dates for the purposes of a progression. This works a little differently than
* 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.
*/
internal fun progressionMonthsBetween(start: Date, endInclusive: Date): IntMonths {
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)

Expand Down
60 changes: 22 additions & 38 deletions core/src/commonMain/kotlin/io/islandtime/ranges/DateRange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.islandtime.base.DateTimeField
import io.islandtime.internal.MONTHS_PER_YEAR
import io.islandtime.measures.*
import io.islandtime.parser.*
import io.islandtime.ranges.internal.buildIsoString
import io.islandtime.ranges.internal.throwUnboundedIntervalException
import kotlin.random.Random
import kotlin.random.nextLong
Expand All @@ -13,62 +14,45 @@ import kotlin.random.nextLong
* An inclusive range of dates.
*
* [Date.MIN] and [Date.MAX] are used as sentinels to indicate an unbounded (ie. infinite) start or end.
*
* @property start The start of this interval, inclusive.
* @property endInclusive The end of this interval, inclusive.
*/
class DateRange(
start: Date = Date.MIN,
endInclusive: Date = Date.MAX
) : DateDayProgression(start, endInclusive, 1.days),
override val start: Date = Date.MIN,
override val endInclusive: Date = Date.MAX
) : DateDayProgression(),
Interval<Date>,
ClosedRange<Date> {

fun hasUnboundedStart(): Boolean = start == Date.MIN
fun hasUnboundedEnd(): Boolean = endInclusive == Date.MAX
override val endExclusive: Date get() = endInclusive + 1.days

fun hasBoundedStart(): Boolean = !hasUnboundedStart()
fun hasBoundedEnd(): Boolean = !hasUnboundedEnd()
fun isBounded(): Boolean = hasBoundedStart() && hasBoundedEnd()
fun isUnbounded(): Boolean = hasUnboundedStart() && hasUnboundedEnd()
override fun hasUnboundedStart(): Boolean = start == Date.MIN
override fun hasUnboundedEnd(): Boolean = endInclusive == Date.MAX
override fun contains(value: Date): Boolean = super.contains(value)

override val start: Date get() = first
override val endInclusive: Date get() = last
override val first: Date get() = start
override val last: Date get() = endInclusive
override val step: IntDays get() = 1.days

override fun isEmpty(): Boolean {
return firstUnixEpochDay > lastUnixEpochDay || endInclusive == Date.MIN || start == Date.MAX
return start > endInclusive || endInclusive == Date.MIN || start == Date.MAX
}

/**
* Converts this range to a string in ISO-8601 extended format.
*/
override fun toString(): String {
return if (isEmpty()) {
""
} else {
buildString(2 * MAX_DATE_STRING_LENGTH + 1) {
if (hasBoundedStart()) {
appendDate(start)
} else {
append("..")
}

append('/')

if (hasBoundedEnd()) {
appendDate(endInclusive)
} else {
append("..")
}
}
}
}
override fun toString(): String = buildIsoString(
maxElementSize = MAX_DATE_STRING_LENGTH,
inclusive = true,
appendFunction = StringBuilder::appendDate
)

override fun equals(other: Any?): Boolean {
return other is DateRange && (isEmpty() && other.isEmpty() || first == other.first && last == other.last)
return other is DateRange &&
((isEmpty() && other.isEmpty()) ||
(start == other.start && endInclusive == other.endInclusive))
}

override fun hashCode(): Int {
return if (isEmpty()) -1 else (31 * first.hashCode() + last.hashCode())
return if (isEmpty()) -1 else (31 * start.hashCode() + endInclusive.hashCode())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import kotlin.random.Random
class DateTimeInterval(
override val start: DateTime = UNBOUNDED.start,
override val endExclusive: DateTime = UNBOUNDED.endExclusive
) : TimeInterval<DateTime> {
) : Interval<DateTime> {

override val endInclusive: DateTime
get() = if (hasUnboundedEnd()) endExclusive else endExclusive - 1.nanoseconds

override fun hasUnboundedStart(): Boolean = start == DateTime.MIN
override fun hasUnboundedEnd(): Boolean = endExclusive == DateTime.MAX
Expand Down Expand Up @@ -45,7 +48,11 @@ class DateTimeInterval(
/**
* Converts this interval to a string in ISO-8601 extended format.
*/
override fun toString() = buildIsoString(MAX_DATE_TIME_STRING_LENGTH, StringBuilder::appendDateTime)
override fun toString() = buildIsoString(
maxElementSize = MAX_DATE_TIME_STRING_LENGTH,
inclusive = false,
appendFunction = StringBuilder::appendDateTime
)

/**
* Converts this interval to the [Duration] between the start and end date-time, assuming they're in the same time
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ class InstantInterval(
/**
* Converts this interval to a string in ISO-8601 extended format.
*/
override fun toString() = buildIsoString(MAX_INSTANT_STRING_LENGTH, StringBuilder::appendInstant)
override fun toString() = buildIsoString(
maxElementSize = MAX_INSTANT_STRING_LENGTH,
inclusive = false,
appendFunction = StringBuilder::appendInstant
)

companion object {
/**
Expand Down
Loading

0 comments on commit 0e51988

Please sign in to comment.