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

Implement the same ranges for all platforms #453

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
9 changes: 7 additions & 2 deletions core/androidNative/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
package kotlinx.datetime.internal

import kotlinx.cinterop.*
import kotlinx.datetime.TimeZone
import platform.posix.*

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
internal actual fun timeZoneById(zoneId: String): TimeZone =
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)

internal actual fun getAvailableZoneIds(): Set<String> =
tzdb.getOrThrow().availableTimeZoneIds()

private val tzdb = runCatching { TzdbBionic() }

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> = memScoped {
val name = readSystemProperty("persist.sys.timezone")
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
return name to null
Expand Down
26 changes: 13 additions & 13 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,33 @@ kotlin {
}
}

val commonKotlinMain by creating {
dependsOn(commonMain.get())
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
}
}

val commonKotlinTest by creating {
dependsOn(commonTest.get())
}

val jvmMain by getting {
}

val jvmTest by getting {
}

val commonJsMain by creating {
dependsOn(commonMain.get())
dependsOn(commonKotlinMain)
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
implementation(npm("@js-joda/core", "3.2.0"))
}
}

val commonJsTest by creating {
dependsOn(commonTest.get())
dependsOn(commonKotlinTest)
dependencies {
implementation(npm("@js-joda/timezone", "2.3.0"))
}
Expand All @@ -211,17 +222,6 @@ kotlin {
dependsOn(commonJsTest)
}

val commonKotlinMain by creating {
dependsOn(commonMain.get())
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
}
}

val commonKotlinTest by creating {
dependsOn(commonTest.get())
}

val nativeMain by getting {
dependsOn(commonKotlinMain)
}
Expand Down
43 changes: 22 additions & 21 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import kotlinx.serialization.Serializable
@Serializable(with = DateTimePeriodIso8601Serializer::class)
// TODO: could be error-prone without explicitly named params
public sealed class DateTimePeriod {
internal abstract val totalMonths: Int
internal abstract val totalMonths: Long

/**
* The number of calendar days. Can be negative.
Expand All @@ -90,14 +90,14 @@ public sealed class DateTimePeriod {
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public val years: Int get() = totalMonths / 12
public val years: Int get() = (totalMonths / 12).toInt()

/**
* The number of months in this period that don't form a whole year, so this value is always in `(-11..11)`.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public val months: Int get() = totalMonths % 12
public val months: Int get() = (totalMonths % 12).toInt()

/**
* The number of whole hours in this period. Can be negative.
Expand Down Expand Up @@ -131,7 +131,7 @@ public sealed class DateTimePeriod {
public open val nanoseconds: Int get() = (totalNanoseconds % NANOS_PER_ONE).toInt()

private fun allNonpositive() =
totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or days != 0 || totalNanoseconds != 0L)
totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or totalNanoseconds != 0L || days != 0)

/**
* Converts this period to the ISO 8601 string representation for durations, for example, `P2M1DT3H`.
Expand Down Expand Up @@ -186,7 +186,7 @@ public sealed class DateTimePeriod {
}

override fun hashCode(): Int {
var result = totalMonths
var result = totalMonths.hashCode()
result = 31 * result + days
result = 31 * result + totalNanoseconds.hashCode()
return result
Expand Down Expand Up @@ -314,7 +314,7 @@ public sealed class DateTimePeriod {
while (i < text.length && text[i] in '0'..'9') {
try {
number = safeAdd(safeMultiply(number, 10), (text[i] - '0').toLong())
} catch (e: ArithmeticException) {
} catch (_: ArithmeticException) {
parseException("The number is too large", iStart)
}
i += 1
Expand Down Expand Up @@ -432,7 +432,7 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this
*/
@Serializable(with = DatePeriodIso8601Serializer::class)
public class DatePeriod internal constructor(
internal override val totalMonths: Int,
internal override val totalMonths: Long,
override val days: Int,
) : DateTimePeriod() {
/**
Expand All @@ -448,7 +448,8 @@ public class DatePeriod internal constructor(
* (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit.DateBased] instead.
* For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
*
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
* @throws IllegalArgumentException if the total number of years
* (together with full years in [months]) overflows an [Int].
* @sample kotlinx.datetime.test.samples.DatePeriodSamples.construction
*/
public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days)
Expand All @@ -464,7 +465,7 @@ public class DatePeriod internal constructor(

/** The number of nanoseconds in this period. Always equal to zero. */
override val nanoseconds: Int get() = 0
internal override val totalNanoseconds: Long get() = 0
override val totalNanoseconds: Long get() = 0

public companion object {
/**
Expand Down Expand Up @@ -494,17 +495,16 @@ public class DatePeriod internal constructor(
public fun String.toDatePeriod(): DatePeriod = DatePeriod.parse(this)

private class DateTimePeriodImpl(
internal override val totalMonths: Int,
override val totalMonths: Long,
override val days: Int,
internal override val totalNanoseconds: Long,
override val totalNanoseconds: Long,
) : DateTimePeriod()

// TODO: these calculations fit in a JS Number. Possible to do an expect/actual here.
private fun totalMonths(years: Int, months: Int): Int =
when (val totalMonths = years.toLong() * 12 + months.toLong()) {
in Int.MIN_VALUE..Int.MAX_VALUE -> totalMonths.toInt()
else -> throw IllegalArgumentException("The total number of months in $years years and $months months overflows an Int")
private fun totalMonths(years: Int, months: Int): Long = (years.toLong() * 12 + months.toLong()).also {
require(it / 12 in Int.MIN_VALUE..Int.MAX_VALUE) {
"The total number of years in $years years and $months months overflows an Int"
}
}

private fun totalNanoseconds(hours: Int, minutes: Int, seconds: Int, nanoseconds: Long): Long {
val totalMinutes: Long = hours.toLong() * 60 + minutes
Expand All @@ -517,12 +517,12 @@ private fun totalNanoseconds(hours: Int, minutes: Int, seconds: Int, nanoseconds
// absolute value at most 2^44 + 2^31 < 2^45
return try {
multiplyAndAdd(totalSeconds, 1_000_000_000, nanoseconds % NANOS_PER_ONE)
} catch (e: ArithmeticException) {
} catch (_: ArithmeticException) {
throw IllegalArgumentException("The total number of nanoseconds in $hours hours, $minutes minutes, $seconds seconds, and $nanoseconds nanoseconds overflows a Long")
}
}

internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanoseconds: Long): DateTimePeriod =
internal fun buildDateTimePeriod(totalMonths: Long = 0, days: Int = 0, totalNanoseconds: Long): DateTimePeriod =
if (totalNanoseconds != 0L)
DateTimePeriodImpl(totalMonths, days, totalNanoseconds)
else
Expand All @@ -541,9 +541,10 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos
* (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit] instead.
* For example, instead of `DateTimePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
*
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
* @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds]
* overflows a [Long].
* @throws IllegalArgumentException if the total number of years
* (together with full years in [months]) overflows an [Int].
* @throws IllegalArgumentException if the total number of nanoseconds in
* [hours], [minutes], [seconds] and [nanoseconds] overflows a [Long].
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.constructorFunction
*/
public fun DateTimePeriod(
Expand Down
40 changes: 19 additions & 21 deletions core/common/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ import kotlin.time.*
* ```
*
* For values very far in the past or the future, this conversion may fail.
* The specific range of values that can be converted to [LocalDateTime] is platform-specific, but at least
* [DISTANT_PAST], [DISTANT_FUTURE], and all values between them can be converted to [LocalDateTime].
* The specific range of values that can be converted to [LocalDateTime] is unspecified, but at least
* [DISTANT_PAST], [DISTANT_FUTURE], and all values between them are included in that range.
*
* #### Date or time separately
*
Expand Down Expand Up @@ -225,7 +225,8 @@ public expect class Instant : Comparable<Instant> {
*
* Any fractional part of a millisecond is rounded toward zero to the whole number of milliseconds.
*
* If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result.
* If the result does not fit in [Long],
* returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result.
*
* @see fromEpochMilliseconds
* @sample kotlinx.datetime.test.samples.InstantSamples.toEpochMilliseconds
Expand All @@ -238,7 +239,7 @@ public expect class Instant : Comparable<Instant> {
* If the [duration] is positive, the returned instant is later than this instant.
* If the [duration] is negative, the returned instant is earlier than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* **Pitfall**: [Duration.Companion.days] are multiples of 24 hours and are not calendar-based.
* Consider using the [plus] overload that accepts a multiple of a [DateTimeUnit] instead for calendar-based
Expand All @@ -255,7 +256,7 @@ public expect class Instant : Comparable<Instant> {
* If the [duration] is positive, the returned instant is earlier than this instant.
* If the [duration] is negative, the returned instant is later than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* **Pitfall**: [Duration.Companion.days] are multiples of 24 hours and are not calendar-based.
* Consider using the [minus] overload that accepts a multiple of a [DateTimeUnit] instead for calendar-based
Expand Down Expand Up @@ -319,8 +320,7 @@ public expect class Instant : Comparable<Instant> {
/**
* Returns an [Instant] that is [epochMilliseconds] number of milliseconds from the epoch instant `1970-01-01T00:00:00Z`.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented.
* Every value of [epochMilliseconds] is guaranteed to be representable as an [Instant].
*
* Note that [Instant] also supports nanosecond precision via [fromEpochSeconds].
*
Expand All @@ -333,7 +333,7 @@ public expect class Instant : Comparable<Instant> {
* Returns an [Instant] that is the [epochSeconds] number of seconds from the epoch instant `1970-01-01T00:00:00Z`
* and the [nanosecondAdjustment] number of nanoseconds from the whole second.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
* In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented.
*
* [fromEpochMilliseconds] is a similar function for when input data only has millisecond precision.
Expand All @@ -348,7 +348,7 @@ public expect class Instant : Comparable<Instant> {
* Returns an [Instant] that is the [epochSeconds] number of seconds from the epoch instant `1970-01-01T00:00:00Z`
* and the [nanosecondAdjustment] number of nanoseconds from the whole second.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
* In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented.
*
* [fromEpochMilliseconds] is a similar function for when input data only has millisecond precision.
Expand Down Expand Up @@ -389,7 +389,7 @@ public expect class Instant : Comparable<Instant> {
* An instant value that is far in the past.
*
* All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be [converted][Instant.toLocalDateTime] to
* [LocalDateTime] without exceptions in every time zone on all supported platforms.
* [LocalDateTime] without exceptions in every time zone.
*
* [isDistantPast] returns true for this value and all earlier ones.
*/
Expand All @@ -399,7 +399,7 @@ public expect class Instant : Comparable<Instant> {
* An instant value that is far in the future.
*
* All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be [converted][Instant.toLocalDateTime] to
* [LocalDateTime] without exceptions in every time zone on all supported platforms.
* [LocalDateTime] without exceptions in every time zone.
*
* [isDistantFuture] returns true for this value and all later ones.
*/
Expand Down Expand Up @@ -467,7 +467,7 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst
public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant =
/* An overflow can happen for any component, but we are only worried about nanoseconds, as having an overflow in
any other component means that `plus` will throw due to the minimum value of the numeric type overflowing the
platform-specific limits. */
`Instant` limits. */
if (period.totalNanoseconds != Long.MIN_VALUE) {
val negatedPeriod = with(period) { buildDateTimePeriod(-totalMonths, -days, -totalNanoseconds) }
plus(negatedPeriod, timeZone)
Expand All @@ -487,7 +487,6 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant =
* - Exactly zero if this instant is equal to the other.
*
* @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime].
* Or (only on the JVM) if the number of months between the two dates exceeds an Int.
* @sample kotlinx.datetime.test.samples.InstantSamples.periodUntil
*/
public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod
Expand Down Expand Up @@ -526,7 +525,7 @@ public fun Instant.until(other: Instant, unit: DateTimeUnit.TimeBased): Long =
NANOS_PER_ONE.toLong(),
(other.nanosecondsOfSecond - nanosecondsOfSecond).toLong(),
unit.nanoseconds)
} catch (e: ArithmeticException) {
} catch (_: ArithmeticException) {
if (this < other) Long.MAX_VALUE else Long.MIN_VALUE
}

Expand Down Expand Up @@ -577,7 +576,6 @@ public fun Instant.yearsUntil(other: Instant, timeZone: TimeZone): Int =
* - Exactly zero if this instant is equal to the other.
*
* @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime].
* Or (only on the JVM) if the number of months between the two dates exceeds an Int.
* @see Instant.periodUntil
* @sample kotlinx.datetime.test.samples.InstantSamples.minusInstantInZone
*/
Expand Down Expand Up @@ -613,7 +611,7 @@ public fun Instant.minus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
*
* The returned instant is later than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*/
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))
public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant =
Expand All @@ -624,7 +622,7 @@ public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant =
*
* The returned instant is earlier than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*/
@Deprecated("Use the minus overload with an explicit number of units", ReplaceWith("this.minus(1, unit)"))
public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant =
Expand Down Expand Up @@ -669,7 +667,7 @@ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZo
* If the [value] is positive, the returned instant is later than this instant.
* If the [value] is negative, the returned instant is earlier than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit
*/
Expand All @@ -682,7 +680,7 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant =
* If the [value] is positive, the returned instant is earlier than this instant.
* If the [value] is negative, the returned instant is later than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit
*/
Expand Down Expand Up @@ -730,7 +728,7 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I
* If the [value] is positive, the returned instant is later than this instant.
* If the [value] is negative, the returned instant is earlier than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit
*/
Expand All @@ -742,7 +740,7 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta
* If the [value] is positive, the returned instant is earlier than this instant.
* If the [value] is negative, the returned instant is later than this instant.
*
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
* The return value is clamped to the boundaries of [Instant] if the result exceeds them.
*
* @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit
*/
Expand Down
Loading