-
Notifications
You must be signed in to change notification settings - Fork 108
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
Statically distinguish date-only and date+time units and periods #7
Changes from all commits
5927c75
399d940
200a434
4de15c3
5cd7e63
9eba478
c719ba9
545f8a4
1beb5ea
b4b0c01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/* | ||
* Copyright 2019-2020 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 | ||
|
||
import kotlin.time.Duration | ||
import kotlin.time.ExperimentalTime | ||
import kotlin.time.nanoseconds | ||
|
||
sealed class DateTimeUnit { | ||
|
||
abstract operator fun times(scalar: Int): DateTimeUnit | ||
|
||
internal abstract val calendarUnit: CalendarUnit | ||
internal abstract val calendarScale: Long | ||
|
||
class TimeBased(val nanoseconds: Long) : DateTimeUnit() { | ||
internal override val calendarUnit: CalendarUnit | ||
internal override val calendarScale: Long | ||
|
||
init { | ||
require(nanoseconds > 0) { "Unit duration must be positive, but was $nanoseconds ns." } | ||
when { | ||
nanoseconds % 3600_000_000_000 == 0L -> { | ||
calendarUnit = CalendarUnit.HOUR | ||
calendarScale = nanoseconds / 3600_000_000_000 | ||
} | ||
nanoseconds % 60_000_000_000 == 0L -> { | ||
calendarUnit = CalendarUnit.MINUTE | ||
calendarScale = nanoseconds / 60_000_000_000 | ||
} | ||
nanoseconds % 1_000_000_000 == 0L -> { | ||
calendarUnit = CalendarUnit.SECOND | ||
calendarScale = nanoseconds / 1_000_000_000 | ||
} | ||
nanoseconds % 1_000_000 == 0L -> { | ||
calendarUnit = CalendarUnit.MILLISECOND | ||
calendarScale = nanoseconds / 1_000_000 | ||
} | ||
nanoseconds % 1_000 == 0L -> { | ||
calendarUnit = CalendarUnit.MICROSECOND | ||
calendarScale = nanoseconds / 1_000 | ||
} | ||
else -> { | ||
calendarUnit = CalendarUnit.NANOSECOND | ||
calendarScale = nanoseconds | ||
} | ||
} | ||
} | ||
|
||
override fun times(scalar: Int): TimeBased = TimeBased(nanoseconds * scalar) // TODO: prevent overflow | ||
|
||
@ExperimentalTime | ||
val duration: Duration = nanoseconds.nanoseconds | ||
|
||
override fun equals(other: Any?): Boolean = | ||
this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds) | ||
|
||
override fun hashCode(): Int = nanoseconds.toInt() xor (nanoseconds shr Int.SIZE_BITS).toInt() | ||
|
||
override fun toString(): String = formatToString(calendarScale, calendarUnit.toString()) | ||
} | ||
|
||
sealed class DateBased : DateTimeUnit() { | ||
// TODO: investigate how to move subclasses to ChronoUnit scope | ||
class DayBased(val days: Int) : DateBased() { | ||
init { | ||
require(days > 0) { "Unit duration must be positive, but was $days days." } | ||
} | ||
|
||
override fun times(scalar: Int): DayBased = DayBased(days * scalar) | ||
|
||
internal override val calendarUnit: CalendarUnit get() = CalendarUnit.DAY | ||
internal override val calendarScale: Long get() = days.toLong() | ||
|
||
override fun equals(other: Any?): Boolean = | ||
this === other || (other is DayBased && this.days == other.days) | ||
|
||
override fun hashCode(): Int = days xor 0x10000 | ||
|
||
override fun toString(): String = if (days % 7 == 0) | ||
formatToString(days / 7, "WEEK") | ||
else | ||
formatToString(days, "DAY") | ||
} | ||
class MonthBased(val months: Int) : DateBased() { | ||
init { | ||
require(months > 0) { "Unit duration must be positive, but was $months months." } | ||
} | ||
|
||
override fun times(scalar: Int): MonthBased = MonthBased(months * scalar) | ||
|
||
internal override val calendarUnit: CalendarUnit get() = CalendarUnit.MONTH | ||
internal override val calendarScale: Long get() = months.toLong() | ||
|
||
override fun equals(other: Any?): Boolean = | ||
this === other || (other is MonthBased && this.months == other.months) | ||
|
||
override fun hashCode(): Int = months xor 0x20000 | ||
|
||
override fun toString(): String = when { | ||
months % 12_00 == 0 -> formatToString(months / 12_00, "CENTURY") | ||
months % 12 == 0 -> formatToString(months / 12, "YEAR") | ||
months % 3 == 0 -> formatToString(months / 3, "QUARTER") | ||
else -> formatToString(months, "MONTH") | ||
} | ||
} | ||
} | ||
|
||
protected fun formatToString(value: Int, unit: String): String = if (value == 1) unit else "$value-$unit" | ||
protected fun formatToString(value: Long, unit: String): String = if (value == 1L) unit else "$value-$unit" | ||
|
||
companion object { | ||
val NANOSECOND = TimeBased(nanoseconds = 1) | ||
val MICROSECOND = NANOSECOND * 1000 | ||
val MILLISECOND = MICROSECOND * 1000 | ||
val SECOND = MILLISECOND * 1000 | ||
val MINUTE = SECOND * 60 | ||
val HOUR = MINUTE * 60 | ||
val DAY = DateBased.DayBased(days = 1) | ||
val WEEK = DAY * 7 | ||
val MONTH = DateBased.MonthBased(months = 1) | ||
val QUARTER = MONTH * 3 | ||
val YEAR = MONTH * 12 | ||
val CENTURY = YEAR * 100 | ||
} | ||
} | ||
|
||
|
||
internal enum class CalendarUnit { | ||
YEAR, | ||
MONTH, | ||
DAY, | ||
HOUR, | ||
MINUTE, | ||
SECOND, | ||
MILLISECOND, | ||
MICROSECOND, | ||
NANOSECOND | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,30 +74,25 @@ public fun String.toInstant(): Instant = Instant.parse(this) | |
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in | ||
* [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant | ||
public expect fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant | ||
|
||
/** | ||
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant | ||
|
||
/** | ||
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant | ||
internal expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant | ||
|
||
/** | ||
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod | ||
public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod | ||
|
||
/** | ||
* The return value is clamped to [Long.MAX_VALUE] or [Long.MIN_VALUE] if [unit] is more granular than | ||
* [CalendarUnit.DAY] and the result is too large. | ||
* | ||
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. | ||
*/ | ||
public expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long | ||
public expect fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long | ||
|
||
/** | ||
* The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic | ||
|
@@ -106,7 +101,7 @@ public expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZo | |
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.daysUntil(other: Instant, zone: TimeZone): Int = | ||
until(other, CalendarUnit.DAY, zone).clampToInt() | ||
until(other, DateTimeUnit.DAY, zone).clampToInt() | ||
|
||
/** | ||
* The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic | ||
|
@@ -115,7 +110,7 @@ public fun Instant.daysUntil(other: Instant, zone: TimeZone): Int = | |
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = | ||
until(other, CalendarUnit.MONTH, zone).clampToInt() | ||
until(other, DateTimeUnit.MONTH, zone).clampToInt() | ||
|
||
/** | ||
* The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic | ||
|
@@ -124,7 +119,33 @@ public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = | |
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = | ||
until(other, CalendarUnit.YEAR, zone).clampToInt() | ||
until(other, DateTimeUnit.YEAR, zone).clampToInt() | ||
|
||
private fun Long.clampToInt(): Int = | ||
// TODO: move to internal utils | ||
internal fun Long.clampToInt(): Int = | ||
if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() | ||
|
||
public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other.periodUntil(this, zone) | ||
|
||
|
||
/** | ||
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant = | ||
plus(unit.calendarScale, unit.calendarUnit, zone) | ||
|
||
// TODO: safeMultiply | ||
/** | ||
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant = | ||
plus(value * unit.calendarScale, unit.calendarUnit, zone) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to provide an example of how this method could throw (assuming
In effect, we're asking the system to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we can specialize |
||
|
||
/** | ||
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. | ||
*/ | ||
public fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = | ||
plus(value * unit.calendarScale, unit.calendarUnit, zone) | ||
|
||
|
||
public fun Instant.minus(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = other.until(this, unit, zone) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,23 +38,39 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) | |
* @throws IllegalArgumentException if the calendar unit is not date-based. | ||
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. | ||
*/ | ||
expect fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate | ||
internal expect fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate | ||
|
||
/** | ||
* @throws IllegalArgumentException if the calendar unit is not date-based. | ||
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. | ||
*/ | ||
expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate | ||
internal expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate | ||
|
||
/** | ||
* @throws IllegalArgumentException if [period] has non-zero time (as opposed to date) components. | ||
* @throws DateTimeArithmeticException if arithmetic overflow occurs or the boundaries of [LocalDate] are exceeded at | ||
* any point in intermediate computations. | ||
*/ | ||
expect operator fun LocalDate.plus(period: CalendarPeriod): LocalDate | ||
expect operator fun LocalDate.plus(period: DatePeriod): LocalDate | ||
|
||
/** */ | ||
expect fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod | ||
expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod | ||
|
||
/** */ | ||
operator fun LocalDate.minus(other: LocalDate): CalendarPeriod = other.periodUntil(this) | ||
operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.periodUntil(this) | ||
|
||
public expect fun LocalDate.daysUntil(other: LocalDate): Int | ||
public expect fun LocalDate.monthsUntil(other: LocalDate): Int | ||
public expect fun LocalDate.yearsUntil(other: LocalDate): Int | ||
|
||
public fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = | ||
plus(unit.calendarScale, unit.calendarUnit) | ||
public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = | ||
plus(value * unit.calendarScale, unit.calendarUnit) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, too, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will be used later, after commonizing that function. |
||
public fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = | ||
plus(value * unit.calendarScale, unit.calendarUnit) | ||
|
||
public fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { | ||
is DateTimeUnit.DateBased.MonthBased -> (monthsUntil(other) / unit.months).toInt() | ||
is DateTimeUnit.DateBased.DayBased -> (daysUntil(other) / unit.days).toInt() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think splitting
CalendarUnit
into a similar date/time hierarchy of sealed classes here could simplify some of our implementation code, but I don't know whether this approach has some other downsides, performance etc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to migrate our implementation code to
DateTimeUnit
.CalendarUnit
is left mostly for conversion toChronoUnit
when interfacing with external implementation.