Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local date range #189

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,19 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod =
safeAdd(totalNanoseconds, other.totalNanoseconds),
)

/**
* Subtracts one [DateTimePeriod] instance from another.
*
* @throws DateTimeArithmeticException if arithmetic overflow happens.
*/
public operator fun DateTimePeriod.minus(other: DateTimePeriod) : DateTimePeriod = buildDateTimePeriod(
safeAdd(totalMonths, -other.totalMonths),
safeAdd(days, -other.days),
safeAdd(totalNanoseconds, -other.totalNanoseconds)
)

public operator fun DateTimePeriod.unaryMinus(): DateTimePeriod = buildDateTimePeriod(-totalMonths, -days, -totalNanoseconds)

/**
* Adds two [DatePeriod] instances.
*
Expand All @@ -457,3 +470,15 @@ public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod(
safeAdd(totalMonths, other.totalMonths),
safeAdd(days, other.days),
)

/**
* Subtracts one [DatePeriod] instance from another.
*
* @throws DateTimeArithmeticException if arithmetic overflow happens.
*/
public operator fun DatePeriod.minus(other: DatePeriod): DatePeriod = DatePeriod(
safeAdd(totalMonths, -other.totalMonths),
safeAdd(days, -other.days)
)

public operator fun DatePeriod.unaryMinus(): DatePeriod = DatePeriod(-totalMonths, -days)
109 changes: 109 additions & 0 deletions core/common/src/LocalDateRange.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2019-2022 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime

public abstract class LocalDateIterator : Iterator<LocalDate> {
final override fun next(): LocalDate = nextLocalDate()
public abstract fun nextLocalDate(): LocalDate
}

internal class LocalDateProgressionIterator(first: LocalDate, last: LocalDate, val step: DatePeriod) : LocalDateIterator() {
private val finalElement: LocalDate = last
private val increasing = step.positive()
private var hasNext: Boolean = if (increasing) first <= last else first >= last
private var next: LocalDate = if (hasNext) first else finalElement

override fun hasNext(): Boolean = hasNext

override fun nextLocalDate(): LocalDate {
if(!hasNext) throw NoSuchElementException()
val value = next
next += step
/**
* Some [DatePeriod]s with opposite-signed constituent parts can get stuck in an infinite loop rather than progressing toward the far future or far past.
* A period of P1M-28D for example, when added to any date in February, will return that same date, thus leading to a loop.
*/
if(next == value) throw IllegalStateException("Progression has hit an infinite loop. Check to ensure that the the values for total months and days in the provided step DatePeriod are not equal and opposite for certain month(s).")
if ((increasing && next > finalElement) || (!increasing && next < finalElement)) {
hasNext = false
}
return value
}
}

public open class LocalDateProgression
internal constructor
(
start: LocalDate,
endInclusive: LocalDate,
public val step: DatePeriod
) : Iterable<LocalDate> {
init {
if(!step.positive() && !step.negative()) throw IllegalArgumentException("Provided step DatePeriod is of size zero (or equivalent over an arbitrarily long timeline)")
}
public val first: LocalDate = start
public val last: LocalDate = endInclusive

override fun iterator(): LocalDateIterator = LocalDateProgressionIterator(first, last, step)

public open fun isEmpty(): Boolean = if (step.positive()) first > last else first < last


override fun toString(): String = if (step.positive()) "$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 LocalDateProgression

if (first != other.first) return false
if (last != other.last) return false
if (step != other.step) return false

return true
}

override fun hashCode(): Int {
var result = first.hashCode()
result = 31 * result + last.hashCode()
result = 31 * result + step.hashCode()
return result
}

public companion object {
public fun fromClosedRange(rangeStart: LocalDate, rangeEnd: LocalDate, step: DatePeriod): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, step)
}
}

public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, DatePeriod(days = 1)), ClosedRange<LocalDate> {
override val start: LocalDate get() = first
override val endInclusive: LocalDate get() = last

@Suppress("ConvertTwoComparisonsToRangeCheck")
override fun contains(value: LocalDate): Boolean = first <= value && value <= last

override fun isEmpty(): Boolean = first > last

override fun toString(): String = "$first..$last"

public companion object {
private val DATE_ONE = LocalDate(1, 1, 1)
private val DATE_TWO = LocalDate(1, 1, 2)
public val EMPTY: LocalDateRange = LocalDateRange(DATE_TWO, DATE_ONE)
}
}

/**
* On an arbitrarily long timeline, the average month will be 30.436875 days long (146097 days over 400 years).
*/
public fun DatePeriod.positive() : Boolean = totalMonths * 30.436875 + days > 0
public fun DatePeriod.negative() : Boolean = totalMonths * 30.436875 + days < 0

public infix fun LocalDateProgression.step(step: DatePeriod) : LocalDateProgression = LocalDateProgression.fromClosedRange(first, last, step)
public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, DatePeriod(days = -1))

public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that)
10 changes: 10 additions & 0 deletions core/common/test/DateTimePeriodTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,19 @@ class DateTimePeriodTest {
assertEquals(DatePeriod(years = 2, months = 12), dp1 + dp1)
assertEquals(DateTimePeriod(years = 1, months = 6, days = 3), p2 + dp1)

assertEquals(p1, DateTimePeriod(years=10, days=3, hours=2) - p2 -p3)
assertEquals(p1, DatePeriod(years = 11, months = 6) - dp1)
assertEquals(dp1, DatePeriod(years = 2, months = 12) - dp1)
assertEquals(p2, DateTimePeriod(years = 1, months = 6, days = 3) - dp1)

val dp2 = dp1 + p3 + p4
val dp3 = dp2 - p3 -p4
assertEquals(dp1, dp2)
assertEquals(dp1, dp3)
assertTrue(dp2 is DatePeriod)
assertTrue(dp3 is DatePeriod)
assertEquals(DateTimePeriod(years = -10, days=-3, hours = -2), -(p1 + p2 + p3))
assertEquals(DatePeriod(years = -11, months = -6), -(dp1 + p1))
}

@Test
Expand Down