From 79ddbfb9547adea21f455d5b47bdbf22f4714b9e Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 30 Oct 2024 14:21:02 +0100 Subject: [PATCH 01/14] Implement unified ranges in Native and the JVM --- core/common/src/DateTimePeriod.kt | 31 ++--- core/common/src/Instant.kt | 40 +++--- core/common/src/LocalDate.kt | 38 +++--- core/common/test/DateTimePeriodTest.kt | 7 +- core/common/test/InstantTest.kt | 37 ++---- core/common/test/LocalDateTest.kt | 34 ++--- core/common/test/samples/LocalDateSamples.kt | 10 +- core/commonKotlin/src/Instant.kt | 67 ++++++---- core/commonKotlin/src/LocalDate.kt | 122 +++++++++--------- core/commonKotlin/src/LocalDateTime.kt | 15 +-- core/commonKotlin/src/TimeZone.kt | 2 +- core/commonKotlin/src/ZonedDateTime.kt | 7 +- .../commonKotlin/src/internal/MonthDayTime.kt | 4 +- core/jvm/src/Instant.kt | 7 +- core/jvm/src/LocalDate.kt | 40 +++--- 15 files changed, 221 insertions(+), 240 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index d8499f4d9..25f008b35 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -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. @@ -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. @@ -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`. @@ -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 @@ -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 @@ -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() { /** @@ -464,7 +464,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 { /** @@ -494,17 +494,12 @@ 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() private fun totalNanoseconds(hours: Int, minutes: Int, seconds: Int, nanoseconds: Long): Long { val totalMinutes: Long = hours.toLong() * 60 + minutes @@ -517,12 +512,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 diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 36fd40d28..3deedd946 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -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 * @@ -225,7 +225,8 @@ public expect class Instant : Comparable { * * 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 @@ -238,7 +239,7 @@ public expect class Instant : Comparable { * 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 @@ -255,7 +256,7 @@ public expect class Instant : Comparable { * 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 @@ -319,8 +320,7 @@ public expect class Instant : Comparable { /** * 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]. * @@ -333,7 +333,7 @@ public expect class Instant : Comparable { * 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. @@ -348,7 +348,7 @@ public expect class Instant : Comparable { * 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. @@ -389,7 +389,7 @@ public expect class Instant : Comparable { * 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. */ @@ -399,7 +399,7 @@ public expect class Instant : Comparable { * 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. */ @@ -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) @@ -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 @@ -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 } @@ -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 */ @@ -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 = @@ -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 = @@ -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 */ @@ -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 */ @@ -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 */ @@ -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 */ diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0146d6f95..700a291a5 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -20,6 +20,9 @@ import kotlinx.serialization.Serializable * is `2020-08-31` everywhere): see various [LocalDate.plus] and [LocalDate.minus] functions, as well * as [LocalDate.periodUntil] and various other [*until][LocalDate.daysUntil] functions. * + * The range of supported years is at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. + * * ### Arithmetic operations * * Operations with [DateTimeUnit.DateBased] and [DatePeriod] are provided for [LocalDate]: @@ -31,9 +34,6 @@ import kotlinx.serialization.Serializable * * ### Platform specifics * - * The range of supported years is platform-dependent, but at least is enough to represent dates of all instants between - * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. - * * On the JVM, * there are `LocalDate.toJavaLocalDate()` and `java.time.LocalDate.toKotlinLocalDate()` * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. @@ -86,7 +86,15 @@ public expect class LocalDate : Comparable { /** * Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`. * - * @throws IllegalArgumentException if the result exceeds the platform-specific boundaries of [LocalDate]. + * @throws IllegalArgumentException if the result exceeds the boundaries of [LocalDate]. + * @see LocalDate.toEpochDays + * @sample kotlinx.datetime.test.samples.LocalDateSamples.fromAndToEpochDays + */ + public fun fromEpochDays(epochDays: Long): LocalDate + + /** + * Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`. + * * @see LocalDate.toEpochDays * @sample kotlinx.datetime.test.samples.LocalDateSamples.fromAndToEpochDays */ @@ -160,7 +168,7 @@ public expect class LocalDate : Comparable { * The components [monthNumber] and [dayOfMonth] are 1-based. * * The supported ranges of components: - * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between + * - [year] the range is at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [monthNumber] `1..12` * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -175,7 +183,7 @@ public expect class LocalDate : Comparable { * Constructs a [LocalDate] instance from the given date components. * * The supported ranges of components: - * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between + * - [year] the range at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [month] all values of the [Month] enum * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -231,12 +239,10 @@ public expect class LocalDate : Comparable { /** * Returns the number of days since the epoch day `1970-01-01`. * - * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result. - * * @see LocalDate.fromEpochDays * @sample kotlinx.datetime.test.samples.LocalDateSamples.toEpochDays */ - public fun toEpochDays(): Int + public fun toEpochDays(): Long /** * Compares `this` date with the [other] date. @@ -344,7 +350,7 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = * - Negative or zero if this date is later than the other. * - Exactly zero if this date is equal to the other. * - * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). + * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int. * * @see LocalDate.minus for the same operation with the order of arguments reversed. * @sample kotlinx.datetime.test.samples.LocalDateSamples.periodUntil @@ -361,7 +367,7 @@ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod * - Positive or zero if this date is later than the other. * - Exactly zero if this date is equal to the other. * - * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). + * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int. * * @see LocalDate.periodUntil for the same operation with the order of arguments reversed. * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusDate @@ -378,14 +384,12 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * * The value is rounded toward zero. * - * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result. - * * @see LocalDate.daysUntil * @see LocalDate.monthsUntil * @see LocalDate.yearsUntil * @sample kotlinx.datetime.test.samples.LocalDateSamples.until */ -public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int +public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Long /** * Returns the number of whole days between two dates. @@ -416,8 +420,6 @@ public expect fun LocalDate.monthsUntil(other: LocalDate): Int * * The value is rounded toward zero. * - * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result. - * * @see LocalDate.until * @sample kotlinx.datetime.test.samples.LocalDateSamples.yearsUntil */ @@ -458,7 +460,7 @@ public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, u * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. * @sample kotlinx.datetime.test.samples.LocalDateSamples.plus */ -public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate +public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(value.toLong(), unit) /** * Returns a [LocalDate] that results from subtracting the [value] number of the specified [unit] from this date. @@ -471,7 +473,7 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ -public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate +public fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value.toLong(), unit) /** * Returns a [LocalDate] that results from adding the [value] number of the specified [unit] to this date. diff --git a/core/common/test/DateTimePeriodTest.kt b/core/common/test/DateTimePeriodTest.kt index 0ff578acb..3c93e4a9d 100644 --- a/core/common/test/DateTimePeriodTest.kt +++ b/core/common/test/DateTimePeriodTest.kt @@ -114,9 +114,10 @@ class DateTimePeriodTest { assertFailsWith { DateTimePeriod.parse("P") } - // overflow of `Int.MAX_VALUE` months - assertFailsWith { DateTimePeriod.parse("P2000000000Y") } - assertFailsWith { DateTimePeriod.parse("P1Y2147483640M") } + // overflow of `Long.MAX_VALUE` months + assertFailsWith { DateTimePeriod.parse("P768614336404564651Y") } + assertFailsWith { DateTimePeriod.parse("P1Y9223372036854775805M") } + assertFailsWith { DateTimePeriod.parse("PT+-2H") } // too large a number in a field diff --git a/core/common/test/InstantTest.kt b/core/common/test/InstantTest.kt index 6fd784bf5..4788e2f68 100644 --- a/core/common/test/InstantTest.kt +++ b/core/common/test/InstantTest.kt @@ -467,33 +467,16 @@ class InstantRangeTest { @Test fun epochMillisecondsClamping() { - // toEpochMilliseconds()/fromEpochMilliseconds() - // assuming that ranges of Long (representing a number of milliseconds) and Instant are not just overlapping, - // but one is included in the other. - if (Instant.MAX.epochSeconds > Long.MAX_VALUE / 1000) { - /* Any number of milliseconds in Long is representable as an Instant */ - for (instant in largePositiveInstants) { - assertEquals(Long.MAX_VALUE, instant.toEpochMilliseconds(), "$instant") - } - for (instant in largeNegativeInstants) { - assertEquals(Long.MIN_VALUE, instant.toEpochMilliseconds(), "$instant") - } - for (milliseconds in largePositiveLongs + largeNegativeLongs) { - assertEquals(milliseconds, Instant.fromEpochMilliseconds(milliseconds).toEpochMilliseconds(), - "$milliseconds") - } - } else { - /* Any Instant is representable as a number of milliseconds in Long */ - for (milliseconds in largePositiveLongs) { - assertEquals(Instant.MAX, Instant.fromEpochMilliseconds(milliseconds), "$milliseconds") - } - for (milliseconds in largeNegativeLongs) { - assertEquals(Instant.MIN, Instant.fromEpochMilliseconds(milliseconds), "$milliseconds") - } - for (instant in largePositiveInstants + smallInstants + largeNegativeInstants) { - assertEquals(instant.epochSeconds, - Instant.fromEpochMilliseconds(instant.toEpochMilliseconds()).epochSeconds, "$instant") - } + /* Any number of milliseconds in Long is representable as an Instant */ + for (instant in largePositiveInstants) { + assertEquals(Long.MAX_VALUE, instant.toEpochMilliseconds(), "$instant") + } + for (instant in largeNegativeInstants) { + assertEquals(Long.MIN_VALUE, instant.toEpochMilliseconds(), "$instant") + } + for (milliseconds in largePositiveLongs + largeNegativeLongs) { + assertEquals(milliseconds, Instant.fromEpochMilliseconds(milliseconds).toEpochMilliseconds(), + "$milliseconds") } } diff --git a/core/common/test/LocalDateTest.kt b/core/common/test/LocalDateTest.kt index 74a2286db..6d4065626 100644 --- a/core/common/test/LocalDateTest.kt +++ b/core/common/test/LocalDateTest.kt @@ -173,8 +173,8 @@ class LocalDateTest { val (unit, length) = interval val start = LocalDate.parse(v1) val end = LocalDate.parse(v2) - assertEquals(length, start.until(end, unit), "$v2 - $v1 = $length($unit)") - assertEquals(-length, end.until(start, unit), "$v1 - $v2 = -$length($unit)") + assertEquals(length.toLong(), start.until(end, unit), "$v2 - $v1 = $length($unit)") + assertEquals(-length.toLong(), end.until(start, unit), "$v1 - $v2 = -$length($unit)") when (unit) { DateTimeUnit.YEAR -> assertEquals(length, start.yearsUntil(end)) DateTimeUnit.MONTH -> assertEquals(length, start.monthsUntil(end)) @@ -230,29 +230,19 @@ class LocalDateTest { } } - @Test - fun unitsUntilClamping() { - val diffInYears = LocalDate.MIN.until(LocalDate.MAX, DateTimeUnit.YEAR) - if (diffInYears > Int.MAX_VALUE / 365) { - assertEquals(Int.MAX_VALUE, LocalDate.MIN.until(LocalDate.MAX, DateTimeUnit.DAY)) - assertEquals(Int.MIN_VALUE, LocalDate.MAX.until(LocalDate.MIN, DateTimeUnit.DAY)) - } - } @Test fun fromEpochDays() { /** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native, * [LocalDate.plus] is implemented via [LocalDate.toEpochDays]/[LocalDate.fromEpochDays], and so it's better to * test those independently. */ - if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MIN) > Int.MIN_VALUE) { - assertEquals(LocalDate.MIN, LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays())) - assertFailsWith { LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays() - 1) } - assertFailsWith { LocalDate.fromEpochDays(Int.MIN_VALUE) } - } - if (LocalDate.fromEpochDays(0).daysUntil(LocalDate.MAX) < Int.MAX_VALUE) { - assertEquals(LocalDate.MAX, LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays())) - assertFailsWith { LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays() + 1) } - assertFailsWith { LocalDate.fromEpochDays(Int.MAX_VALUE) } - } + assertEquals(LocalDate.MIN, LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays())) + assertFailsWith { LocalDate.fromEpochDays(LocalDate.MIN.toEpochDays() - 1) } + assertFailsWith { LocalDate.fromEpochDays(Long.MIN_VALUE) } + + assertEquals(LocalDate.MAX, LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays())) + assertFailsWith { LocalDate.fromEpochDays(LocalDate.MAX.toEpochDays() + 1) } + assertFailsWith { LocalDate.fromEpochDays(Long.MAX_VALUE) } + val eraBeginning = -678941 - 40587 assertEquals(LocalDate(1970, 1, 1), LocalDate.fromEpochDays(0)) assertEquals(LocalDate(0, 1, 1), LocalDate.fromEpochDays(eraBeginning)) @@ -278,12 +268,12 @@ class LocalDateTest { val startOfEra = -678941 - 40587 var date = LocalDate(0, 1, 1) for (i in startOfEra..699999) { - assertEquals(i, date.toEpochDays()) + assertEquals(i.toLong(), date.toEpochDays()) date = date.next } date = LocalDate(0, 1, 1) for (i in startOfEra downTo -2000000 + 1) { - assertEquals(i, date.toEpochDays()) + assertEquals(i.toLong(), date.toEpochDays()) date = date.previous } assertEquals(-40587, LocalDate(1858, 11, 17).toEpochDays()) diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index 194af1f8e..2ad5fe9b6 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -33,7 +33,7 @@ class LocalDateSamples { fun fromAndToEpochDays() { // Converting LocalDate values to the number of days since 1970-01-01 and back check(LocalDate.fromEpochDays(0) == LocalDate(1970, Month.JANUARY, 1)) - val randomEpochDay = Random.nextInt(-50_000..50_000) + val randomEpochDay = Random.nextLong(-50_000L..50_000L) val randomDate = LocalDate.fromEpochDays(randomEpochDay) check(randomDate.toEpochDays() == randomEpochDay) } @@ -112,9 +112,9 @@ class LocalDateSamples { @Test fun toEpochDays() { // Converting LocalDate values to the number of days since 1970-01-01 - check(LocalDate(2024, Month.APRIL, 16).toEpochDays() == 19829) - check(LocalDate(1970, Month.JANUARY, 1).toEpochDays() == 0) - check(LocalDate(1969, Month.DECEMBER, 25).toEpochDays() == -7) + check(LocalDate(2024, Month.APRIL, 16).toEpochDays() == 19829L) + check(LocalDate(1970, Month.JANUARY, 1).toEpochDays() == 0L) + check(LocalDate(1969, Month.DECEMBER, 25).toEpochDays() == -7L) } @Test @@ -212,7 +212,7 @@ class LocalDateSamples { val startDate = LocalDate(2023, Month.JANUARY, 2) val endDate = LocalDate(2024, Month.APRIL, 1) val differenceInMonths = startDate.until(endDate, DateTimeUnit.MONTH) - check(differenceInMonths == 14) + check(differenceInMonths == 14L) // one year, two months, and 30 days, rounded toward zero. } diff --git a/core/commonKotlin/src/Instant.kt b/core/commonKotlin/src/Instant.kt index 70e0017be..1a779606d 100644 --- a/core/commonKotlin/src/Instant.kt +++ b/core/commonKotlin/src/Instant.kt @@ -29,25 +29,38 @@ public actual enum class DayOfWeek { /** * The minimum supported epoch second. */ -private const val MIN_SECOND = -31619119219200L // -1000000-01-01T00:00:00Z +private const val MIN_SECOND = -31557014167219200L // -1000000000-01-01T00:00:00Z /** * The maximum supported epoch second. */ -private const val MAX_SECOND = 31494816403199L // +1000000-12-31T23:59:59 - -private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second <= MAX_SECOND +private const val MAX_SECOND = 31556889864403199L // +1000000000-12-31T23:59:59 @Serializable(with = InstantIso8601Serializer::class) public actual class Instant internal constructor(public actual val epochSeconds: Long, public actual val nanosecondsOfSecond: Int) : Comparable { init { - require(isValidInstantSecond(epochSeconds)) { "Instant exceeds minimum or maximum instant" } + require(epochSeconds in MIN_SECOND..MAX_SECOND) { "Instant exceeds minimum or maximum instant" } } // org.threeten.bp.Instant#toEpochMilli - public actual fun toEpochMilliseconds(): Long = - epochSeconds * MILLIS_PER_ONE + nanosecondsOfSecond / NANOS_PER_MILLI + public actual fun toEpochMilliseconds(): Long = try { + if (epochSeconds >= 0) { + val millis = safeMultiply(epochSeconds, MILLIS_PER_ONE.toLong()) + safeAdd(millis, (nanosecondsOfSecond / NANOS_PER_MILLI).toLong()) + } else { + // prevent an overflow in seconds * 1000 + // instead of going form the second farther away from 0 + // going toward 0 + // we go from the second closer to 0 away from 0 + // that way we always stay in the valid long range + // seconds + 1 can not overflow because it is negative + val millis = safeMultiply(epochSeconds + 1, MILLIS_PER_ONE.toLong()) + safeAdd(millis, (nanosecondsOfSecond / NANOS_PER_MILLI - MILLIS_PER_ONE).toLong()) + } + } catch (_: ArithmeticException) { + if (epochSeconds > 0) Long.MAX_VALUE else Long.MIN_VALUE + } // org.threeten.bp.Instant#plus(long, long) /** @@ -67,9 +80,9 @@ public actual class Instant internal constructor(public actual val epochSeconds: public actual operator fun plus(duration: Duration): Instant = duration.toComponents { secondsToAdd, nanosecondsToAdd -> try { plus(secondsToAdd, nanosecondsToAdd.toLong()) - } catch (e: IllegalArgumentException) { + } catch (_: IllegalArgumentException) { if (duration.isPositive()) MAX else MIN - } catch (e: ArithmeticException) { + } catch (_: ArithmeticException) { if (duration.isPositive()) MAX else MIN } } @@ -106,13 +119,15 @@ public actual class Instant internal constructor(public actual val epochSeconds: public actual fun now(): Instant = currentTime() // org.threeten.bp.Instant#ofEpochMilli - public actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = - if (epochMilliseconds < MIN_SECOND * MILLIS_PER_ONE) MIN - else if (epochMilliseconds > MAX_SECOND * MILLIS_PER_ONE) MAX - else Instant( - epochMilliseconds.floorDiv(MILLIS_PER_ONE.toLong()), - (epochMilliseconds.mod(MILLIS_PER_ONE.toLong()) * NANOS_PER_MILLI).toInt() - ) + public actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant { + val epochSeconds = epochMilliseconds.floorDiv(MILLIS_PER_ONE.toLong()) + val nanosecondsOfSecond = (epochMilliseconds.mod(MILLIS_PER_ONE.toLong()) * NANOS_PER_MILLI).toInt() + return when { + epochSeconds < MIN_SECOND -> MIN + epochSeconds > MAX_SECOND -> MAX + else -> fromEpochSeconds(epochSeconds, nanosecondsOfSecond) + } + } /** * @throws ArithmeticException if arithmetic overflow occurs @@ -128,9 +143,9 @@ public actual class Instant internal constructor(public actual val epochSeconds: public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = try { fromEpochSecondsThrowing(epochSeconds, nanosecondAdjustment) - } catch (e: ArithmeticException) { + } catch (_: ArithmeticException) { if (epochSeconds > 0) MAX else MIN - } catch (e: IllegalArgumentException) { + } catch (_: IllegalArgumentException) { if (epochSeconds > 0) MAX else MIN } @@ -177,8 +192,8 @@ private fun Instant.check(zone: TimeZone): Instant = this@check.also { public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { val withDate = toZonedDateTimeFailing(timeZone) - .run { if (totalMonths != 0) plus(totalMonths, DateTimeUnit.MONTH) else this } - .run { if (days != 0) plus(days, DateTimeUnit.DAY) else this } + .run { if (totalMonths != 0L) plus(totalMonths.toLong(), DateTimeUnit.MONTH) else this } + .run { if (days != 0) plus(days.toLong(), DateTimeUnit.DAY) else this } withDate.toInstant() .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } }.check(timeZone) @@ -200,7 +215,7 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo is DateTimeUnit.DateBased -> { if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") - toZonedDateTimeFailing(timeZone).plus(value.toInt(), unit).toInstant() + toZonedDateTimeFailing(timeZone).plus(value, unit).toInstant() } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -216,9 +231,9 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) -> plus(seconds, nanoseconds) } - } catch (e: ArithmeticException) { + } catch (_: ArithmeticException) { if (value > 0) Instant.MAX else Instant.MIN - } catch (e: IllegalArgumentException) { + } catch (_: IllegalArgumentException) { if (value > 0) Instant.MAX else Instant.MIN } @@ -226,10 +241,10 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT var thisLdt = toZonedDateTimeFailing(timeZone) val otherLdt = other.toZonedDateTimeFailing(timeZone) - val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH).toInt() // `until` on dates never fails - thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails + thisLdt = thisLdt.plus(months.toLong(), DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid val days = thisLdt.until(otherLdt, DateTimeUnit.DAY).toInt() // `until` on dates never fails - thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt + thisLdt = thisLdt.plus(days.toLong(), DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt val nanoseconds = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) diff --git a/core/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index 14ee3a173..712ac4038 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -16,8 +16,8 @@ import kotlinx.datetime.serializers.LocalDateIso8601Serializer import kotlinx.serialization.Serializable import kotlin.math.* -internal const val YEAR_MIN = -999_999 -internal const val YEAR_MAX = 999_999 +internal const val YEAR_MIN = -999_999_999 +internal const val YEAR_MAX = 999_999_999 private fun isValidYear(year: Int): Boolean = year >= YEAR_MIN && year <= YEAR_MAX @@ -48,22 +48,24 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu public fun parse(isoString: String): LocalDate = parse(input = isoString) // org.threeten.bp.LocalDate#toEpochDay - public actual fun fromEpochDays(epochDays: Int): LocalDate { - // LocalDate(-999999, 1, 1).toEpochDay(), LocalDate(999999, 12, 31).toEpochDay() + public actual fun fromEpochDays(epochDays: Long): LocalDate { + // LocalDate(-999_999_999, 1, 1).toEpochDay(), LocalDate(999_999_999, 12, 31).toEpochDay() require(epochDays in MIN_EPOCH_DAY..MAX_EPOCH_DAY) { - "Invalid date: boundaries of LocalDate exceeded" + "Invalid date: epoch day $epochDays is outside the boundaries of LocalDate" } - var zeroDay = epochDays + DAYS_0000_TO_1970 + var zeroDay = epochDays + DAYS_0000_TO_1970 // -3.7e11 .. 3.7e11 // find the march-based year zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle - var adjust = 0 + var adjust = 0L // -1e9 .. 0 if (zeroDay < 0) { // adjust negative years to positive for calculation - val adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1 + val adjustCycles = ((zeroDay + 1) / DAYS_PER_CYCLE - 1) // -2.5e6 .. -1 adjust = adjustCycles * 400 zeroDay += -adjustCycles * DAYS_PER_CYCLE + // zeroDay = DAYS_PER_CYCLE - (-zeroDay - 1) % DAYS_PER_CYCLE - 1, in 0 ..< DAYS_PER_CYCLE } - var yearEst = ((400 * zeroDay.toLong() + 591) / DAYS_PER_CYCLE).toInt() + // zeroDay in 0 .. 3.7e11 now + var yearEst = ((400 * zeroDay + 591) / DAYS_PER_CYCLE) // -1e9 .. 1e9 var doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400) if (doyEst < 0) { // fix estimate yearEst-- @@ -71,7 +73,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu } yearEst += adjust // reset any negative year - val marchDoy0 = doyEst + val marchDoy0 = doyEst.toInt() // convert march-based values back to january-based val marchMonth0 = (marchDoy0 * 5 + 2) / 153 @@ -79,14 +81,16 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu val dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1 yearEst += marchMonth0 / 10 - return LocalDate(yearEst, month, dom) + return LocalDate(yearEst.toInt(), month, dom) } + public actual fun fromEpochDays(epochDays: Int): LocalDate = fromEpochDays(epochDays.toLong()) + internal actual val MIN = LocalDate(YEAR_MIN, 1, 1) internal actual val MAX = LocalDate(YEAR_MAX, 12, 31) - internal const val MIN_EPOCH_DAY = -365961662 - internal const val MAX_EPOCH_DAY = 364522971 + internal const val MIN_EPOCH_DAY = -365243219162 + internal const val MAX_EPOCH_DAY = 365241780471 @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = @@ -100,10 +104,10 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu } // org.threeten.bp.LocalDate#toEpochDay - public actual fun toEpochDays(): Int { - val y = year - val m = monthNumber - var total = 0 + public actual fun toEpochDays(): Long { + val y = year.toLong() + val m = monthNumber.toLong() + var total = 0L total += 365 * y if (y >= 0) { total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400 @@ -162,15 +166,17 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs */ - internal fun plusMonths(monthsToAdd: Int): LocalDate { - if (monthsToAdd == 0) { + internal fun plusMonths(monthsToAdd: Long): LocalDate { + if (monthsToAdd == 0L) { return this } - val monthCount = year * 12 + (monthNumber - 1) - val calcMonths = safeAdd(monthCount, monthsToAdd) + val calcMonths = safeAdd(prolepticMonth, monthsToAdd) val newYear = calcMonths.floorDiv(12) + if (newYear !in YEAR_MIN..YEAR_MAX) { + throw IllegalArgumentException("The result of adding $monthsToAdd months to $this is out of LocalDate range.") + } val newMonth = calcMonths.mod(12) + 1 - return resolvePreviousValid(newYear, newMonth, dayOfMonth) + return resolvePreviousValid(newYear.toInt(), newMonth, dayOfMonth) } // org.threeten.bp.LocalDate#plusDays @@ -178,8 +184,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs */ - internal fun plusDays(daysToAdd: Int): LocalDate = - if (daysToAdd == 0) this + internal fun plusDays(daysToAdd: Long): LocalDate = + if (daysToAdd == 0L) this else fromEpochDays(safeAdd(toEpochDays(), daysToAdd)) override fun equals(other: Any?): Boolean = @@ -198,34 +204,25 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) -public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = - plus(1, unit) - -public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = - try { - when (unit) { - is DateTimeUnit.DayBased -> plusDays(safeMultiply(value, unit.days)) - is DateTimeUnit.MonthBased -> plusMonths(safeMultiply(value, unit.months)) - } - } catch (e: ArithmeticException) { - throw DateTimeArithmeticException("Arithmetic overflow when adding a value to a date", e) - } catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e) - } +public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plus(1, unit) -public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) - -public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = - if (value > Int.MAX_VALUE || value < Int.MIN_VALUE) - throw DateTimeArithmeticException("Can't add a Long to a LocalDate") // TODO: less specific message - else plus(value.toInt(), unit) +public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = try { + when (unit) { + is DateTimeUnit.DayBased -> plusDays(safeMultiply(value, unit.days.toLong())) + is DateTimeUnit.MonthBased -> plusMonths(safeMultiply(value, unit.months.toLong())) + } +} catch (e: ArithmeticException) { + throw DateTimeArithmeticException("Arithmetic overflow when adding a value to a date", e) +} catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e) +} public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = with(period) { try { this@plus - .run { if (totalMonths != 0) plusMonths(totalMonths) else this } - .run { if (days != 0) plusDays(days) else this } + .run { if (totalMonths != 0L) plusMonths(totalMonths) else this } + .run { if (days != 0) plusDays(days.toLong()) else this } } catch (e: ArithmeticException) { throw DateTimeArithmeticException("Arithmetic overflow when adding a period to a date", e) } catch (e: IllegalArgumentException) { @@ -233,30 +230,31 @@ public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = } } -public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { - is DateTimeUnit.MonthBased -> monthsUntil(other) / unit.months - is DateTimeUnit.DayBased -> daysUntil(other) / unit.days +public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Long = when(unit) { + is DateTimeUnit.MonthBased -> { + val packed1 = prolepticMonth * 32 + dayOfMonth + val packed2 = other.prolepticMonth * 32 + other.dayOfMonth + val result = (packed2 - packed1) / 32 + result / unit.months + } + is DateTimeUnit.DayBased -> { + (other.toEpochDays() - this.toEpochDays()) / unit.days + } } -// org.threeten.bp.LocalDate#daysUntil -public actual fun LocalDate.daysUntil(other: LocalDate): Int = - other.toEpochDays() - this.toEpochDays() - // org.threeten.bp.LocalDate#getProlepticMonth -internal val LocalDate.prolepticMonth get() = (year * 12) + (monthNumber - 1) +private val LocalDate.prolepticMonth get() = (year * 12L) + (monthNumber - 1) + +// org.threeten.bp.LocalDate#daysUntil +public actual fun LocalDate.daysUntil(other: LocalDate): Int = until(other, DateTimeUnit.DAY).clampToInt() // org.threeten.bp.LocalDate#monthsUntil -public actual fun LocalDate.monthsUntil(other: LocalDate): Int { - val packed1 = prolepticMonth * 32 + dayOfMonth - val packed2 = other.prolepticMonth * 32 + other.dayOfMonth - return (packed2 - packed1) / 32 -} +public actual fun LocalDate.monthsUntil(other: LocalDate): Int = until(other, DateTimeUnit.MONTH).clampToInt() -public actual fun LocalDate.yearsUntil(other: LocalDate): Int = - monthsUntil(other) / 12 +public actual fun LocalDate.yearsUntil(other: LocalDate): Int = until(other, DateTimeUnit.YEAR).toInt() public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { - val months = monthsUntil(other) + val months = until(other, DateTimeUnit.MONTH) val days = plusMonths(months).daysUntil(other) return DatePeriod(totalMonths = months, days) } diff --git a/core/commonKotlin/src/LocalDateTime.kt b/core/commonKotlin/src/LocalDateTime.kt index 33187d4a5..e6b7730b6 100644 --- a/core/commonKotlin/src/LocalDateTime.kt +++ b/core/commonKotlin/src/LocalDateTime.kt @@ -79,17 +79,10 @@ public actual constructor(public actual val date: LocalDate, public actual val t secs -= offset.totalSeconds return secs } - - /** - * @throws IllegalArgumentException if the result exceeds the boundaries - * @throws ArithmeticException if arithmetic overflow occurs - */ - internal fun plus(value: Int, unit: DateTimeUnit.DateBased): LocalDateTime = - LocalDateTime(date.plus(value, unit), time) } // org.threeten.bp.LocalDateTime#until -internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Int { +internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long { var endDate: LocalDate = other.date if (endDate > date && other.time < time) { endDate = endDate.plusDays(-1) // won't throw: endDate - date >= 1 @@ -97,8 +90,8 @@ internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBa endDate = endDate.plusDays(1) // won't throw: date - endDate >= 1 } return when (unit) { - is DateTimeUnit.MonthBased -> date.monthsUntil(endDate) / unit.months - is DateTimeUnit.DayBased -> date.daysUntil(endDate) / unit.days + is DateTimeUnit.MonthBased -> date.until(endDate, DateTimeUnit.MONTH) / unit.months + is DateTimeUnit.DayBased -> date.until(endDate, DateTimeUnit.DAY) / unit.days } } @@ -127,7 +120,7 @@ internal fun LocalDateTime.plusSeconds(seconds: Int): LocalDateTime totalNanos.floorDiv(NANOS_PER_DAY) // max 2 days val newNanoOfDay: Long = totalNanos.mod(NANOS_PER_DAY) val newTime: LocalTime = if (newNanoOfDay == currentNanoOfDay) time else LocalTime.ofNanoOfDay(newNanoOfDay) - return LocalDateTime(date.plusDays(totalDays.toInt()), newTime) + return LocalDateTime(date.plusDays(totalDays), newTime) } private val ISO_DATETIME_OPTIONAL_SECONDS_TRAILING_ZEROS by lazy { diff --git a/core/commonKotlin/src/TimeZone.kt b/core/commonKotlin/src/TimeZone.kt index 70dc0e950..aea398dad 100644 --- a/core/commonKotlin/src/TimeZone.kt +++ b/core/commonKotlin/src/TimeZone.kt @@ -141,7 +141,7 @@ internal actual fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime = internal fun Instant.toLocalDateTimeImpl(offset: UtcOffset): LocalDateTime { val localSecond: Long = epochSeconds + offset.totalSeconds // overflow caught later - val localEpochDay = localSecond.floorDiv(SECONDS_PER_DAY.toLong()).toInt() + val localEpochDay = localSecond.floorDiv(SECONDS_PER_DAY.toLong()) val secsOfDay = localSecond.mod(SECONDS_PER_DAY.toLong()).toInt() val date: LocalDate = LocalDate.fromEpochDays(localEpochDay) // may throw val time: LocalTime = LocalTime.ofSecondOfDay(secsOfDay, nanosecondsOfSecond) diff --git a/core/commonKotlin/src/ZonedDateTime.kt b/core/commonKotlin/src/ZonedDateTime.kt index 655da4408..effd5acd0 100644 --- a/core/commonKotlin/src/ZonedDateTime.kt +++ b/core/commonKotlin/src/ZonedDateTime.kt @@ -13,7 +13,8 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs */ - internal fun plus(value: Int, unit: DateTimeUnit.DateBased): ZonedDateTime = dateTime.plus(value, unit).resolve() + internal fun plus(value: Long, unit: DateTimeUnit.DateBased): ZonedDateTime = + dateTime.date.plus(value, unit).atTime(dateTime.time).resolve() // Never throws in practice private fun LocalDateTime.resolve(): ZonedDateTime = @@ -29,7 +30,7 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time override fun equals(other: Any?): Boolean = this === other || other is ZonedDateTime && - dateTime == other.dateTime && offset == other.offset && zone == other.zone + dateTime == other.dateTime && offset == other.offset && zone == other.zone override fun hashCode(): Int { return dateTime.hashCode() xor offset.hashCode() xor zone.hashCode().rotateLeft(3) @@ -59,7 +60,7 @@ internal fun ZonedDateTime.toInstant(): Instant = internal fun ZonedDateTime.until(other: ZonedDateTime, unit: DateTimeUnit): Long = when (unit) { // if the time unit is date-based, the offsets are disregarded and only the dates and times are compared. - is DateTimeUnit.DateBased -> dateTime.until(other.dateTime, unit).toLong() + is DateTimeUnit.DateBased -> dateTime.until(other.dateTime, unit) // if the time unit is not date-based, we need to make sure that [other] is at the same offset as [this]. is DateTimeUnit.TimeBased -> { val offsetDiff = offset.totalSeconds - other.offset.totalSeconds diff --git a/core/commonKotlin/src/internal/MonthDayTime.kt b/core/commonKotlin/src/internal/MonthDayTime.kt index 22cb0a06b..5ac4f7b08 100644 --- a/core/commonKotlin/src/internal/MonthDayTime.kt +++ b/core/commonKotlin/src/internal/MonthDayTime.kt @@ -37,7 +37,7 @@ internal class JulianDayOfYear(val zeroBasedDayOfYear: Int) : DateOfYear { } } override fun toLocalDate(year: Int): LocalDate = - LocalDate(year, 1, 1).plusDays(zeroBasedDayOfYear) + LocalDate(year, 1, 1).plusDays(zeroBasedDayOfYear.toLong()) override fun toString(): String = "JulianDayOfYear($zeroBasedDayOfYear)" } @@ -52,7 +52,7 @@ internal fun JulianDayOfYearSkippingLeapDate(dayOfYear: Int) : DateOfYear { // In this form, the `dayOfYear` corresponds exactly to a specific month and day. // For example, `dayOfYear = 60` is always 1st March, even in leap years. // We take a non-leap year, as in that case, this is the same as JulianDayOfYear, so regular addition works. - val date = LocalDate(2011, 1, 1).plusDays(dayOfYear - 1) + val date = LocalDate(2011, 1, 1).plusDays(dayOfYear.toLong() - 1) return MonthDayOfYear(date.month, MonthDayOfYear.TransitionDay.ExactlyDayOfMonth(date.dayOfMonth)) } diff --git a/core/jvm/src/Instant.kt b/core/jvm/src/Instant.kt index ab3474647..53c670449 100644 --- a/core/jvm/src/Instant.kt +++ b/core/jvm/src/Instant.kt @@ -111,7 +111,7 @@ public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst val thisZdt = atZone(timeZone) return with(period) { thisZdt - .run { if (totalMonths != 0) plusMonths(totalMonths.toLong()) else this } + .run { if (totalMonths != 0L) plusMonths(totalMonths) else this } .run { if (days != 0) plusDays(days.toLong()) else this } .run { if (totalNanoseconds != 0L) plusNanos(totalNanoseconds) else this } }.toInstant().let(::Instant) @@ -164,10 +164,7 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT val days = thisZdt.until(otherZdt, ChronoUnit.DAYS); thisZdt = thisZdt.plusDays(days) val nanoseconds = thisZdt.until(otherZdt, ChronoUnit.NANOS) - if (months > Int.MAX_VALUE || months < Int.MIN_VALUE) { - throw DateTimeArithmeticException("The number of months between $this and $other does not fit in an Int") - } - return buildDateTimePeriod(months.toInt(), days.toInt(), nanoseconds) + return buildDateTimePeriod(months, days.toInt(), nanoseconds) } public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = try { diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index 8d1bcc23d..b817869e4 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -15,6 +15,7 @@ import java.time.DateTimeException import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import java.time.LocalDate as jtLocalDate +import kotlin.internal.* @Serializable(with = LocalDateIso8601Serializer::class) public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { @@ -37,8 +38,14 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN) internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX) - public actual fun fromEpochDays(epochDays: Int): LocalDate = - LocalDate(jtLocalDate.ofEpochDay(epochDays.toLong())) + public actual fun fromEpochDays(epochDays: Long): LocalDate = + try { + LocalDate(jtLocalDate.ofEpochDay(epochDays)) + } catch (e: DateTimeException) { + throw IllegalArgumentException(e) + } + + public actual fun fromEpochDays(epochDays: Int): LocalDate = fromEpochDays(epochDays.toLong()) @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = @@ -76,18 +83,22 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value) - public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt() + public actual fun toEpochDays(): Long = value.toEpochDay() } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plus(1L, unit) -public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = - plus(value.toLong(), unit) +@PublishedApi +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@LowPriorityInOverloadResolution +internal fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(value.toLong(), unit) -public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = - plus(-value.toLong(), unit) +@PublishedApi +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@LowPriorityInOverloadResolution +internal fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value.toLong(), unit) public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = try { @@ -116,7 +127,7 @@ private fun ofEpochDayChecked(epochDay: Long): java.time.LocalDate { public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = try { with(period) { return@with value - .run { if (totalMonths != 0) plusMonths(totalMonths.toLong()) else this } + .run { if (totalMonths != 0L) plusMonths(totalMonths) else this } .run { if (days != 0) plusDays(days.toLong()) else this } }.let(::LocalDate) @@ -131,15 +142,12 @@ public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { val months = startD.until(endD, ChronoUnit.MONTHS); startD = startD.plusMonths(months) val days = startD.until(endD, ChronoUnit.DAYS) - if (months > Int.MAX_VALUE || months < Int.MIN_VALUE) { - throw DateTimeArithmeticException("The number of months between $this and $other does not fit in an Int") - } - return DatePeriod(totalMonths = months.toInt(), days.toInt()) + return DatePeriod(totalMonths = months, days.toInt()) } -public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { - is DateTimeUnit.MonthBased -> (this.value.until(other.value, ChronoUnit.MONTHS) / unit.months).clampToInt() - is DateTimeUnit.DayBased -> (this.value.until(other.value, ChronoUnit.DAYS) / unit.days).clampToInt() +public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Long = when(unit) { + is DateTimeUnit.MonthBased -> (this.value.until(other.value, ChronoUnit.MONTHS) / unit.months) + is DateTimeUnit.DayBased -> (this.value.until(other.value, ChronoUnit.DAYS) / unit.days) } public actual fun LocalDate.daysUntil(other: LocalDate): Int = @@ -149,4 +157,4 @@ public actual fun LocalDate.monthsUntil(other: LocalDate): Int = this.value.until(other.value, ChronoUnit.MONTHS).clampToInt() public actual fun LocalDate.yearsUntil(other: LocalDate): Int = - this.value.until(other.value, ChronoUnit.YEARS).clampToInt() + this.value.until(other.value, ChronoUnit.YEARS).toInt() From ca1fb4e29c749b5e0d5a38ea16dff5c736259c78 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 31 Oct 2024 16:32:19 +0100 Subject: [PATCH 02/14] Implement for JS and Wasm --- core/build.gradle.kts | 26 +-- core/commonJs/src/DayOfWeek.kt | 20 --- core/commonJs/src/Instant.kt | 214 ------------------------- core/commonJs/src/LocalDate.kt | 140 ---------------- core/commonJs/src/LocalDateTime.kt | 86 ---------- core/commonJs/src/LocalTime.kt | 92 ----------- core/commonJs/src/Month.kt | 25 --- core/commonJs/src/PlatformSpecifics.kt | 7 +- core/commonJs/src/TimeZone.kt | 93 ----------- core/commonJs/src/UtcOffset.kt | 83 ---------- core/commonJs/src/internal/Platform.kt | 110 +++++++++++++ core/commonJs/src/internal/mathJs.kt | 60 ------- core/js/src/PlatformSpecifics.kt | 10 +- core/wasmJs/src/PlatformSpecifics.kt | 20 ++- 14 files changed, 148 insertions(+), 838 deletions(-) delete mode 100644 core/commonJs/src/DayOfWeek.kt delete mode 100644 core/commonJs/src/Instant.kt delete mode 100644 core/commonJs/src/LocalDate.kt delete mode 100644 core/commonJs/src/LocalDateTime.kt delete mode 100644 core/commonJs/src/LocalTime.kt delete mode 100644 core/commonJs/src/Month.kt delete mode 100644 core/commonJs/src/TimeZone.kt delete mode 100644 core/commonJs/src/UtcOffset.kt create mode 100644 core/commonJs/src/internal/Platform.kt delete mode 100644 core/commonJs/src/internal/mathJs.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a354a0483..063221406 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -174,6 +174,17 @@ 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 { } @@ -181,7 +192,7 @@ kotlin { } 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")) @@ -189,7 +200,7 @@ kotlin { } val commonJsTest by creating { - dependsOn(commonTest.get()) + dependsOn(commonKotlinTest) dependencies { implementation(npm("@js-joda/timezone", "2.3.0")) } @@ -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) } diff --git a/core/commonJs/src/DayOfWeek.kt b/core/commonJs/src/DayOfWeek.kt deleted file mode 100644 index aff66bbbe..000000000 --- a/core/commonJs/src/DayOfWeek.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 kotlinx.datetime.internal.JSJoda.DayOfWeek as jsDayOfWeek - -public actual enum class DayOfWeek { - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY, - SUNDAY; -} - -internal fun jsDayOfWeek.toDayOfWeek(): DayOfWeek = DayOfWeek(this.value()) \ No newline at end of file diff --git a/core/commonJs/src/Instant.kt b/core/commonJs/src/Instant.kt deleted file mode 100644 index af7cc868a..000000000 --- a/core/commonJs/src/Instant.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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 kotlinx.datetime.format.* -import kotlinx.datetime.internal.JSJoda.Instant as jtInstant -import kotlinx.datetime.internal.JSJoda.Duration as jtDuration -import kotlinx.datetime.internal.JSJoda.Clock as jtClock -import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit -import kotlinx.datetime.internal.JSJoda.ZonedDateTime as jtZonedDateTime -import kotlinx.datetime.internal.safeAdd -import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.InstantIso8601Serializer -import kotlinx.serialization.Serializable -import kotlin.time.* -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.Duration.Companion.seconds - -@Serializable(with = InstantIso8601Serializer::class) -public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { - - public actual val epochSeconds: Long - get() = value.epochSecond().toLong() - public actual val nanosecondsOfSecond: Int - get() = value.nano().toInt() - - public actual fun toEpochMilliseconds(): Long = - epochSeconds * MILLIS_PER_ONE + nanosecondsOfSecond / NANOS_PER_MILLI - - public actual operator fun plus(duration: Duration): Instant = duration.toComponents { seconds, nanoseconds -> - return try { - Instant(plusFix(seconds.toDouble(), nanoseconds)) - } catch (e: Throwable) { - if (!e.isJodaDateTimeException()) throw e - if (duration.isPositive()) MAX else MIN - } - } - - internal fun plusFix(seconds: Double, nanos: Int): jtInstant { - val newSeconds = value.epochSecond() + seconds - val newNanos = value.nano() + nanos - return jsTry { jtInstant.ofEpochSecond(newSeconds, newNanos.toInt()) } - } - - public actual operator fun minus(duration: Duration): Instant = plus(-duration) - - public actual operator fun minus(other: Instant): Duration { - val diff = jtDuration.between(other.value, this.value) - return diff.seconds().seconds + diff.nano().nanoseconds - } - - public actual override operator fun compareTo(other: Instant): Int = this.value.compareTo(other.value) - - override fun equals(other: Any?): Boolean = - (this === other) || (other is Instant && (this.value === other.value || this.value.equals(other.value))) - - override fun hashCode(): Int = value.hashCode() - - actual override fun toString(): String = value.toString() - - public actual companion object { - @Deprecated("Use Clock.System.now() instead", ReplaceWith("Clock.System.now()", "kotlinx.datetime.Clock"), level = DeprecationLevel.ERROR) - public actual fun now(): Instant = - Instant(jtClock.systemUTC().instant()) - - public actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = try { - fromEpochSeconds(epochMilliseconds / MILLIS_PER_ONE, epochMilliseconds % MILLIS_PER_ONE * NANOS_PER_MILLI) - } catch (e: Throwable) { - if (!e.isJodaDateTimeException()) throw e - if (epochMilliseconds > 0) MAX else MIN - } - - // TODO: implement a custom parser to 1) help DCE get rid of the formatting machinery 2) move Instant to stdlib - public actual fun parse(input: CharSequence, format: DateTimeFormat): Instant = try { - // This format is not supported properly by Joda-Time, so we can't delegate to it. - format.parse(input).toInstantUsingOffset() - } catch (e: IllegalArgumentException) { - throw DateTimeFormatException("Failed to parse an instant from '$input'", e) - } - - @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) - public fun parse(isoString: String): Instant = parse(input = isoString) - - public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = try { - /* Performing normalization here because otherwise this fails: - assertEquals((Long.MAX_VALUE % 1_000_000_000).toInt(), - Instant.fromEpochSeconds(0, Long.MAX_VALUE).nanosecondsOfSecond) */ - val secs = safeAdd(epochSeconds, nanosecondAdjustment.floorDiv(NANOS_PER_ONE.toLong())) - val nos = nanosecondAdjustment.mod(NANOS_PER_ONE.toLong()).toInt() - Instant(jsTry { jtInstant.ofEpochSecond(secs.toDouble(), nos) }) - } catch (e: Throwable) { - if (!e.isJodaDateTimeException() && e !is ArithmeticException) throw e - if (epochSeconds > 0) MAX else MIN - } - - public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant = try { - Instant(jsTry { jtInstant.ofEpochSecond(epochSeconds.toDouble(), nanosecondAdjustment) }) - } catch (e: Throwable) { - if (!e.isJodaDateTimeException()) throw e - if (epochSeconds > 0) MAX else MIN - } - - public actual val DISTANT_PAST: Instant = Instant(jsTry { jtInstant.ofEpochSecond(DISTANT_PAST_SECONDS.toDouble(), 999_999_999) }) - public actual val DISTANT_FUTURE: Instant = Instant(jsTry { jtInstant.ofEpochSecond(DISTANT_FUTURE_SECONDS.toDouble(), 0) }) - - internal actual val MIN: Instant = Instant(jtInstant.MIN) - internal actual val MAX: Instant = Instant(jtInstant.MAX) - } -} - - -public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { - val thisZdt = jsTry { this.value.atZone(timeZone.zoneId) } - with(period) { - thisZdt - .run { if (totalMonths != 0) jsTry { plusMonths(totalMonths) } else this } - .run { if (days != 0) jsTry { plusDays(days) } else this } - .run { if (hours != 0) jsTry { plusHours(hours) } else this } - .run { if (minutes != 0) jsTry { plusMinutes(minutes) } else this } - .run { if (seconds != 0) jsTry { plusSeconds(seconds) } else this } - .run { if (nanoseconds != 0) jsTry { plusNanos(nanoseconds.toDouble()) } else this } - }.toInstant().let(::Instant) -} catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) - throw e -} - -private fun Instant.atZone(zone: TimeZone): jtZonedDateTime = jsTry { value.atZone(zone.zoneId) } -private fun jtInstant.checkZone(zone: TimeZone): jtInstant = apply { jsTry { atZone(zone.zoneId) } } - -@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) -public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(1, unit, timeZone) - -public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = - try { - val thisZdt = this.atZone(timeZone) - when (unit) { - is DateTimeUnit.TimeBased -> { - plus(value, unit).value.checkZone(timeZone) - } - is DateTimeUnit.DayBased -> - jsTry {thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant() - is DateTimeUnit.MonthBased -> - jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant() - }.let(::Instant) - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) - throw e - } - -public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - try { - val thisZdt = this.atZone(timeZone) - when (unit) { - is DateTimeUnit.TimeBased -> - plus(value.toLong(), unit).value.checkZone(timeZone) - is DateTimeUnit.DayBased -> - jsTry { thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant() - is DateTimeUnit.MonthBased -> - jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant() - }.let(::Instant) - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) - throw e - } - -public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - if (value == Int.MIN_VALUE) - plus(-value.toLong(), unit, timeZone) - else - plus(-value, unit, timeZone) - -public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant = - try { - multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (d, r) -> - Instant(plusFix(d.toDouble(), r.toInt())) - } - } catch (e: Throwable) { - if (!e.isJodaDateTimeException()) { - throw e - } - if (value > 0) Instant.MAX else Instant.MIN - } - -public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod = try { - var thisZdt = jsTry { this.value.atZone(timeZone.zoneId) } - val otherZdt = jsTry { other.value.atZone(timeZone.zoneId) } - - val months = thisZdt.until(otherZdt, jtChronoUnit.MONTHS); thisZdt = jsTry { thisZdt.plusMonths(months) } - val days = thisZdt.until(otherZdt, jtChronoUnit.DAYS); thisZdt = jsTry { thisZdt.plusDays(days) } - val nanoseconds = thisZdt.until(otherZdt, jtChronoUnit.NANOS) - - buildDateTimePeriod(months.toInt(), days.toInt(), nanoseconds.toLong()) -} catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) else throw e -} - -public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = try { - val thisZdt = this.atZone(timeZone) - val otherZdt = other.atZone(timeZone) - when(unit) { - is DateTimeUnit.TimeBased -> until(other, unit) - is DateTimeUnit.DayBased -> (thisZdt.until(otherZdt, jtChronoUnit.DAYS) / unit.days).toLong() - is DateTimeUnit.MonthBased -> (thisZdt.until(otherZdt, jtChronoUnit.MONTHS) / unit.months).toLong() - } -} catch (e: ArithmeticException) { - if (this < other) Long.MAX_VALUE else Long.MIN_VALUE -} catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) else throw e -} diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt deleted file mode 100644 index bfca5511f..000000000 --- a/core/commonJs/src/LocalDate.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 kotlinx.datetime.format.* -import kotlinx.datetime.internal.removeLeadingZerosFromLongYearFormLocalDate -import kotlinx.datetime.serializers.LocalDateIso8601Serializer -import kotlinx.serialization.Serializable -import kotlinx.datetime.internal.JSJoda.LocalDate as jtLocalDate -import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit - -@Serializable(with = LocalDateIso8601Serializer::class) -public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { - public actual companion object { - - public actual fun parse( - input: CharSequence, - format: DateTimeFormat - ): LocalDate = if (format === Formats.ISO) { - try { - val sanitizedInput = removeLeadingZerosFromLongYearFormLocalDate(input.toString()) - jsTry { jtLocalDate.parse(sanitizedInput.toString()) }.let(::LocalDate) - } catch (e: Throwable) { - if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) - throw e - } - } else { - format.parse(input) - } - - @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) - public fun parse(isoString: String): LocalDate = parse(input = isoString) - - internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN) - internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX) - - public actual fun fromEpochDays(epochDays: Int): LocalDate = try { - LocalDate(jsTry { jtLocalDate.ofEpochDay(epochDays) }) - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) - throw e - } - - @Suppress("FunctionName") - public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = - LocalDateFormat.build(block) - } - - public actual object Formats { - public actual val ISO: DateTimeFormat get() = ISO_DATE - - public actual val ISO_BASIC: DateTimeFormat = ISO_DATE_BASIC - } - - public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) : - this(try { - jsTry { jtLocalDate.of(year, monthNumber, dayOfMonth) } - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) - throw e - }) - - public actual constructor(year: Int, month: Month, dayOfMonth: Int) : this(year, month.number, dayOfMonth) - - public actual val year: Int get() = value.year() - public actual val monthNumber: Int get() = value.monthValue() - public actual val month: Month get() = value.month().toMonth() - public actual val dayOfMonth: Int get() = value.dayOfMonth() - public actual val dayOfWeek: DayOfWeek get() = value.dayOfWeek().toDayOfWeek() - public actual val dayOfYear: Int get() = value.dayOfYear() - - override fun equals(other: Any?): Boolean = - (this === other) || (other is LocalDate && (this.value === other.value || this.value.equals(other.value))) - - override fun hashCode(): Int = value.hashCode() - - actual override fun toString(): String = value.toString() - - actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value) - - public actual fun toEpochDays(): Int = value.toEpochDay().toInt() -} - -@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) -public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit) -public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit) -public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(-value, unit) -public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit) - -private fun LocalDate.plusNumber(value: Number, unit: DateTimeUnit.DateBased): LocalDate = - try { - when (unit) { - is DateTimeUnit.DayBased -> jsTry { this.value.plusDays((value.toDouble() * unit.days).toInt()) } - is DateTimeUnit.MonthBased -> jsTry { this.value.plusMonths((value.toDouble() * unit.months).toInt()) } - }.let(::LocalDate) - } catch (e: Throwable) { - if (!e.isJodaDateTimeException() && !e.isJodaArithmeticException()) throw e - throw DateTimeArithmeticException("The result of adding $value of $unit to $this is out of LocalDate range.", e) - } - - -public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = try { - with(period) { - return@with value - .run { if (totalMonths != 0) jsTry { plusMonths(totalMonths) } else this } - .run { if (days != 0) jsTry { plusDays(days) } else this } - - }.let(::LocalDate) -} catch (e: Throwable) { - if (e.isJodaDateTimeException() || e.isJodaArithmeticException()) throw DateTimeArithmeticException(e) - throw e -} - - - -public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { - var startD = this.value - val endD = other.value - val months = startD.until(endD, jtChronoUnit.MONTHS).toInt(); startD = jsTry { startD.plusMonths(months) } - val days = startD.until(endD, jtChronoUnit.DAYS).toInt() - - return DatePeriod(totalMonths = months, days) -} - -public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { - is DateTimeUnit.MonthBased -> monthsUntil(other) / unit.months - is DateTimeUnit.DayBased -> daysUntil(other) / unit.days -} - -public actual fun LocalDate.daysUntil(other: LocalDate): Int = - this.value.until(other.value, jtChronoUnit.DAYS).toInt() - -public actual fun LocalDate.monthsUntil(other: LocalDate): Int = - this.value.until(other.value, jtChronoUnit.MONTHS).toInt() - -public actual fun LocalDate.yearsUntil(other: LocalDate): Int = - this.value.until(other.value, jtChronoUnit.YEARS).toInt() diff --git a/core/commonJs/src/LocalDateTime.kt b/core/commonJs/src/LocalDateTime.kt deleted file mode 100644 index b8c8f67da..000000000 --- a/core/commonJs/src/LocalDateTime.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019-2022 JetBrains s.r.o. and contributors. - * 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 kotlinx.datetime.format.* -import kotlinx.datetime.format.ISO_DATETIME -import kotlinx.datetime.format.LocalDateTimeFormat -import kotlinx.datetime.internal.removeLeadingZerosFromLongYearFormLocalDateTime -import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer -import kotlinx.serialization.Serializable -import kotlinx.datetime.internal.JSJoda.LocalDateTime as jtLocalDateTime - -@Serializable(with = LocalDateTimeIso8601Serializer::class) -public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable { - - public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : - this(try { - jsTry { jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) } - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) - throw e - }) - - public actual constructor(year: Int, month: Month, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : - this(year, month.number, dayOfMonth, hour, minute, second, nanosecond) - - public actual constructor(date: LocalDate, time: LocalTime) : - this(jsTry { jtLocalDateTime.of(date.value, time.value) }) - - public actual val year: Int get() = value.year() - public actual val monthNumber: Int get() = value.monthValue() - public actual val month: Month get() = value.month().toMonth() - public actual val dayOfMonth: Int get() = value.dayOfMonth() - public actual val dayOfWeek: DayOfWeek get() = value.dayOfWeek().toDayOfWeek() - public actual val dayOfYear: Int get() = value.dayOfYear() - - public actual val hour: Int get() = value.hour() - public actual val minute: Int get() = value.minute() - public actual val second: Int get() = value.second() - public actual val nanosecond: Int get() = value.nano().toInt() - - public actual val date: LocalDate get() = LocalDate(value.toLocalDate()) // cache? - - public actual val time: LocalTime get() = LocalTime(value.toLocalTime()) - - override fun equals(other: Any?): Boolean = - (this === other) || (other is LocalDateTime && (this.value === other.value || this.value.equals(other.value))) - - override fun hashCode(): Int = value.hashCode() - - actual override fun toString(): String = value.toString() - - actual override fun compareTo(other: LocalDateTime): Int = this.value.compareTo(other.value) - - public actual companion object { - public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDateTime = - if (format === Formats.ISO) { - try { - val sanitizedInput = removeLeadingZerosFromLongYearFormLocalDateTime(input.toString()) - jsTry { jtLocalDateTime.parse(sanitizedInput.toString()) }.let(::LocalDateTime) - } catch (e: Throwable) { - if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) - throw e - } - } else { - format.parse(input) - } - - @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) - public fun parse(isoString: String): LocalDateTime = parse(input = isoString) - - internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN) - internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX) - - @Suppress("FunctionName") - public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat = - LocalDateTimeFormat.build(builder) - } - - public actual object Formats { - public actual val ISO: DateTimeFormat = ISO_DATETIME - } - -} diff --git a/core/commonJs/src/LocalTime.kt b/core/commonJs/src/LocalTime.kt deleted file mode 100644 index 674e5cb69..000000000 --- a/core/commonJs/src/LocalTime.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2019-2022 JetBrains s.r.o. and contributors. - * 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 kotlinx.datetime.format.* -import kotlinx.datetime.format.ISO_TIME -import kotlinx.datetime.format.LocalTimeFormat -import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.LocalTimeIso8601Serializer -import kotlinx.serialization.Serializable -import kotlinx.datetime.internal.JSJoda.LocalTime as jtLocalTime - -@Serializable(LocalTimeIso8601Serializer::class) -public actual class LocalTime internal constructor(internal val value: jtLocalTime) : - Comparable { - - public actual constructor(hour: Int, minute: Int, second: Int, nanosecond: Int) : - this( - try { - jsTry { jtLocalTime.of(hour, minute, second, nanosecond) } - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) - throw e - } - ) - - public actual val hour: Int get() = value.hour() - public actual val minute: Int get() = value.minute() - public actual val second: Int get() = value.second() - public actual val nanosecond: Int get() = value.nano().toInt() - public actual fun toSecondOfDay(): Int = value.toSecondOfDay() - public actual fun toMillisecondOfDay(): Int = (value.toNanoOfDay() / NANOS_PER_MILLI).toInt() - public actual fun toNanosecondOfDay(): Long = value.toNanoOfDay().toLong() - - override fun equals(other: Any?): Boolean = - (this === other) || (other is LocalTime && (this.value === other.value || this.value.equals(other.value))) - - override fun hashCode(): Int = value.hashCode() - - actual override fun toString(): String = value.toString() - - actual override fun compareTo(other: LocalTime): Int = this.value.compareTo(other.value) - - public actual companion object { - public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalTime = - if (format === Formats.ISO) { - try { - jsTry { jtLocalTime.parse(input.toString()) }.let(::LocalTime) - } catch (e: Throwable) { - if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) - throw e - } - } else { - format.parse(input) - } - - @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) - public fun parse(isoString: String): LocalTime = parse(input = isoString) - - public actual fun fromSecondOfDay(secondOfDay: Int): LocalTime = try { - jsTry { jtLocalTime.ofSecondOfDay(secondOfDay, 0) }.let(::LocalTime) - } catch (e: Throwable) { - throw IllegalArgumentException(e) - } - - public actual fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime = try { - jsTry { jtLocalTime.ofNanoOfDay(millisecondOfDay * 1_000_000.0) }.let(::LocalTime) - } catch (e: Throwable) { - throw IllegalArgumentException(e) - } - - public actual fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime = try { - // number of nanoseconds in a day is much less than `Number.MAX_SAFE_INTEGER`. - jsTry { jtLocalTime.ofNanoOfDay(nanosecondOfDay.toDouble()) }.let(::LocalTime) - } catch (e: Throwable) { - throw IllegalArgumentException(e) - } - - internal actual val MIN: LocalTime = LocalTime(jtLocalTime.MIN) - internal actual val MAX: LocalTime = LocalTime(jtLocalTime.MAX) - - @Suppress("FunctionName") - public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat = - LocalTimeFormat.build(builder) - } - - public actual object Formats { - public actual val ISO: DateTimeFormat get() = ISO_TIME - } -} diff --git a/core/commonJs/src/Month.kt b/core/commonJs/src/Month.kt deleted file mode 100644 index ae113dac8..000000000 --- a/core/commonJs/src/Month.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 kotlinx.datetime.internal.JSJoda.Month as jsMonth - -public actual enum class Month { - JANUARY, - FEBRUARY, - MARCH, - APRIL, - MAY, - JUNE, - JULY, - AUGUST, - SEPTEMBER, - OCTOBER, - NOVEMBER, - DECEMBER; -} - -internal fun jsMonth.toMonth(): Month = Month(this.value()) \ No newline at end of file diff --git a/core/commonJs/src/PlatformSpecifics.kt b/core/commonJs/src/PlatformSpecifics.kt index 4bdd1c044..7e2f26335 100644 --- a/core/commonJs/src/PlatformSpecifics.kt +++ b/core/commonJs/src/PlatformSpecifics.kt @@ -5,16 +5,17 @@ package kotlinx.datetime.internal -internal expect fun getAvailableZoneIdsSet(): Set +// a pair of ZoneRulesProvider.asDynamic().getTzdbData().zones and ZoneRulesProvider.asDynamic().getTzdbData().links +internal expect fun readTzdb(): Pair, List> public expect interface InteropInterface @OptIn(ExperimentalMultiplatform::class) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.FILE) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) @OptionalExpectation public expect annotation class JsNonModule() @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.FILE) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) public expect annotation class JsModule(val import: String) diff --git a/core/commonJs/src/TimeZone.kt b/core/commonJs/src/TimeZone.kt deleted file mode 100644 index d1fb6e906..000000000 --- a/core/commonJs/src/TimeZone.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 kotlinx.datetime.internal.JSJoda.LocalDateTime as jtLocalDateTime -import kotlinx.datetime.internal.JSJoda.ZoneId as jtZoneId -import kotlinx.datetime.internal.JSJoda.ZoneOffset as jtZoneOffset -import kotlinx.datetime.internal.getAvailableZoneIdsSet -import kotlinx.datetime.serializers.* -import kotlinx.serialization.Serializable - -@Serializable(with = TimeZoneSerializer::class) -public actual open class TimeZone internal constructor(internal val zoneId: jtZoneId) { - public actual val id: String get() = zoneId.id() - - - // experimental member-extensions - public actual fun Instant.toLocalDateTime(): LocalDateTime = toLocalDateTime(this@TimeZone) - public actual fun LocalDateTime.toInstant(): Instant = toInstant(this@TimeZone) - - actual override fun equals(other: Any?): Boolean = - (this === other) || (other is TimeZone && (this.zoneId === other.zoneId || this.zoneId.equals(other.zoneId))) - - override fun hashCode(): Int = zoneId.hashCode() - - actual override fun toString(): String = zoneId.toString() - - public actual companion object { - public actual fun currentSystemDefault(): TimeZone = ofZone(jtZoneId.systemDefault()) - public actual val UTC: FixedOffsetTimeZone = UtcOffset(jtZoneOffset.UTC).asTimeZone() - - public actual fun of(zoneId: String): TimeZone = try { - ofZone(jsTry { jtZoneId.of(zoneId) }) - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalTimeZoneException(e) - throw e - } - - private fun ofZone(zoneId: jtZoneId): TimeZone = when { - zoneId is jtZoneOffset -> - FixedOffsetTimeZone(UtcOffset(zoneId)) - zoneId.rules().isFixedOffset() -> - FixedOffsetTimeZone(UtcOffset(zoneId.normalized() as jtZoneOffset), zoneId) - else -> - TimeZone(zoneId) - } - - public actual val availableZoneIds: Set get() = getAvailableZoneIdsSet() - } -} - -@Serializable(with = FixedOffsetTimeZoneSerializer::class) -public actual class FixedOffsetTimeZone -internal constructor(public actual val offset: UtcOffset, zoneId: jtZoneId): TimeZone(zoneId) { - public actual constructor(offset: UtcOffset) : this(offset, offset.zoneOffset) - - @Deprecated("Use offset.totalSeconds", ReplaceWith("offset.totalSeconds")) - public actual val totalSeconds: Int get() = offset.totalSeconds -} - - -public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = try { - jsTry { jtLocalDateTime.ofInstant(this.value, timeZone.zoneId) }.let(::LocalDateTime) -} catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) - throw e -} - -internal actual fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime = try { - jsTry { jtLocalDateTime.ofInstant(this.value, offset.zoneOffset) }.let(::LocalDateTime) -} catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) - throw e -} - -// throws DateTimeArithmeticException if the number of milliseconds is too large to represent as a safe int in JS -public actual fun TimeZone.offsetAt(instant: Instant): UtcOffset = try { - jsTry { zoneId.rules().offsetOfInstant(instant.value) }.let(::UtcOffset) -} catch (e: Throwable) { - if (e.isJodaArithmeticException()) throw DateTimeArithmeticException(e) - throw e -} - -public actual fun LocalDateTime.toInstant(timeZone: TimeZone): Instant = - this.value.atZone(timeZone.zoneId).toInstant().let(::Instant) - -public actual fun LocalDateTime.toInstant(offset: UtcOffset): Instant = - this.value.toInstant(offset.zoneOffset).let(::Instant) - -public actual fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant = - this.value.atStartOfDay(timeZone.zoneId).toInstant().let(::Instant) diff --git a/core/commonJs/src/UtcOffset.kt b/core/commonJs/src/UtcOffset.kt deleted file mode 100644 index 65dcf8651..000000000 --- a/core/commonJs/src/UtcOffset.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2019-2021 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 kotlinx.datetime.internal.JSJoda.ZoneOffset as jtZoneOffset -import kotlinx.datetime.internal.JSJoda.ChronoField as jtChronoField -import kotlinx.datetime.internal.JSJoda.DateTimeFormatterBuilder as jtDateTimeFormatterBuilder -import kotlinx.datetime.internal.JSJoda.DateTimeFormatter as jtDateTimeFormatter -import kotlinx.datetime.internal.JSJoda.ResolverStyle as jtResolverStyle -import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.UtcOffsetSerializer -import kotlinx.serialization.Serializable - -@Serializable(with = UtcOffsetSerializer::class) -public actual class UtcOffset internal constructor(internal val zoneOffset: jtZoneOffset) { - public actual val totalSeconds: Int get() = zoneOffset.totalSeconds() - - override fun hashCode(): Int = zoneOffset.hashCode() - actual override fun equals(other: Any?): Boolean = - other is UtcOffset && (this.zoneOffset === other.zoneOffset || this.zoneOffset.equals(other.zoneOffset)) - actual override fun toString(): String = zoneOffset.toString() - - public actual companion object { - public actual val ZERO: UtcOffset = UtcOffset(jtZoneOffset.UTC) - - public actual fun parse(input: CharSequence, format: DateTimeFormat): UtcOffset = when { - format === Formats.ISO -> parseWithFormat(input, isoFormat) - format === Formats.ISO_BASIC -> parseWithFormat(input, isoBasicFormat) - format === Formats.FOUR_DIGITS -> parseWithFormat(input, fourDigitsFormat) - else -> format.parse(input) - } - - @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) - public fun parse(offsetString: String): UtcOffset = parse(input = offsetString) - - @Suppress("FunctionName") - public actual fun Format(block: DateTimeFormatBuilder.WithUtcOffset.() -> Unit): DateTimeFormat = - UtcOffsetFormat.build(block) - } - - public actual object Formats { - public actual val ISO: DateTimeFormat get() = ISO_OFFSET - public actual val ISO_BASIC: DateTimeFormat get() = ISO_OFFSET_BASIC - public actual val FOUR_DIGITS: DateTimeFormat get() = FOUR_DIGIT_OFFSET - } -} - -@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") -public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset = - try { - when { - hours != null -> - UtcOffset(jsTry { jtZoneOffset.ofHoursMinutesSeconds(hours, minutes ?: 0, seconds ?: 0) }) - minutes != null -> - UtcOffset(jsTry { jtZoneOffset.ofHoursMinutesSeconds(minutes / 60, minutes % 60, seconds ?: 0) }) - else -> { - UtcOffset(jsTry { jtZoneOffset.ofTotalSeconds(seconds ?: 0) }) - } - } - } catch (e: Throwable) { - if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) else throw e - } - -private val isoFormat by lazy { - jtDateTimeFormatterBuilder().parseCaseInsensitive().appendOffsetId().toFormatter(jtResolverStyle.STRICT) -} -private val isoBasicFormat by lazy { - jtDateTimeFormatterBuilder().parseCaseInsensitive().appendOffset("+HHmmss", "Z").toFormatter(jtResolverStyle.STRICT) -} -private val fourDigitsFormat by lazy { - jtDateTimeFormatterBuilder().parseCaseInsensitive().appendOffset("+HHMM", "+0000").toFormatter(jtResolverStyle.STRICT) -} - -private fun parseWithFormat(input: CharSequence, format: jtDateTimeFormatter) = UtcOffset(seconds = try { - jsTry { format.parse(input.toString()).get(jtChronoField.OFFSET_SECONDS) } -} catch (e: Throwable) { - if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) - if (e.isJodaDateTimeException()) throw DateTimeFormatException(e) - throw e -}) diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt new file mode 100644 index 000000000..3d0aef943 --- /dev/null +++ b/core/commonJs/src/internal/Platform.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * 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.internal + +import kotlinx.datetime.* +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.internal.JSJoda.ZoneId +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +private val tzdb: Result = runCatching { parseTzdb() } + +internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow() + +private fun parseTzdb(): TimeZoneDatabase { + /** + * References: + * - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/MomentZoneRulesProvider.js#L78-L94 + * - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/unpack.js + * - + */ + fun unpackBase60(string: String): Double { + var i = 0 + var parts = string.split('.') + var whole = parts[0] + var multiplier = 1.0 + var out = 0.0 + var sign = 1 + + // handle negative numbers + if (string.startsWith('-')) { + i = 1 + sign = -1 + } + + fun charCodeToInt(char: Char): Int = + when (char) { + in '0'..'9' -> char - '0' + in 'a'..'z' -> char - 'a' + 10 + in 'A'..'Z' -> char - 'A' + 36 + else -> throw IllegalArgumentException("Invalid character: $char") + } + + // handle digits before the decimal + for (ix in i..whole.lastIndex) { + out = 60 * out + charCodeToInt(whole[ix]) + } + + // handle digits after the decimal + parts.getOrNull(1)?.let { fractional -> + for (c in fractional) { + multiplier = multiplier / 60.0 + out += charCodeToInt(c) * multiplier + } + } + + return out * sign + } + + fun List.scanWithoutInitial(initial: R, operation: (acc: R, T) -> R): List = buildList { + var accumulator = initial + for (element in this@scanWithoutInitial) { + accumulator = operation(accumulator, element) + add(accumulator) + } + } + + fun List.partialSums(): List = scanWithoutInitial(0, Long::plus) + + val zones = mutableMapOf() + val (zonesPacked, linksPacked) = readTzdb() + for (zone in zonesPacked) { + val components = zone.split('|') + val offsets = components[2].split(' ').map { unpackBase60(it) } + val indices = components[3].map { it - '0' } + val lengthsOfPeriodsWithOffsets = components[4].split(' ').map { + (unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds + MILLIS_PER_ONE // but we only need seconds + } + zones[components[0]] = TimeZoneRules( + transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take(indices.size - 1), + offsets = indices.map { UtcOffset(null, -offsets[it].roundToInt(), null) }, + recurringZoneRules = null + ) + } + for (link in linksPacked) { + val components = link.split('|') + zones[components[0]]?.let { rules -> + zones[components[1]] = rules + } + } + return object : TimeZoneDatabase { + override fun rulesForId(id: String): TimeZoneRules = + zones[id] ?: throw IllegalTimeZoneException("Unknown time zone: $id") + + override fun availableTimeZoneIds(): Set = zones.keys + } +} + +internal actual fun currentSystemDefaultZone(): Pair = + ZoneId.systemDefault().id() to null + +internal actual fun currentTime(): Instant = Instant.fromEpochMilliseconds(Date().getTime().toLong()) + +internal external class Date() { + fun getTime(): Double +} diff --git a/core/commonJs/src/internal/mathJs.kt b/core/commonJs/src/internal/mathJs.kt deleted file mode 100644 index 91e09a1c8..000000000 --- a/core/commonJs/src/internal/mathJs.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019-2022 JetBrains s.r.o. and contributors. - * 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.internal - -/** - * Safely adds two long values. - * throws [ArithmeticException] if the result overflows a long - */ -internal actual fun safeAdd(a: Long, b: Long): Long { - val sum = a + b - // check for a change of sign in the result when the inputs have the same sign - if ((a xor sum) < 0 && (a xor b) >= 0) { - throw ArithmeticException("Addition overflows a long: $a + $b") - } - return sum -} - -internal actual fun safeAdd(a: Int, b: Int): Int { - val sum = a + b - // check for a change of sign in the result when the inputs have the same sign - if ((a xor sum) < 0 && (a xor b) >= 0) { - throw ArithmeticException("Addition overflows Int range: $a + $b") - } - return sum -} - -/** - * Safely multiply a long by an int. - * - * @param a the first value - * @param b the second value - * @return the new total - * @throws ArithmeticException if the result overflows a long - */ -internal actual fun safeMultiply(a: Long, b: Long): Long { - when (b) { - -1L -> { - if (a == Long.MIN_VALUE) { - throw ArithmeticException("Multiplication overflows a long: $a * $b") - } - return -a - } - 0L -> return 0L - 1L -> return a - } - val total = a * b - if (total / b != a) { - throw ArithmeticException("Multiplication overflows a long: $a * $b") - } - return total -} - -internal actual fun safeMultiply(a: Int, b: Int): Int { - val result = a.toLong() * b - if (result > Int.MAX_VALUE || result < Int.MIN_VALUE) throw ArithmeticException("Multiplication overflows Int range: $a * $b.") - return result.toInt() -} \ No newline at end of file diff --git a/core/js/src/PlatformSpecifics.kt b/core/js/src/PlatformSpecifics.kt index bfa166f1f..65236319b 100644 --- a/core/js/src/PlatformSpecifics.kt +++ b/core/js/src/PlatformSpecifics.kt @@ -4,13 +4,15 @@ */ package kotlinx.datetime.internal -import kotlinx.datetime.internal.JSJoda.ZoneId +import kotlinx.datetime.internal.JSJoda.ZoneRulesProvider -internal actual fun getAvailableZoneIdsSet(): Set = - ZoneId.getAvailableZoneIds().unsafeCast>().toSet() +internal actual fun readTzdb(): Pair, List> { + val tzdbData = ZoneRulesProvider.asDynamic().getTzdbData() + return tzdbData.zones.unsafeCast>().toList() to tzdbData.links.unsafeCast>().toList() +} public actual external interface InteropInterface public actual typealias JsNonModule = kotlin.js.JsNonModule -public actual typealias JsModule = kotlin.js.JsModule \ No newline at end of file +public actual typealias JsModule = kotlin.js.JsModule diff --git a/core/wasmJs/src/PlatformSpecifics.kt b/core/wasmJs/src/PlatformSpecifics.kt index 95e2fa35c..0b2e0a84b 100644 --- a/core/wasmJs/src/PlatformSpecifics.kt +++ b/core/wasmJs/src/PlatformSpecifics.kt @@ -5,14 +5,24 @@ package kotlinx.datetime.internal import kotlinx.datetime.internal.JSJoda.ZoneId +import kotlinx.datetime.internal.JSJoda.ZoneRulesProvider +import kotlin.js.unsafeCast -internal actual fun getAvailableZoneIdsSet(): Set = buildSet { - val ids = ZoneId.getAvailableZoneIds().unsafeCast>() - for (i in 0 until ids.length) { - add(ids[i].toString()) +private fun getZones(rulesProvider: JsAny): JsAny = js("rulesProvider.getTzdbData().zones") +private fun getLinks(rulesProvider: JsAny): JsAny = js("rulesProvider.getTzdbData().links") + +internal actual fun readTzdb(): Pair, List> { + val zones = getZones(ZoneRulesProvider as JsAny) + val links = getLinks(ZoneRulesProvider as JsAny) + return zones.unsafeCast>().toList() to links.unsafeCast>().toList() +} + +private fun JsArray.toList(): List = buildList { + for (i in 0 until toList@length) { + add(this@toList[i].toString()) } } public actual typealias InteropInterface = JsAny -public actual typealias JsModule = kotlin.js.JsModule \ No newline at end of file +public actual typealias JsModule = kotlin.js.JsModule From 12f7ca8d9420d13419039a8b331ef50d20ade819 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 4 Nov 2024 12:41:53 +0100 Subject: [PATCH 03/14] Fixes --- core/common/src/LocalDate.kt | 4 +--- core/commonJs/src/internal/Platform.kt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 700a291a5..02ded3244 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -350,8 +350,6 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = * - Negative or zero if this date is later than the other. * - Exactly zero if this date is equal to the other. * - * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int. - * * @see LocalDate.minus for the same operation with the order of arguments reversed. * @sample kotlinx.datetime.test.samples.LocalDateSamples.periodUntil */ @@ -473,7 +471,7 @@ public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ -public fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value.toLong(), unit) +public fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-(value.toLong()), unit) /** * Returns a [LocalDate] that results from adding the [value] number of the specified [unit] to this date. diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 3d0aef943..53a5abd96 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -101,7 +101,7 @@ private fun parseTzdb(): TimeZoneDatabase { } internal actual fun currentSystemDefaultZone(): Pair = - ZoneId.systemDefault().id() to null + ZoneId.systemDefault().id() to null // TODO: make this function with SYSTEM internal actual fun currentTime(): Instant = Instant.fromEpochMilliseconds(Date().getTime().toLong()) From ad2ed3cd0a0b4279c72c820fd1f8cbe002ea07a6 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 4 Nov 2024 14:02:27 +0100 Subject: [PATCH 04/14] Remove the no-longer-needed JS Joda entries --- core/commonJs/src/JSJodaExceptions.kt | 14 - core/commonJs/src/PlatformSpecifics.kt | 2 +- core/commonJs/src/internal/Platform.kt | 17 +- ...al.JSJoda.module_@js-joda_core._aliases.kt | 8 - ...me.internal.JSJoda.module_@js-joda_core.kt | 1200 +---------------- core/js/src/JSJodaExceptions.kt | 11 - core/js/src/PlatformSpecifics.kt | 6 +- ...me.internal.JSJoda.module_@js-joda_core.kt | 32 - core/wasmJs/src/JSJodaExceptions.kt | 12 +- core/wasmJs/src/PlatformSpecifics.kt | 12 +- 10 files changed, 33 insertions(+), 1281 deletions(-) delete mode 100644 core/commonJs/src/JSJodaExceptions.kt delete mode 100644 core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core._aliases.kt delete mode 100644 core/js/src/JSJodaExceptions.kt delete mode 100644 core/js/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt diff --git a/core/commonJs/src/JSJodaExceptions.kt b/core/commonJs/src/JSJodaExceptions.kt deleted file mode 100644 index 0b854d1ad..000000000 --- a/core/commonJs/src/JSJodaExceptions.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2019-2023 JetBrains s.r.o. and contributors. - * 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 - -internal fun Throwable.isJodaArithmeticException(): Boolean = hasJsExceptionName("ArithmeticException") -internal fun Throwable.isJodaDateTimeException(): Boolean = hasJsExceptionName("DateTimeException") -internal fun Throwable.isJodaDateTimeParseException(): Boolean = hasJsExceptionName("DateTimeParseException") - -internal expect fun Throwable.hasJsExceptionName(name: String): Boolean - -internal expect inline fun jsTry(crossinline body: () -> T): T \ No newline at end of file diff --git a/core/commonJs/src/PlatformSpecifics.kt b/core/commonJs/src/PlatformSpecifics.kt index 7e2f26335..2813677da 100644 --- a/core/commonJs/src/PlatformSpecifics.kt +++ b/core/commonJs/src/PlatformSpecifics.kt @@ -6,7 +6,7 @@ package kotlinx.datetime.internal // a pair of ZoneRulesProvider.asDynamic().getTzdbData().zones and ZoneRulesProvider.asDynamic().getTzdbData().links -internal expect fun readTzdb(): Pair, List> +internal expect fun readTzdb(): Pair, List>? public expect interface InteropInterface diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 53a5abd96..2e2ffdc4c 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -71,7 +71,7 @@ private fun parseTzdb(): TimeZoneDatabase { fun List.partialSums(): List = scanWithoutInitial(0, Long::plus) val zones = mutableMapOf() - val (zonesPacked, linksPacked) = readTzdb() + val (zonesPacked, linksPacked) = readTzdb() ?: return EmptyTimeZoneDatabase for (zone in zonesPacked) { val components = zone.split('|') val offsets = components[2].split(' ').map { unpackBase60(it) } @@ -100,8 +100,21 @@ private fun parseTzdb(): TimeZoneDatabase { } } +private object EmptyTimeZoneDatabase : TimeZoneDatabase { + override fun rulesForId(id: String): TimeZoneRules = when (id) { + "SYSTEM" -> TimeZoneRules( + transitionEpochSeconds = emptyList(), + offsets = listOf(UtcOffset.ZERO), + recurringZoneRules = null + ) // TODO: that's not correct, we need to use `Date()`'s offset + else -> throw IllegalTimeZoneException("JSJoda timezone database is not available") + } + + override fun availableTimeZoneIds(): Set = emptySet() +} + internal actual fun currentSystemDefaultZone(): Pair = - ZoneId.systemDefault().id() to null // TODO: make this function with SYSTEM + ZoneId.systemDefault().id() to null internal actual fun currentTime(): Instant = Instant.fromEpochMilliseconds(Date().getTime().toLong()) diff --git a/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core._aliases.kt b/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core._aliases.kt deleted file mode 100644 index 5fadaa6a1..000000000 --- a/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core._aliases.kt +++ /dev/null @@ -1,8 +0,0 @@ -@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE", "INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION") -package kotlinx.datetime.internal.JSJoda - -typealias Chronology = IsoChronology - -typealias DateTimeParseException = Error - -typealias DateTimeException = Error \ No newline at end of file diff --git a/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt b/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt index a780b1e7e..e999aaeb7 100644 --- a/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt +++ b/core/commonJs/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt @@ -1,1209 +1,15 @@ @file:kotlinx.datetime.internal.JsModule("@js-joda/core") @file:kotlinx.datetime.internal.JsNonModule -@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE", "INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") package kotlinx.datetime.internal.JSJoda import kotlinx.datetime.internal.InteropInterface -import kotlin.js.* -external open class TemporalField : InteropInterface { - open fun isSupportedBy(temporal: TemporalAccessor): Boolean - open fun isDateBased(): Boolean - open fun isTimeBased(): Boolean - open fun baseUnit(): TemporalUnit - open fun rangeUnit(): TemporalUnit - open fun range(): ValueRange - open fun rangeRefinedBy(temporal: TemporalAccessor): ValueRange - open fun getFrom(temporal: TemporalAccessor): Int - open fun adjustInto(temporal: R, newValue: Int): R - open fun name(): String - open fun displayName(): String - open fun equals(other: InteropInterface): Boolean -} - -external open class TemporalUnit : InteropInterface { - open fun addTo(temporal: T, amount: Int): T - open fun between(temporal1: Temporal, temporal2: Temporal): Int - open fun duration(): Duration - open fun isDateBased(): Boolean - open fun isDurationEstimated(): Boolean - open fun isSupportedBy(temporal: Temporal): Boolean - open fun isTimeBased(): Boolean -} - -external open class ValueRange : InteropInterface { - open fun checkValidValue(value: Int, field: TemporalField): Int - open fun checkValidIntValue(value: Int, field: TemporalField): Int - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): Int - open fun isFixed(): Boolean - open fun isIntValue(): Boolean - open fun isValidIntValue(value: Int): Boolean - open fun isValidValue(value: Int): Boolean - open fun largestMinimum(): Int - open fun maximum(): Int - open fun minimum(): Int - open fun smallestMaximum(): Int - override fun toString(): String - - companion object { - fun of(min: Int, max: Int): ValueRange - fun of(min: Int, maxSmallest: Int, maxLargest: Int): ValueRange - fun of(minSmallest: Int, minLargest: Int, maxSmallest: Int, maxLargest: Int): ValueRange - } -} - -external open class TemporalAmount : InteropInterface { - open fun addTo(temporal: T): T - open fun get(unit: TemporalUnit): Int - open fun subtractFrom(temporal: T): T -} - -external open class TemporalAccessor : InteropInterface { - open fun get(field: TemporalField): Int - open fun query(query: TemporalQuery): R? - open fun range(field: TemporalField): ValueRange - open fun isSupported(field: TemporalField): Boolean -} - -external open class Temporal : TemporalAccessor { - override fun isSupported(field: TemporalField): Boolean - open fun isSupported(unit: TemporalUnit): Boolean - open fun minus(amountToSubtract: Int, unit: TemporalUnit): Temporal - open fun minus(amount: TemporalAmount): Temporal - open fun plus(amountToAdd: Int, unit: TemporalUnit): Temporal - open fun plus(amount: TemporalAmount): Temporal - open fun until(endTemporal: Temporal, unit: TemporalUnit): Double - open fun with(adjuster: TemporalAdjuster): Temporal - open fun with(field: TemporalField, newValue: Int): Temporal -} - -external open class TemporalAdjuster : InteropInterface { - open fun adjustInto(temporal: Temporal): Temporal -} - -external open class TemporalQuery : InteropInterface { - open fun queryFrom(temporal: TemporalAccessor): R -} - -external open class ChronoField : TemporalField { - override fun isSupportedBy(temporal: TemporalAccessor): Boolean - override fun baseUnit(): TemporalUnit - open fun checkValidValue(value: Int): Int - open fun checkValidIntValue(value: Int): Int - override fun displayName(): String - override fun equals(other: InteropInterface): Boolean - override fun getFrom(temporal: TemporalAccessor): Int - override fun isDateBased(): Boolean - override fun isTimeBased(): Boolean - override fun name(): String - override fun range(): ValueRange - override fun rangeRefinedBy(temporal: TemporalAccessor): ValueRange - override fun rangeUnit(): TemporalUnit - override fun adjustInto(temporal: R, newValue: Int): R - override fun toString(): String - - companion object { - var ALIGNED_DAY_OF_WEEK_IN_MONTH: ChronoField - var ALIGNED_DAY_OF_WEEK_IN_YEAR: ChronoField - var ALIGNED_WEEK_OF_MONTH: ChronoField - var ALIGNED_WEEK_OF_YEAR: ChronoField - var AMPM_OF_DAY: ChronoField - var CLOCK_HOUR_OF_AMPM: ChronoField - var CLOCK_HOUR_OF_DAY: ChronoField - var DAY_OF_MONTH: ChronoField - var DAY_OF_WEEK: ChronoField - var DAY_OF_YEAR: ChronoField - var EPOCH_DAY: ChronoField - var ERA: ChronoField - var HOUR_OF_AMPM: ChronoField - var HOUR_OF_DAY: ChronoField - var INSTANT_SECONDS: ChronoField - var MICRO_OF_DAY: ChronoField - var MICRO_OF_SECOND: ChronoField - var MILLI_OF_DAY: ChronoField - var MILLI_OF_SECOND: ChronoField - var MINUTE_OF_DAY: ChronoField - var MINUTE_OF_HOUR: ChronoField - var MONTH_OF_YEAR: ChronoField - var NANO_OF_DAY: ChronoField - var NANO_OF_SECOND: ChronoField - var OFFSET_SECONDS: ChronoField - var PROLEPTIC_MONTH: ChronoField - var SECOND_OF_DAY: ChronoField - var SECOND_OF_MINUTE: ChronoField - var YEAR: ChronoField - var YEAR_OF_ERA: ChronoField - } -} - -external open class ChronoUnit : TemporalUnit { - override fun addTo(temporal: T, amount: Int): T - override fun between(temporal1: Temporal, temporal2: Temporal): Int - open fun compareTo(other: TemporalUnit): Int - override fun duration(): Duration - override fun isDateBased(): Boolean - override fun isDurationEstimated(): Boolean - override fun isSupportedBy(temporal: Temporal): Boolean - override fun isTimeBased(): Boolean - override fun toString(): String - - companion object { - var NANOS: ChronoUnit - var MICROS: ChronoUnit - var MILLIS: ChronoUnit - var SECONDS: ChronoUnit - var MINUTES: ChronoUnit - var HOURS: ChronoUnit - var HALF_DAYS: ChronoUnit - var DAYS: ChronoUnit - var WEEKS: ChronoUnit - var MONTHS: ChronoUnit - var YEARS: ChronoUnit - var DECADES: ChronoUnit - var CENTURIES: ChronoUnit - var MILLENNIA: ChronoUnit - var ERAS: ChronoUnit - var FOREVER: ChronoUnit - } -} - -external open class Clock : InteropInterface { - open fun equals(other: InteropInterface): Boolean - open fun instant(): Instant - open fun millis(): Double - open fun withZone(zone: ZoneId): Clock - open fun zone(): ZoneId - - companion object { - fun fixed(fixedInstant: Instant, zoneId: ZoneId): Clock - fun offset(baseClock: Clock, offsetDuration: Duration): Clock - fun system(zone: ZoneId): Clock - fun systemDefaultZone(): Clock - fun systemUTC(): Clock - } -} - -external open class Duration : TemporalAmount { - open fun abs(): Duration - override fun addTo(temporal: T): T - open fun compareTo(otherDuration: Duration): Int - open fun dividedBy(divisor: Int): Duration - open fun equals(other: InteropInterface): Boolean - override fun get(unit: TemporalUnit): Int - open fun isNegative(): Boolean - open fun isZero(): Boolean - open fun minus(amount: Int, unit: TemporalUnit): Duration - open fun minus(duration: Duration): Duration - open fun minusDays(daysToSubtract: Int): Duration - open fun minusHours(hoursToSubtract: Int): Duration - open fun minusMillis(millisToSubtract: Double): Duration - open fun minusMinutes(minutesToSubtract: Int): Duration - open fun minusNanos(nanosToSubtract: Double): Duration - open fun minusSeconds(secondsToSubtract: Int): Duration - open fun multipliedBy(multiplicand: Int): Duration - open fun nano(): Double - open fun negated(): Duration - open fun plus(amount: Int, unit: TemporalUnit): Duration - open fun plus(duration: Duration): Duration - open fun plusDays(daysToAdd: Int): Duration - open fun plusHours(hoursToAdd: Int): Duration - open fun plusMillis(millisToAdd: Double): Duration - open fun plusMinutes(minutesToAdd: Int): Duration - open fun plusNanos(nanosToAdd: Double): Duration - open fun plusSeconds(secondsToAdd: Int): Duration - open fun plusSecondsNanos(secondsToAdd: Int, nanosToAdd: Double): Duration - open fun seconds(): Double - override fun subtractFrom(temporal: T): T - open fun toDays(): Int - open fun toHours(): Int - open fun toJSON(): String - open fun toMillis(): Double - open fun toMinutes(): Int - open fun toNanos(): Double - override fun toString(): String - open fun withNanos(nanoOfSecond: Double): Duration - open fun withSeconds(seconds: Int): Duration - - companion object { - var ZERO: Duration - fun between(startInclusive: Temporal, endExclusive: Temporal): Duration - fun from(amount: TemporalAmount): Duration - fun of(amount: Int, unit: TemporalUnit): Duration - fun ofDays(days: Int): Duration - fun ofHours(hours: Int): Duration - fun ofMillis(millis: Double): Duration - fun ofMinutes(minutes: Int): Duration - fun ofNanos(nanos: Double): Duration - fun ofSeconds(seconds: Int, nanoAdjustment: Int): Duration - fun parse(text: String): Duration - } -} - -external open class Instant : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atZone(zone: ZoneId): ZonedDateTime - open fun compareTo(otherInstant: Instant): Int - open fun epochSecond(): Double - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): Int - open fun isAfter(otherInstant: Instant): Boolean - open fun isBefore(otherInstant: Instant): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amount: TemporalAmount): Instant - override fun minus(amountToSubtract: Int, unit: TemporalUnit): Instant - open fun minusMillis(millisToSubtract: Double): Instant - open fun minusNanos(nanosToSubtract: Double): Instant - open fun minusSeconds(secondsToSubtract: Int): Instant - open fun nano(): Double - override fun plus(amount: TemporalAmount): Instant - override fun plus(amountToAdd: Int, unit: TemporalUnit): Instant - open fun plusMillis(millisToAdd: Double): Instant - open fun plusNanos(nanosToAdd: Double): Instant - open fun plusSeconds(secondsToAdd: Int): Instant - open fun toEpochMilli(): Double - open fun toJSON(): String - override fun toString(): String - open fun truncatedTo(unit: TemporalUnit): Instant - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): Instant - override fun with(field: TemporalField, newValue: Int): Instant - - companion object { - var EPOCH: Instant - var MIN: Instant - var MAX: Instant - var MIN_SECONDS: Instant - var MAX_SECONDS: Instant - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): Instant - fun now(clock: Clock): Instant - fun ofEpochMilli(epochMilli: Double): Instant - fun ofEpochSecond(epochSecond: Double, nanoAdjustment: Int): Instant - fun parse(text: String): Instant - } -} - -external open class LocalDate : ChronoLocalDate { - open fun atStartOfDay(): LocalDateTime - open fun atStartOfDay(zone: ZoneId): ZonedDateTime - open fun atTime(hour: Int, minute: Int, second: Int, nanoOfSecond: Double): LocalDateTime - open fun atTime(hour: Int, minute: Int): LocalDateTime - open fun atTime(hour: Int, minute: Int, second: Int): LocalDateTime - open fun atTime(time: LocalTime): LocalDateTime - open fun chronology(): Chronology - open fun compareTo(other: LocalDate): Int - open fun dayOfMonth(): Int - open fun dayOfWeek(): DayOfWeek - open fun dayOfYear(): Int - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): Int - open fun isAfter(other: LocalDate): Boolean - open fun isBefore(other: LocalDate): Boolean - open fun isEqual(other: LocalDate): Boolean - open fun isLeapYear(): Boolean - open fun isoWeekOfWeekyear(): Int - open fun isoWeekyear(): Int - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - open fun lengthOfMonth(): Int - open fun lengthOfYear(): Int - override fun minus(amount: TemporalAmount): LocalDate - override fun minus(amountToSubtract: Int, unit: TemporalUnit): LocalDate - open fun minusDays(daysToSubtract: Int): LocalDate - open fun minusMonths(monthsToSubtract: Int): LocalDate - open fun minusWeeks(weeksToSubtract: Int): LocalDate - open fun minusYears(yearsToSubtract: Int): LocalDate - open fun month(): Month - open fun monthValue(): Int - override fun plus(amount: TemporalAmount): LocalDate - override fun plus(amountToAdd: Int, unit: TemporalUnit): LocalDate - open fun plusDays(daysToAdd: Int): LocalDate - open fun plusDays(daysToAdd: Double): LocalDate - open fun plusMonths(monthsToAdd: Int): LocalDate - open fun plusMonths(monthsToAdd: Double): LocalDate - open fun plusWeeks(weeksToAdd: Int): LocalDate - open fun plusYears(yearsToAdd: Int): LocalDate - open fun toEpochDay(): Double - open fun toJSON(): String - override fun toString(): String - open fun until(endDate: TemporalAccessor): Period - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): LocalDate - override fun with(field: TemporalField, newValue: Int): LocalDate - open fun withDayOfMonth(dayOfMonth: Int): LocalDate - open fun withDayOfYear(dayOfYear: Int): LocalDate - open fun withMonth(month: Month): LocalDate - open fun withMonth(month: Int): LocalDate - open fun withYear(year: Int): LocalDate - open fun year(): Int - - companion object { - var MIN: LocalDate - var MAX: LocalDate - var EPOCH_0: LocalDate - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): LocalDate - fun now(clockOrZone: Clock): LocalDate - fun now(clockOrZone: ZoneId): LocalDate - fun of(year: Int, month: Month, dayOfMonth: Int): LocalDate - fun of(year: Int, month: Int, dayOfMonth: Int): LocalDate - fun ofEpochDay(epochDay: Int): LocalDate - fun ofInstant(instant: Instant, zoneId: ZoneId): LocalDate - fun ofYearDay(year: Int, dayOfYear: Int): LocalDate - fun parse(text: String, formatter: DateTimeFormatter): LocalDate - fun parse(text: String): LocalDate - } -} - -external open class LocalDateTime : ChronoLocalDateTime { - open fun atOffset(offset: ZoneOffset): OffsetDateTime - open fun atZone(zone: ZoneId): ZonedDateTime - open fun compareTo(other: LocalDateTime): Int - open fun dayOfMonth(): Int - open fun dayOfWeek(): DayOfWeek - open fun dayOfYear(): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - override fun hashCode(): Int - open fun hour(): Int - open fun isAfter(other: LocalDateTime): Boolean - open fun isBefore(other: LocalDateTime): Boolean - open fun isEqual(other: LocalDateTime): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amount: TemporalAmount): LocalDateTime - override fun minus(amountToSubtract: Int, unit: TemporalUnit): LocalDateTime - open fun minusDays(days: Int): LocalDateTime - open fun minusHours(hours: Int): LocalDateTime - open fun minusMinutes(minutes: Int): LocalDateTime - open fun minusMonths(months: Int): LocalDateTime - open fun minusNanos(nanos: Double): LocalDateTime - open fun minusSeconds(seconds: Int): LocalDateTime - open fun minusWeeks(weeks: Int): LocalDateTime - open fun minusYears(years: Int): LocalDateTime - open fun minute(): Int - open fun month(): Month - open fun monthValue(): Int - open fun nano(): Double - override fun plus(amount: TemporalAmount): LocalDateTime - override fun plus(amountToAdd: Int, unit: TemporalUnit): LocalDateTime - open fun plusDays(days: Int): LocalDateTime - open fun plusHours(hours: Int): LocalDateTime - open fun plusMinutes(minutes: Int): LocalDateTime - open fun plusMonths(months: Int): LocalDateTime - open fun plusNanos(nanos: Double): LocalDateTime - open fun plusSeconds(seconds: Int): LocalDateTime - open fun plusWeeks(weeks: Int): LocalDateTime - open fun plusYears(years: Int): LocalDateTime - open fun second(): Int - open fun toJSON(): String - open fun toLocalDate(): LocalDate - open fun toLocalTime(): LocalTime - override fun toString(): String - open fun truncatedTo(unit: TemporalUnit): LocalDateTime - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): LocalDateTime - override fun with(field: TemporalField, newValue: Int): LocalDateTime - open fun withDayOfMonth(dayOfMonth: Int): LocalDateTime - open fun withDayOfYear(dayOfYear: Int): LocalDateTime - open fun withHour(hour: Int): LocalDateTime - open fun withMinute(minute: Int): LocalDateTime - open fun withMonth(month: Int): LocalDateTime - open fun withMonth(month: Month): LocalDateTime - open fun withNano(nanoOfSecond: Int): LocalDateTime - open fun withSecond(second: Int): LocalDateTime - open fun withYear(year: Int): LocalDateTime - open fun year(): Int - - companion object { - var MIN: LocalDateTime - var MAX: LocalDateTime - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): LocalDateTime - fun now(clockOrZone: Clock): LocalDateTime - fun now(clockOrZone: ZoneId): LocalDateTime - fun of(date: LocalDate, time: LocalTime): LocalDateTime - fun of(year: Int, month: Month, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanoSecond: Int): LocalDateTime - fun of(year: Int, month: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanoSecond: Int): LocalDateTime - fun ofEpochSecond(epochSecond: Double, nanoOfSecond: Int, offset: ZoneOffset): LocalDateTime - fun ofEpochSecond(epochSecond: Double, offset: ZoneOffset): LocalDateTime - fun ofInstant(instant: Instant, zoneId: ZoneId): LocalDateTime - fun parse(text: String): LocalDateTime - } -} - -external open class LocalTime : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atDate(date: LocalDate): LocalDateTime - open fun compareTo(other: LocalTime): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - override fun hashCode(): Int - open fun hour(): Int - open fun isAfter(other: LocalTime): Boolean - open fun isBefore(other: LocalTime): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amount: TemporalAmount): LocalTime - override fun minus(amountToSubtract: Int, unit: TemporalUnit): LocalTime - open fun minusHours(hoursToSubtract: Int): LocalTime - open fun minusMinutes(minutesToSubtract: Int): LocalTime - open fun minusNanos(nanosToSubtract: Double): LocalTime - open fun minusSeconds(secondsToSubtract: Int): LocalTime - open fun minute(): Int - open fun nano(): Double - override fun plus(amount: TemporalAmount): LocalTime - override fun plus(amountToAdd: Int, unit: TemporalUnit): LocalTime - open fun plusHours(hoursToAdd: Int): LocalTime - open fun plusMinutes(minutesToAdd: Int): LocalTime - open fun plusNanos(nanosToAdd: Double): LocalTime - open fun plusSeconds(secondstoAdd: Int): LocalTime - open fun second(): Int - open fun toJSON(): String - open fun toNanoOfDay(): Double - open fun toSecondOfDay(): Int - override fun toString(): String - open fun truncatedTo(unit: ChronoUnit): LocalTime - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): LocalTime - override fun with(field: TemporalField, newValue: Int): LocalTime - open fun withHour(hour: Int): LocalTime - open fun withMinute(minute: Int): LocalTime - open fun withNano(nanoOfSecond: Int): LocalTime - open fun withSecond(second: Int): LocalTime - - companion object { - var MIN: LocalTime - var MAX: LocalTime - var MIDNIGHT: LocalTime - var NOON: LocalTime - var HOURS_PER_DAY: Int - var MINUTES_PER_HOUR: Int - var MINUTES_PER_DAY: Int - var SECONDS_PER_MINUTE: Int - var SECONDS_PER_HOUR: Int - var SECONDS_PER_DAY: Int - var MILLIS_PER_DAY: Double - var MICROS_PER_DAY: Double - var NANOS_PER_SECOND: Double - var NANOS_PER_MINUTE: Double - var NANOS_PER_HOUR: Double - var NANOS_PER_DAY: Double - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): LocalTime - fun now(clockOrZone: Clock): LocalTime - fun now(clockOrZone: ZoneId): LocalTime - fun of(hour: Int, minute: Int, second: Int, nanoOfSecond: Int): LocalTime - fun ofInstant(instant: Instant, zone: ZoneId): LocalTime - fun ofNanoOfDay(nanoOfDay: Double): LocalTime - fun ofSecondOfDay(secondOfDay: Int, nanoOfSecond: Int): LocalTime - fun parse(text: String): LocalTime - } -} - -external open class MonthDay : TemporalAccessor { - open fun adjustInto(temporal: Temporal): Temporal - open fun atYear(year: Int): LocalDate - open fun compareTo(other: MonthDay): Int - open fun dayOfMonth(): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - open fun isAfter(other: MonthDay): Boolean - open fun isBefore(other: MonthDay): Boolean - override fun isSupported(field: TemporalField): Boolean - open fun isValidYear(year: Int): Boolean - open fun month(): Month - open fun monthValue(): Int - open fun toJSON(): String - override fun toString(): String - open fun with(month: Month): MonthDay - open fun withDayOfMonth(dayOfMonth: Int): MonthDay - open fun withMonth(month: Int): MonthDay - - companion object { - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): MonthDay - fun now(zoneIdOrClock: ZoneId): MonthDay - fun now(zoneIdOrClock: Clock): MonthDay - fun of(month: Month, dayOfMonth: Int): MonthDay - fun of(month: Int, dayOfMonth: Int): MonthDay - fun parse(text: String, formatter: DateTimeFormatter): MonthDay - } -} - -external open class Period : TemporalAmount { - override fun addTo(temporal: T): T - open fun chronology(): IsoChronology - open fun days(): Int - open fun equals(other: InteropInterface): Boolean - override fun get(unit: TemporalUnit): Int - override fun hashCode(): Int - open fun isNegative(): Boolean - open fun isZero(): Boolean - open fun minus(amountToSubtract: TemporalAmount): Period - open fun minusDays(daysToSubtract: Int): Period - open fun minusMonths(monthsToSubtract: Int): Period - open fun minusYears(yearsToSubtract: Int): Period - open fun months(): Int - open fun multipliedBy(scalar: Int): Period - open fun negated(): Period - open fun normalized(): Period - open fun plus(amountToAdd: TemporalAmount): Period - open fun plusDays(daysToAdd: Int): Period - open fun plusMonths(monthsToAdd: Int): Period - open fun plusYears(yearsToAdd: Int): Period - override fun subtractFrom(temporal: T): T - open fun toJSON(): String - override fun toString(): String - open fun toTotalMonths(): Int - open fun withDays(days: Int): Period - open fun withMonths(months: Int): Period - open fun withYears(years: Int): Period - open fun years(): Int - - companion object { - var ZERO: Period - fun between(startDate: LocalDate, endDate: LocalDate): Period - fun from(amount: TemporalAmount): Period - fun of(years: Int, months: Int, days: Int): Period - fun ofDays(days: Int): Period - fun ofMonths(months: Int): Period - fun ofWeeks(weeks: Int): Period - fun ofYears(years: Int): Period - fun parse(text: String): Period - } -} - -external open class Year : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atDay(dayOfYear: Int): LocalDate - open fun atMonth(month: Month): YearMonth - open fun atMonth(month: Int): YearMonth - open fun atMonthDay(monthDay: MonthDay): LocalDate - open fun compareTo(other: Year): Int - open fun equals(other: InteropInterface): Boolean - open fun isAfter(other: Year): Boolean - open fun isBefore(other: Year): Boolean - open fun isLeap(): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - open fun isValidMonthDay(monthDay: MonthDay): Boolean - open fun length(): Int - override fun minus(amount: TemporalAmount): Year - override fun minus(amountToSubtract: Int, unit: TemporalUnit): Year - open fun minusYears(yearsToSubtract: Int): Year - override fun plus(amount: TemporalAmount): Year - override fun plus(amountToAdd: Int, unit: TemporalUnit): Year - open fun plusYears(yearsToAdd: Int): Year - open fun toJSON(): String - override fun toString(): String - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - open fun value(): Int - override fun with(adjuster: TemporalAdjuster): Year - override fun with(field: TemporalField, newValue: Int): Year - - companion object { - var MIN_VALUE: Int - var MAX_VALUE: Int - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): Year - fun isLeap(year: Int): Boolean - fun now(zoneIdOrClock: ZoneId): Year - fun now(zoneIdOrClock: Clock): Year - fun of(isoYear: Int): Year - fun parse(text: String, formatter: DateTimeFormatter): Year - } -} - -external open class YearMonth : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atDay(dayOfMonth: Int): LocalDate - open fun atEndOfMonth(): LocalDate - open fun compareTo(other: YearMonth): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - open fun isAfter(other: YearMonth): Boolean - open fun isBefore(other: YearMonth): Boolean - open fun isLeapYear(): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - open fun isValidDay(): Boolean - open fun lengthOfMonth(): Int - open fun lengthOfYear(): Int - override fun minus(amount: TemporalAmount): YearMonth - override fun minus(amountToSubtract: Int, unit: TemporalUnit): YearMonth - open fun minusMonths(monthsToSubtract: Int): YearMonth - open fun minusYears(yearsToSubtract: Int): YearMonth - open fun month(): Month - open fun monthValue(): Int - override fun plus(amount: TemporalAmount): YearMonth - override fun plus(amountToAdd: Int, unit: TemporalUnit): YearMonth - open fun plusMonths(monthsToAdd: Int): YearMonth - open fun plusYears(yearsToAdd: Int): YearMonth - open fun toJSON(): String - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): YearMonth - override fun with(field: TemporalField, newValue: Int): YearMonth - open fun withMonth(month: Int): YearMonth - open fun withYear(year: Int): YearMonth - open fun year(): Int - - companion object { - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): YearMonth - fun now(zoneIdOrClock: ZoneId): YearMonth - fun now(zoneIdOrClock: Clock): YearMonth - fun of(year: Int, monthOrInt: Month): YearMonth - fun of(year: Int, monthOrInt: Int): YearMonth - fun parse(text: String, formatter: DateTimeFormatter): YearMonth - } -} - -external open class OffsetDateTime : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atZoneSameInstant(zone: ZoneId): ZonedDateTime - open fun atZoneSimilarLocal(zone: ZoneId): ZonedDateTime - open fun compareTo(other: OffsetDateTime): Int - open fun equals(obj: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - override fun get(field: TemporalField): Int - open fun dayOfMonth(): Int - open fun dayOfWeek(): DayOfWeek - open fun dayOfYear(): Int - open fun hour(): Int - open fun minute(): Int - open fun month(): Month - open fun monthValue(): Int - open fun nano(): Double - open fun offset(): ZoneOffset - open fun second(): Int - open fun year(): Int - override fun hashCode(): Int - open fun isAfter(other: OffsetDateTime): Boolean - open fun isBefore(other: OffsetDateTime): Boolean - open fun isEqual(other: OffsetDateTime): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amountToSubtract: Int, unit: TemporalUnit): OffsetDateTime - override fun minus(amountToSubtract: TemporalAmount): OffsetDateTime - open fun minusDays(days: Int): OffsetDateTime - open fun minusHours(hours: Int): OffsetDateTime - open fun minusMinutes(minutes: Int): OffsetDateTime - open fun minusMonths(months: Int): OffsetDateTime - open fun minusNanos(nanos: Double): OffsetDateTime - open fun minusSeconds(seconds: Int): OffsetDateTime - open fun minusWeeks(weeks: Int): OffsetDateTime - open fun minusYears(years: Int): OffsetDateTime - override fun plus(amountToAdd: Int, unit: TemporalUnit): OffsetDateTime - override fun plus(amountToAdd: TemporalAmount): OffsetDateTime - open fun plusDays(days: Int): OffsetDateTime - open fun plusHours(hours: Int): OffsetDateTime - open fun plusMinutes(minutes: Int): OffsetDateTime - open fun plusMonths(months: Int): OffsetDateTime - open fun plusNanos(nanos: Double): OffsetDateTime - open fun plusSeconds(seconds: Int): OffsetDateTime - open fun plusWeeks(weeks: Int): OffsetDateTime - open fun plusYears(years: Int): OffsetDateTime - override fun query(query: TemporalQuery): R? - override fun range(field: TemporalField): ValueRange - open fun toEpochSecond(): Double - open fun toJSON(): String - open fun toInstant(): Instant - open fun toLocalDate(): LocalDate - open fun toLocalDateTime(): LocalDateTime - open fun toLocalTime(): LocalTime - open fun toOffsetTime(): OffsetTime - override fun toString(): String - open fun truncatedTo(unit: TemporalUnit): OffsetDateTime - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): OffsetDateTime - override fun with(field: TemporalField, newValue: Int): OffsetDateTime - open fun withDayOfMonth(dayOfMonth: Int): OffsetDateTime - open fun withDayOfYear(dayOfYear: Int): OffsetDateTime - open fun withHour(hour: Int): OffsetDateTime - open fun withMinute(minute: Int): OffsetDateTime - open fun withMonth(month: Int): OffsetDateTime - open fun withNano(nanoOfSecond: Int): OffsetDateTime - open fun withOffsetSameInstant(offset: ZoneOffset): OffsetDateTime - open fun withOffsetSameLocal(offset: ZoneOffset): OffsetDateTime - open fun withSecond(second: Int): OffsetDateTime - open fun withYear(year: Int): OffsetDateTime - - companion object { - var MIN: OffsetDateTime - var MAX: OffsetDateTime - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): OffsetDateTime - fun now(clockOrZone: Clock): OffsetDateTime - fun now(clockOrZone: ZoneId): OffsetDateTime - fun of(dateTime: LocalDateTime, offset: ZoneOffset): OffsetDateTime - fun of(date: LocalDate, time: LocalTime, offset: ZoneOffset): OffsetDateTime - fun of(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanoOfSecond: Int, offset: ZoneOffset): OffsetDateTime - fun ofInstant(instant: Instant, zone: ZoneId): OffsetDateTime - fun parse(text: String): OffsetDateTime - } -} - -external open class OffsetTime : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun atDate(date: LocalDate): OffsetDateTime - open fun compareTo(other: OffsetTime): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - override fun get(field: TemporalField): Int - open fun hour(): Int - open fun minute(): Int - open fun nano(): Double - open fun offset(): ZoneOffset - open fun second(): Int - override fun hashCode(): Int - open fun isAfter(other: OffsetTime): Boolean - open fun isBefore(other: OffsetTime): Boolean - open fun isEqual(other: OffsetTime): Boolean - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amountToSubtract: Int, unit: TemporalUnit): OffsetTime - override fun minus(amountToSubtract: TemporalAmount): OffsetTime - open fun minusHours(hours: Int): OffsetTime - open fun minusMinutes(minutes: Int): OffsetTime - open fun minusNanos(nanos: Double): OffsetTime - open fun minusSeconds(seconds: Int): OffsetTime - override fun plus(amountToAdd: Int, unit: TemporalUnit): OffsetTime - override fun plus(amountToAdd: TemporalAmount): OffsetTime - open fun plusHours(hours: Int): OffsetTime - open fun plusMinutes(minutes: Int): OffsetTime - open fun plusNanos(nanos: Double): OffsetTime - open fun plusSeconds(seconds: Int): OffsetTime - override fun query(query: TemporalQuery): R? - override fun range(field: TemporalField): ValueRange - open fun toEpochSecond(date: LocalDate): Double - open fun toJSON(): String - open fun toLocalTime(): LocalTime - override fun toString(): String - open fun truncatedTo(unit: TemporalUnit): OffsetTime - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): OffsetTime - override fun with(field: TemporalField, newValue: Int): OffsetTime - open fun withHour(hour: Int): OffsetTime - open fun withMinute(minute: Int): OffsetTime - open fun withNano(nanoOfSecond: Int): OffsetTime - open fun withOffsetSameInstant(offset: ZoneOffset): OffsetTime - open fun withOffsetSameLocal(offset: ZoneOffset): OffsetTime - open fun withSecond(second: Int): OffsetTime +internal external class ZoneId : InteropInterface { + fun id(): String companion object { - var MIN: OffsetTime - var MAX: OffsetTime - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): OffsetTime - fun now(clockOrZone: Clock): OffsetTime - fun now(clockOrZone: ZoneId): OffsetTime - fun of(time: LocalTime, offset: ZoneOffset): OffsetTime - fun of(hour: Int, minute: Int, second: Int, nanoOfSecond: Int, offset: ZoneOffset): OffsetTime - fun ofInstant(instant: Instant, zone: ZoneId): OffsetTime - fun parse(text: String, formatter: DateTimeFormatter): OffsetTime - } -} - -external open class ZonedDateTime : ChronoZonedDateTime { - open fun dayOfMonth(): Int - open fun dayOfWeek(): DayOfWeek - open fun dayOfYear(): Int - override fun equals(other: InteropInterface): Boolean - override fun format(formatter: DateTimeFormatter): String - override fun hashCode(): Int - open fun hour(): Int - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean - override fun minus(amount: TemporalAmount): ZonedDateTime - override fun minus(amountToSubtract: Int, unit: TemporalUnit): ZonedDateTime - open fun minusDays(days: Int): ZonedDateTime - open fun minusHours(hours: Int): ZonedDateTime - open fun minusMinutes(minutes: Int): ZonedDateTime - open fun minusMonths(months: Int): ZonedDateTime - open fun minusNanos(nanos: Double): ZonedDateTime - open fun minusSeconds(seconds: Int): ZonedDateTime - open fun minusWeeks(weeks: Int): ZonedDateTime - open fun minusYears(years: Int): ZonedDateTime - open fun minute(): Int - open fun month(): Month - open fun monthValue(): Int - open fun nano(): Double - open fun offset(): ZoneOffset - override fun plus(amount: TemporalAmount): ZonedDateTime - override fun plus(amountToAdd: Int, unit: TemporalUnit): ZonedDateTime - open fun plusDays(days: Int): ZonedDateTime - open fun plusDays(days: Double): ZonedDateTime - open fun plusHours(hours: Int): ZonedDateTime - open fun plusMinutes(minutes: Int): ZonedDateTime - open fun plusMonths(months: Int): ZonedDateTime - open fun plusMonths(months: Double): ZonedDateTime - open fun plusNanos(nanos: Double): ZonedDateTime - open fun plusSeconds(seconds: Int): ZonedDateTime - open fun plusWeeks(weeks: Int): ZonedDateTime - open fun plusYears(years: Int): ZonedDateTime - override fun range(field: TemporalField): ValueRange - open fun second(): Int - open fun toJSON(): String - open fun toLocalDate(): LocalDate - open fun toLocalDateTime(): LocalDateTime - open fun toLocalTime(): LocalTime - open fun toOffsetDateTime(): OffsetDateTime - override fun toString(): String - open fun truncatedTo(unit: TemporalUnit): ZonedDateTime - override fun until(endExclusive: Temporal, unit: TemporalUnit): Double - override fun with(adjuster: TemporalAdjuster): ZonedDateTime - override fun with(field: TemporalField, newValue: Int): ZonedDateTime - open fun withDayOfMonth(dayOfMonth: Int): ZonedDateTime - open fun withDayOfYear(dayOfYear: Int): ZonedDateTime - open fun withEarlierOffsetAtOverlap(): ZonedDateTime - open fun withFixedOffsetZone(): ZonedDateTime - open fun withHour(hour: Int): ZonedDateTime - open fun withLaterOffsetAtOverlap(): ZonedDateTime - open fun withMinute(minute: Int): ZonedDateTime - open fun withMonth(month: Int): ZonedDateTime - open fun withNano(nanoOfSecond: Int): ZonedDateTime - open fun withSecond(second: Int): ZonedDateTime - open fun withYear(year: Int): ZonedDateTime - open fun withZoneSameInstant(zone: ZoneId): ZonedDateTime - open fun withZoneSameLocal(zone: ZoneId): ZonedDateTime - open fun year(): Int - open fun zone(): ZoneId - - companion object { - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): ZonedDateTime - fun now(clockOrZone: Clock): ZonedDateTime - fun now(clockOrZone: ZoneId): ZonedDateTime - fun of(localDateTime: LocalDateTime, zone: ZoneId): ZonedDateTime - fun of(date: LocalDate, time: LocalTime, zone: ZoneId): ZonedDateTime - fun of(year: Int, month: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanoOfSecond: Int, zone: ZoneId): ZonedDateTime - fun ofInstant(instant: Instant, zone: ZoneId): ZonedDateTime - fun ofInstant(localDateTime: LocalDateTime, offset: ZoneOffset, zone: ZoneId): ZonedDateTime - fun ofLocal(localDateTime: LocalDateTime, zone: ZoneId, preferredOffset: ZoneOffset?): ZonedDateTime - fun ofStrict(localDateTime: LocalDateTime, offset: ZoneOffset, zone: ZoneId): ZonedDateTime - fun parse(text: String): ZonedDateTime - } -} - -external open class ZoneId : InteropInterface { - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): Int - open fun id(): String - open fun normalized(): ZoneId - open fun rules(): ZoneRules - open fun toJSON(): String - override fun toString(): String - - companion object { - var SYSTEM: ZoneId - var UTC: ZoneId fun systemDefault(): ZoneId - fun of(zoneId: String): ZoneId - fun ofOffset(prefix: String, offset: ZoneOffset): ZoneId - fun from(temporal: TemporalAccessor): ZoneId - fun getAvailableZoneIds(): InteropInterface - } -} - -external open class ZoneOffset : ZoneId { - open fun adjustInto(temporal: Temporal): Temporal - open fun compareTo(other: ZoneOffset): Int - override fun equals(other: InteropInterface): Boolean - open fun get(field: TemporalField): Int - override fun hashCode(): Int - override fun id(): String - override fun rules(): ZoneRules - override fun toString(): String - open fun totalSeconds(): Int - - companion object { - var MAX_SECONDS: ZoneOffset - var UTC: ZoneOffset - var MIN: ZoneOffset - var MAX: ZoneOffset - fun of(offsetId: String): ZoneOffset - fun ofHours(hours: Int): ZoneOffset - fun ofHoursMinutes(hours: Int, minutes: Int): ZoneOffset - fun ofHoursMinutesSeconds(hours: Int, minutes: Int, seconds: Int): ZoneOffset - fun ofTotalMinutes(totalMinutes: Int): ZoneOffset - fun ofTotalSeconds(totalSeconds: Int): ZoneOffset - } -} - -external open class ZoneRegion : ZoneId { - override fun id(): String - override fun rules(): ZoneRules - - companion object { - fun ofId(zoneId: String): ZoneId - } -} - -external open class DayOfWeek : TemporalAccessor { - open fun adjustInto(temporal: Temporal): Temporal - open fun compareTo(other: DayOfWeek): Int - open fun equals(other: InteropInterface): Boolean - open fun displayName(style: TextStyle, locale: Locale): String - override fun isSupported(field: TemporalField): Boolean - open fun minus(days: Int): DayOfWeek - open fun name(): String - open fun ordinal(): Int - open fun plus(days: Int): DayOfWeek - open fun toJSON(): String - override fun toString(): String - open fun value(): Int - - companion object { - var MONDAY: DayOfWeek - var TUESDAY: DayOfWeek - var WEDNESDAY: DayOfWeek - var THURSDAY: DayOfWeek - var FRIDAY: DayOfWeek - var SATURDAY: DayOfWeek - var SUNDAY: DayOfWeek - var FROM: TemporalQuery - fun from(temporal: TemporalAccessor): DayOfWeek - fun of(dayOfWeek: Int): DayOfWeek - fun valueOf(name: String): DayOfWeek } } -external open class Month : TemporalAccessor { - open fun adjustInto(temporal: Temporal): Temporal - open fun compareTo(other: Month): Int - open fun equals(other: InteropInterface): Boolean - open fun firstDayOfYear(leapYear: Boolean): Int - open fun firstMonthOfQuarter(): Month - open fun displayName(style: TextStyle, locale: Locale): String - override fun isSupported(field: TemporalField): Boolean - open fun length(leapYear: Boolean): Int - open fun maxLength(): Int - open fun minLength(): Int - open fun minus(months: Int): Month - open fun name(): String - open fun ordinal(): Int - open fun plus(months: Int): Month - open fun toJSON(): String - override fun toString(): String - open fun value(): Int - - companion object { - var JANUARY: Month - var FEBRUARY: Month - var MARCH: Month - var APRIL: Month - var MAY: Month - var JUNE: Month - var JULY: Month - var AUGUST: Month - var SEPTEMBER: Month - var OCTOBER: Month - var NOVEMBER: Month - var DECEMBER: Month - fun from(temporal: TemporalAccessor): Month - fun of(month: Int): Month - fun valueOf(name: String): Month - } -} - -external open class DateTimeFormatter : InteropInterface { - open fun chronology(): Chronology? - open fun decimalStyle(): DecimalStyle - open fun format(temporal: TemporalAccessor): String - open fun locale(): InteropInterface - open fun parse(text: String): TemporalAccessor - open fun parse(text: String, query: TemporalQuery): T - open fun parseUnresolved(text: String, position: ParsePosition): TemporalAccessor - override fun toString(): String - open fun withChronology(chrono: Chronology): DateTimeFormatter - open fun withLocale(locale: Locale): DateTimeFormatter - open fun withResolverStyle(resolverStyle: ResolverStyle): DateTimeFormatter - - companion object { - var ISO_LOCAL_DATE: DateTimeFormatter - var ISO_LOCAL_TIME: DateTimeFormatter - var ISO_LOCAL_DATE_TIME: DateTimeFormatter - var ISO_INSTANT: DateTimeFormatter - var ISO_OFFSET_DATE_TIME: DateTimeFormatter - var ISO_ZONED_DATE_TIME: DateTimeFormatter - fun ofPattern(pattern: String): DateTimeFormatter - fun parsedExcessDays(): TemporalQuery - } -} - -external open class DateTimeFormatterBuilder : InteropInterface { - open fun append(formatter: DateTimeFormatter): DateTimeFormatterBuilder - open fun appendFraction(field: TemporalField, minWidth: Int, maxWidth: Int, decimalPoint: Boolean): DateTimeFormatterBuilder - open fun appendInstant(fractionalDigits: Int): DateTimeFormatterBuilder - open fun appendLiteral(literal: InteropInterface): DateTimeFormatterBuilder - open fun appendOffset(pattern: String, noOffsetText: String): DateTimeFormatterBuilder - open fun appendOffsetId(): DateTimeFormatterBuilder - open fun appendPattern(pattern: String): DateTimeFormatterBuilder - open fun appendValue(field: TemporalField, width: Int, maxWidth: Int, signStyle: SignStyle): DateTimeFormatterBuilder - open fun appendValueReduced(field: TemporalField, width: Int, maxWidth: Int, base: ChronoLocalDate): DateTimeFormatterBuilder - open fun appendValueReduced(field: TemporalField, width: Int, maxWidth: Int, base: Int): DateTimeFormatterBuilder - open fun appendZoneId(): DateTimeFormatterBuilder - open fun optionalEnd(): DateTimeFormatterBuilder - open fun optionalStart(): DateTimeFormatterBuilder - open fun padNext(): DateTimeFormatterBuilder - open fun parseCaseInsensitive(): DateTimeFormatterBuilder - open fun parseCaseSensitive(): DateTimeFormatterBuilder - open fun parseLenient(): DateTimeFormatterBuilder - open fun parseStrict(): DateTimeFormatterBuilder - open fun toFormatter(resolverStyle: ResolverStyle): DateTimeFormatter -} - -external open class DecimalStyle : InteropInterface { - open fun decimalSeparator(): String - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): InteropInterface - open fun negativeSign(): String - open fun positiveSign(): String - override fun toString(): String - open fun zeroDigit(): String -} - -external open class ResolverStyle : InteropInterface { - open fun equals(other: InteropInterface): Boolean - open fun toJSON(): String - override fun toString(): String - - companion object { - var STRICT: ResolverStyle - var SMART: ResolverStyle - var LENIENT: ResolverStyle - } -} - -external open class SignStyle : InteropInterface { - open fun equals(other: InteropInterface): Boolean - open fun toJSON(): String - override fun toString(): String - - companion object { - var NORMAL: SignStyle - var NEVER: SignStyle - var ALWAYS: SignStyle - var EXCEEDS_PAD: SignStyle - var NOT_NEGATIVE: SignStyle - } -} - -external open class TextStyle : InteropInterface { - open fun asNormal(): TextStyle - open fun asStandalone(): TextStyle - open fun isStandalone(): Boolean - open fun equals(other: InteropInterface): Boolean - open fun toJSON(): String - override fun toString(): String - - companion object { - var FULL: TextStyle - var FULL_STANDALONE: TextStyle - var SHORT: TextStyle - var SHORT_STANDALONE: TextStyle - var NARROW: TextStyle - var NARROW_STANDALONE: TextStyle - } -} - -external open class ParsePosition(index: Int) : InteropInterface { - open fun getIndex(): Int - open fun setIndex(index: Int) - open fun getErrorIndex(): Int - open fun setErrorIndex(errorIndex: Int) -} - -external open class ZoneOffsetTransition : InteropInterface { - open fun compareTo(transition: ZoneOffsetTransition): Int - open fun dateTimeAfter(): LocalDateTime - open fun dateTimeBefore(): LocalDateTime - open fun duration(): Duration - open fun durationSeconds(): Int - open fun equals(other: InteropInterface): Boolean - override fun hashCode(): Int - open fun instant(): Instant - open fun isGap(): Boolean - open fun isOverlap(): Boolean - open fun isValidOffset(offset: ZoneOffset): Boolean - open fun offsetAfter(): ZoneOffset - open fun offsetBefore(): ZoneOffset - open fun toEpochSecond(): Double - override fun toString(): String - - companion object { - fun of(transition: LocalDateTime, offsetBefore: ZoneOffset, offsetAfter: ZoneOffset): ZoneOffsetTransition - } -} - -external interface ZoneOffsetTransitionRule : InteropInterface - -external open class ZoneRules : InteropInterface { - open fun offset(instant: Instant): ZoneOffset - open fun offset(localDateTime: LocalDateTime): ZoneOffset - open fun toJSON(): String - open fun daylightSavings(instant: Instant): Duration - open fun isDaylightSavings(instant: Instant): Boolean - open fun isFixedOffset(): Boolean - open fun isValidOffset(localDateTime: LocalDateTime, offset: ZoneOffset): Boolean - open fun nextTransition(instant: Instant): ZoneOffsetTransition - open fun offsetOfEpochMilli(epochMilli: Double): ZoneOffset - open fun offsetOfInstant(instant: Instant): ZoneOffset - open fun offsetOfLocalDateTime(localDateTime: LocalDateTime): ZoneOffset - open fun previousTransition(instant: Instant): ZoneOffsetTransition - open fun standardOffset(instant: Instant): ZoneOffset - override fun toString(): String - open fun transition(localDateTime: LocalDateTime): ZoneOffsetTransition - - companion object { - fun of(offest: ZoneOffset): ZoneRules - } -} - -external open class ZoneRulesProvider : InteropInterface { - companion object { - fun getRules(zoneId: String): ZoneRules - } -} - -external open class IsoChronology : InteropInterface { - open fun equals(other: InteropInterface): Boolean - open fun resolveDate(fieldValues: InteropInterface, resolverStyle: InteropInterface): InteropInterface - override fun toString(): String - - companion object { - fun isLeapYear(prolepticYear: Int): Boolean - } -} - -external open class ChronoLocalDate : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun format(formatter: DateTimeFormatter): String - override fun isSupported(fieldOrUnit: TemporalField): Boolean - override fun isSupported(fieldOrUnit: TemporalUnit): Boolean -} - -external open class ChronoLocalDateTime : Temporal { - open fun adjustInto(temporal: Temporal): Temporal - open fun chronology(): Chronology - open fun toEpochSecond(offset: ZoneOffset): Double - open fun toInstant(offset: ZoneOffset): Instant -} - -external open class ChronoZonedDateTime : Temporal { - open fun compareTo(other: ChronoZonedDateTime): Int - open fun equals(other: InteropInterface): Boolean - open fun format(formatter: DateTimeFormatter): String - open fun isAfter(other: ChronoZonedDateTime): Boolean - open fun isBefore(other: ChronoZonedDateTime): Boolean - open fun isEqual(other: ChronoZonedDateTime): Boolean - open fun toEpochSecond(): Double - open fun toInstant(): Instant -} - -external interface Locale : InteropInterface - -external fun use(plugin: () -> InteropInterface): InteropInterface \ No newline at end of file +internal external object ZoneRulesProvider : InteropInterface diff --git a/core/js/src/JSJodaExceptions.kt b/core/js/src/JSJodaExceptions.kt deleted file mode 100644 index 9d0b53472..000000000 --- a/core/js/src/JSJodaExceptions.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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 - -internal actual fun Throwable.hasJsExceptionName(name: String): Boolean = - this.asDynamic().name == name - -internal actual inline fun jsTry(crossinline body: () -> T): T = body() \ No newline at end of file diff --git a/core/js/src/PlatformSpecifics.kt b/core/js/src/PlatformSpecifics.kt index 65236319b..c2be2b63b 100644 --- a/core/js/src/PlatformSpecifics.kt +++ b/core/js/src/PlatformSpecifics.kt @@ -6,9 +6,11 @@ package kotlinx.datetime.internal import kotlinx.datetime.internal.JSJoda.ZoneRulesProvider -internal actual fun readTzdb(): Pair, List> { +internal actual fun readTzdb(): Pair, List>? = try { val tzdbData = ZoneRulesProvider.asDynamic().getTzdbData() - return tzdbData.zones.unsafeCast>().toList() to tzdbData.links.unsafeCast>().toList() + tzdbData.zones.unsafeCast>().toList() to tzdbData.links.unsafeCast>().toList() +} catch (_: Throwable) { + null } public actual external interface InteropInterface diff --git a/core/js/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt b/core/js/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt deleted file mode 100644 index d2307c099..000000000 --- a/core/js/src/kotlinx.datetime.internal.JSJoda.module_@js-joda_core.kt +++ /dev/null @@ -1,32 +0,0 @@ -@file:JsModule("@js-joda/core") -@file:kotlinx.datetime.internal.JsNonModule -@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE", "INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") -package kotlinx.datetime.internal.JSJoda - -import kotlinx.datetime.internal.InteropInterface -import kotlin.js.* - -external fun nativeJs(date: Date, zone: ZoneId = definedExternally): TemporalAccessor - -external fun nativeJs(date: Date): TemporalAccessor - -external fun nativeJs(date: InteropInterface, zone: ZoneId = definedExternally): TemporalAccessor - -external fun nativeJs(date: InteropInterface): TemporalAccessor - -external interface `T$0` : InteropInterface { - var toDate: () -> Date - var toEpochMilli: () -> Double -} - -external fun convert(temporal: LocalDate, zone: ZoneId = definedExternally): `T$0` - -external fun convert(temporal: LocalDate): `T$0` - -external fun convert(temporal: LocalDateTime, zone: ZoneId = definedExternally): `T$0` - -external fun convert(temporal: LocalDateTime): `T$0` - -external fun convert(temporal: ZonedDateTime, zone: ZoneId = definedExternally): `T$0` - -external fun convert(temporal: ZonedDateTime): `T$0` \ No newline at end of file diff --git a/core/wasmJs/src/JSJodaExceptions.kt b/core/wasmJs/src/JSJodaExceptions.kt index 4f836904b..9e94fb3c3 100644 --- a/core/wasmJs/src/JSJodaExceptions.kt +++ b/core/wasmJs/src/JSJodaExceptions.kt @@ -5,14 +5,6 @@ package kotlinx.datetime -private fun checkExceptionName(exception: JsAny, name: String): Boolean = - js("exception.name === name") - -internal actual fun Throwable.hasJsExceptionName(name: String): Boolean { - val cause = (this as? JsException)?.jsException ?: return false - return checkExceptionName(cause, name) -} - private fun withCaughtJsException(body: () -> Unit): JsAny? = js("""{ try { body(); @@ -29,7 +21,7 @@ internal class JsException(val jsException: JsAny): Throwable() { get() = getExceptionMessage(jsException) } -internal actual inline fun jsTry(crossinline body: () -> T): T { +internal inline fun jsTry(crossinline body: () -> T): T { var result: T? = null val exception = withCaughtJsException { result = body() @@ -40,4 +32,4 @@ internal actual inline fun jsTry(crossinline body: () -> T): T } else { return result as T } -} \ No newline at end of file +} diff --git a/core/wasmJs/src/PlatformSpecifics.kt b/core/wasmJs/src/PlatformSpecifics.kt index 0b2e0a84b..2c08003f0 100644 --- a/core/wasmJs/src/PlatformSpecifics.kt +++ b/core/wasmJs/src/PlatformSpecifics.kt @@ -11,10 +11,14 @@ import kotlin.js.unsafeCast private fun getZones(rulesProvider: JsAny): JsAny = js("rulesProvider.getTzdbData().zones") private fun getLinks(rulesProvider: JsAny): JsAny = js("rulesProvider.getTzdbData().links") -internal actual fun readTzdb(): Pair, List> { - val zones = getZones(ZoneRulesProvider as JsAny) - val links = getLinks(ZoneRulesProvider as JsAny) - return zones.unsafeCast>().toList() to links.unsafeCast>().toList() +internal actual fun readTzdb(): Pair, List>? = try { + jsTry { + val zones = getZones(ZoneRulesProvider as JsAny) + val links = getLinks(ZoneRulesProvider as JsAny) + return zones.unsafeCast>().toList() to links.unsafeCast>().toList() + } +} catch (_: Throwable) { + return null } private fun JsArray.toList(): List = buildList { From 74cbbe3c9ad5f9463a1bf9aa15ce7d9728cc40ce Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 4 Nov 2024 16:28:57 +0100 Subject: [PATCH 05/14] Reimplement the behavior for when the js-joda tzdb is not available --- .../src/internal/TimeZoneNative.kt | 9 ++- core/commonJs/src/internal/Platform.kt | 65 ++++++++++++++----- core/commonKotlin/src/TimeZone.kt | 13 ++-- core/commonKotlin/src/internal/Platform.kt | 10 ++- core/darwin/src/internal/TimeZoneNative.kt | 9 ++- core/linux/src/internal/TimeZoneNative.kt | 9 ++- core/wasmJs/src/JSJodaExceptions.kt | 2 +- core/wasmJs/src/PlatformSpecifics.kt | 4 +- core/wasmWasi/src/internal/Platform.kt | 5 -- .../src/internal/TimeZonesInitializer.kt | 23 ++++--- core/windows/src/internal/TimeZoneNative.kt | 10 ++- core/windows/src/internal/TzdbInRegistry.kt | 5 +- 12 files changed, 110 insertions(+), 54 deletions(-) diff --git a/core/androidNative/src/internal/TimeZoneNative.kt b/core/androidNative/src/internal/TimeZoneNative.kt index 08b600413..f819e4b00 100644 --- a/core/androidNative/src/internal/TimeZoneNative.kt +++ b/core/androidNative/src/internal/TimeZoneNative.kt @@ -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 = + tzdb.getOrThrow().availableTimeZoneIds() private val tzdb = runCatching { TzdbBionic() } -internal actual fun currentSystemDefaultZone(): Pair = memScoped { +internal actual fun currentSystemDefaultZone(): Pair = memScoped { val name = readSystemProperty("persist.sys.timezone") ?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone") return name to null diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 2e2ffdc4c..20d3dcf51 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -11,11 +11,7 @@ import kotlinx.datetime.internal.JSJoda.ZoneId import kotlin.math.roundToInt import kotlin.math.roundToLong -private val tzdb: Result = runCatching { parseTzdb() } - -internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow() - -private fun parseTzdb(): TimeZoneDatabase { +private val tzdb: Result = runCatching { /** * References: * - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/MomentZoneRulesProvider.js#L78-L94 @@ -71,7 +67,7 @@ private fun parseTzdb(): TimeZoneDatabase { fun List.partialSums(): List = scanWithoutInitial(0, Long::plus) val zones = mutableMapOf() - val (zonesPacked, linksPacked) = readTzdb() ?: return EmptyTimeZoneDatabase + val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null for (zone in zonesPacked) { val components = zone.split('|') val offsets = components[2].split(' ').map { unpackBase60(it) } @@ -92,7 +88,7 @@ private fun parseTzdb(): TimeZoneDatabase { zones[components[1]] = rules } } - return object : TimeZoneDatabase { + object : TimeZoneDatabase { override fun rulesForId(id: String): TimeZoneRules = zones[id] ?: throw IllegalTimeZoneException("Unknown time zone: $id") @@ -100,24 +96,57 @@ private fun parseTzdb(): TimeZoneDatabase { } } -private object EmptyTimeZoneDatabase : TimeZoneDatabase { - override fun rulesForId(id: String): TimeZoneRules = when (id) { - "SYSTEM" -> TimeZoneRules( - transitionEpochSeconds = emptyList(), - offsets = listOf(UtcOffset.ZERO), - recurringZoneRules = null - ) // TODO: that's not correct, we need to use `Date()`'s offset - else -> throw IllegalTimeZoneException("JSJoda timezone database is not available") +private object SystemTimeZone: TimeZone() { + override val id: String get() = "SYSTEM" + + /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/LocalDate.js#L1404-L1416 + + * https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L69-L71 */ + override fun atStartOfDay(date: LocalDate): Instant = atZone(date.atTime(LocalTime.MIN)).toInstant() + + /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L21-L24 */ + override fun offsetAtImpl(instant: Instant): UtcOffset = + UtcOffset(minutes = -Date(instant.toEpochMilliseconds().toDouble()).getTimezoneOffset().toInt()) + + /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L49-L55 */ + override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime { + val epochMilli = dateTime.toInstant(UTC).toEpochMilliseconds() + val offsetInMinutesBeforePossibleTransition = Date(epochMilli.toDouble()).getTimezoneOffset().toInt() + val epochMilliSystemZone = epochMilli + + offsetInMinutesBeforePossibleTransition * SECONDS_PER_MINUTE * MILLIS_PER_ONE + val offsetInMinutesAfterPossibleTransition = Date(epochMilliSystemZone.toDouble()).getTimezoneOffset().toInt() + val offset = UtcOffset(minutes = -offsetInMinutesAfterPossibleTransition) + return ZonedDateTime(dateTime, this, offset) } - override fun availableTimeZoneIds(): Set = emptySet() + override fun equals(other: Any?): Boolean = other === this + + override fun hashCode(): Int = id.hashCode() +} + +internal actual fun currentSystemDefaultZone(): Pair { + val id = ZoneId.systemDefault().id() + return if (id == "SYSTEM") id to SystemTimeZone + else id to null +} + +internal actual fun timeZoneById(zoneId: String): TimeZone { + val id = if (zoneId == "SYSTEM") { + val (name, zone) = currentSystemDefaultZone() + if (zone != null) return zone + name + } else zoneId + val rules = tzdb.getOrThrow()?.rulesForId(id) + if (rules != null) return RegionTimeZone(rules, id) + throw IllegalTimeZoneException("js-joda timezone database is not available") } -internal actual fun currentSystemDefaultZone(): Pair = - ZoneId.systemDefault().id() to null +internal actual fun getAvailableZoneIds(): Set = + tzdb.getOrThrow()?.availableTimeZoneIds() ?: setOf("UTC") internal actual fun currentTime(): Instant = Instant.fromEpochMilliseconds(Date().getTime().toLong()) internal external class Date() { + constructor(milliseconds: Double) fun getTime(): Double + fun getTimezoneOffset(): Double } diff --git a/core/commonKotlin/src/TimeZone.kt b/core/commonKotlin/src/TimeZone.kt index aea398dad..70cca29b2 100644 --- a/core/commonKotlin/src/TimeZone.kt +++ b/core/commonKotlin/src/TimeZone.kt @@ -20,11 +20,11 @@ public actual open class TimeZone internal constructor() { public actual fun currentSystemDefault(): TimeZone { // TODO: probably check if currentSystemDefault name is parseable as FixedOffsetTimeZone? - val (name, rules) = currentSystemDefaultZone() - return if (rules == null) { + val (name, zone) = currentSystemDefaultZone() + return if (zone == null) { of(name) } else { - RegionTimeZone(rules, name) + zone } } @@ -36,6 +36,9 @@ public actual open class TimeZone internal constructor() { if (zoneId == "Z") { return UTC } + if (zoneId == "SYSTEM") { + return currentSystemDefault() + } if (zoneId.length == 1) { throw IllegalTimeZoneException("Invalid zone ID: $zoneId") } @@ -67,14 +70,14 @@ public actual open class TimeZone internal constructor() { throw IllegalTimeZoneException(e) } return try { - RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId) + timeZoneById(zoneId) } catch (e: Exception) { throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e) } } public actual val availableZoneIds: Set - get() = systemTzdb.availableTimeZoneIds() + get() = getAvailableZoneIds() } public actual open val id: String diff --git a/core/commonKotlin/src/internal/Platform.kt b/core/commonKotlin/src/internal/Platform.kt index 51efdc955..9f52b0b46 100644 --- a/core/commonKotlin/src/internal/Platform.kt +++ b/core/commonKotlin/src/internal/Platform.kt @@ -6,9 +6,13 @@ package kotlinx.datetime.internal import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone -internal expect val systemTzdb: TimeZoneDatabase +// RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId) +internal expect fun timeZoneById(zoneId: String): TimeZone -internal expect fun currentSystemDefaultZone(): Pair +internal expect fun getAvailableZoneIds(): Set -internal expect fun currentTime(): Instant \ No newline at end of file +internal expect fun currentSystemDefaultZone(): Pair + +internal expect fun currentTime(): Instant diff --git a/core/darwin/src/internal/TimeZoneNative.kt b/core/darwin/src/internal/TimeZoneNative.kt index 74134336e..3182ad005 100644 --- a/core/darwin/src/internal/TimeZoneNative.kt +++ b/core/darwin/src/internal/TimeZoneNative.kt @@ -7,16 +7,21 @@ package kotlinx.datetime.internal import kotlinx.cinterop.* +import kotlinx.datetime.TimeZone import kotlinx.datetime.internal.* import platform.Foundation.* -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 = + tzdb.getOrThrow().availableTimeZoneIds() private val tzdb = runCatching { TzdbOnFilesystem(Path.fromString(defaultTzdbPath())) } internal expect fun defaultTzdbPath(): String -internal actual fun currentSystemDefaultZone(): Pair { +internal actual fun currentSystemDefaultZone(): Pair { /* The framework has its own cache of the system timezone. Calls to [NSTimeZone systemTimeZone] do not reflect changes to the system timezone and instead just return the cached value. Thus, to acquire the current diff --git a/core/linux/src/internal/TimeZoneNative.kt b/core/linux/src/internal/TimeZoneNative.kt index 47a1accb9..745023aa4 100644 --- a/core/linux/src/internal/TimeZoneNative.kt +++ b/core/linux/src/internal/TimeZoneNative.kt @@ -6,12 +6,17 @@ package kotlinx.datetime.internal import kotlinx.datetime.IllegalTimeZoneException +import kotlinx.datetime.TimeZone -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 = + tzdb.getOrThrow().availableTimeZoneIds() private val tzdb = runCatching { TzdbOnFilesystem() } -internal actual fun currentSystemDefaultZone(): Pair { +internal actual fun currentSystemDefaultZone(): Pair { // according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used val zonePath = currentSystemTimeZonePath ?: return "Z" to null val zoneId = zonePath.splitTimeZonePath()?.second?.toString() diff --git a/core/wasmJs/src/JSJodaExceptions.kt b/core/wasmJs/src/JSJodaExceptions.kt index 9e94fb3c3..2b5fb9ada 100644 --- a/core/wasmJs/src/JSJodaExceptions.kt +++ b/core/wasmJs/src/JSJodaExceptions.kt @@ -3,7 +3,7 @@ * 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 +package kotlinx.datetime.internal private fun withCaughtJsException(body: () -> Unit): JsAny? = js("""{ try { diff --git a/core/wasmJs/src/PlatformSpecifics.kt b/core/wasmJs/src/PlatformSpecifics.kt index 2c08003f0..02bbd7ef5 100644 --- a/core/wasmJs/src/PlatformSpecifics.kt +++ b/core/wasmJs/src/PlatformSpecifics.kt @@ -15,10 +15,10 @@ internal actual fun readTzdb(): Pair, List>? = try { jsTry { val zones = getZones(ZoneRulesProvider as JsAny) val links = getLinks(ZoneRulesProvider as JsAny) - return zones.unsafeCast>().toList() to links.unsafeCast>().toList() + zones.unsafeCast>().toList() to links.unsafeCast>().toList() } } catch (_: Throwable) { - return null + null } private fun JsArray.toList(): List = buildList { diff --git a/core/wasmWasi/src/internal/Platform.kt b/core/wasmWasi/src/internal/Platform.kt index efd3cf1ac..1403cae7d 100644 --- a/core/wasmWasi/src/internal/Platform.kt +++ b/core/wasmWasi/src/internal/Platform.kt @@ -37,8 +37,3 @@ internal actual fun currentTime(): Instant = clockTimeGet().let { time -> // Instant.MAX and Instant.MIN are never going to be exceeded using just the Long number of nanoseconds Instant(time.floorDiv(NANOS_PER_ONE.toLong()), time.mod(NANOS_PER_ONE.toLong()).toInt()) } - -internal actual fun currentSystemDefaultZone(): Pair = - "UTC" to null - -internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData() \ No newline at end of file diff --git a/core/wasmWasi/src/internal/TimeZonesInitializer.kt b/core/wasmWasi/src/internal/TimeZonesInitializer.kt index 7a8fbec98..023d83c10 100644 --- a/core/wasmWasi/src/internal/TimeZonesInitializer.kt +++ b/core/wasmWasi/src/internal/TimeZonesInitializer.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.internal import kotlinx.datetime.IllegalTimeZoneException +import kotlinx.datetime.TimeZone @RequiresOptIn internal annotation class InternalDateTimeApi @@ -32,13 +33,15 @@ public fun initializeTimeZonesProvider(provider: TimeZonesProvider) { private var timeZonesProvider: TimeZonesProvider? = null @OptIn(InternalDateTimeApi::class) -internal class TzdbOnData: TimeZoneDatabase { - override fun rulesForId(id: String): TimeZoneRules { - val data = timeZonesProvider?.zoneDataByName(id) - ?: throw IllegalTimeZoneException("TimeZones are not supported") - return readTzFile(data).toTimeZoneRules() - } - - override fun availableTimeZoneIds(): Set = - timeZonesProvider?.getTimeZones() ?: setOf("UTC") -} \ No newline at end of file +internal actual fun timeZoneById(zoneId: String): TimeZone { + val data = timeZonesProvider?.zoneDataByName(zoneId) + ?: throw IllegalTimeZoneException("TimeZones are not supported") + val rules = readTzFile(data).toTimeZoneRules() + return RegionTimeZone(rules, zoneId) +} + +@OptIn(InternalDateTimeApi::class) +internal actual fun getAvailableZoneIds(): Set = + timeZonesProvider?.getTimeZones() ?: setOf("UTC") + +internal actual fun currentSystemDefaultZone(): Pair = "UTC" to null diff --git a/core/windows/src/internal/TimeZoneNative.kt b/core/windows/src/internal/TimeZoneNative.kt index 0d9462855..b889d158a 100644 --- a/core/windows/src/internal/TimeZoneNative.kt +++ b/core/windows/src/internal/TimeZoneNative.kt @@ -5,9 +5,15 @@ package kotlinx.datetime.internal -internal actual val systemTzdb: TimeZoneDatabase get() = tzdbInRegistry.getOrThrow() +import kotlinx.datetime.TimeZone -internal actual fun currentSystemDefaultZone(): Pair = +internal actual fun timeZoneById(zoneId: String): TimeZone = + RegionTimeZone(tzdbInRegistry.getOrThrow().rulesForId(zoneId), zoneId) + +internal actual fun getAvailableZoneIds(): Set = + tzdbInRegistry.getOrThrow().availableTimeZoneIds() + +internal actual fun currentSystemDefaultZone(): Pair = tzdbInRegistry.getOrThrow().currentSystemDefault() private val tzdbInRegistry = runCatching { TzdbInRegistry() } diff --git a/core/windows/src/internal/TzdbInRegistry.kt b/core/windows/src/internal/TzdbInRegistry.kt index f5781da6e..b78f7ec6d 100644 --- a/core/windows/src/internal/TzdbInRegistry.kt +++ b/core/windows/src/internal/TzdbInRegistry.kt @@ -86,7 +86,7 @@ internal class TzdbInRegistry: TimeZoneDatabase { windowsToRules.containsKey(it.value) }.keys - internal fun currentSystemDefault(): Pair = memScoped { + internal fun currentSystemDefault(): Pair = memScoped { val dtzi = alloc() val result = GetDynamicTimeZoneInformation(dtzi.ptr) check(result != TIME_ZONE_ID_INVALID) { "The current system time zone is invalid: ${getLastWindowsError()}" } @@ -95,12 +95,13 @@ internal class TzdbInRegistry: TimeZoneDatabase { ?: throw IllegalStateException("Unknown time zone name '$windowsName'") val tz = windowsToRules[windowsName] check(tz != null) { "The system time zone is set to a value rules for which are not known: '$windowsName'" } - ianaTzName to if (dtzi.DynamicDaylightTimeDisabled == 0.convert()) { + val rules = if (dtzi.DynamicDaylightTimeDisabled == 0.convert()) { tz } else { // the user explicitly disabled DST transitions, so TimeZoneRules(UtcOffset(minutes = -(dtzi.Bias + dtzi.StandardBias)), RecurringZoneRules(emptyList())) } + return ianaTzName to RegionTimeZone(rules, ianaTzName) } } From 34ab70152ba108e6f983fb7df349cd72e2e3d980 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 5 Nov 2024 11:45:24 +0100 Subject: [PATCH 06/14] Add tests for no-timezone-database JS implementation --- js-without-timezones/build.gradle.kts | 64 +++++++++++++ .../test/TimezonesWithoutDatabaseTest.kt | 96 +++++++++++++++++++ settings.gradle.kts | 3 +- 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 js-without-timezones/build.gradle.kts create mode 100644 js-without-timezones/common/test/TimezonesWithoutDatabaseTest.kt diff --git a/js-without-timezones/build.gradle.kts b/js-without-timezones/build.gradle.kts new file mode 100644 index 000000000..0e2b221f4 --- /dev/null +++ b/js-without-timezones/build.gradle.kts @@ -0,0 +1,64 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import java.util.Locale + +plugins { + id("kotlin-multiplatform") + id("org.jetbrains.kotlinx.kover") +} + +val mainJavaToolchainVersion: String by project + +java { + toolchain { languageVersion.set(JavaLanguageVersion.of(mainJavaToolchainVersion)) } +} + +kotlin { + + js { + nodejs { + } + compilations.all { + kotlinOptions { + sourceMap = true + moduleKind = "umd" + metaInfo = true + } + } + } + + + wasmJs { + nodejs { + } + } + + sourceSets.all { + val suffixIndex = name.indexOfLast { it.isUpperCase() } + val targetName = name.substring(0, suffixIndex) + val suffix = name.substring(suffixIndex).toLowerCase(Locale.ROOT).takeIf { it != "main" } + kotlin.srcDir("$targetName/${suffix ?: "src"}") + resources.srcDir("$targetName/${suffix?.let { it + "Resources" } ?: "resources"}") + } + + targets.withType { + compilations["test"].kotlinOptions { + freeCompilerArgs += listOf("-trw") + } + } + + sourceSets { + commonMain { + dependencies { + api(project(":kotlinx-datetime")) + } + } + + commonTest { + dependencies { + api("org.jetbrains.kotlin:kotlin-test") + } + } + } +} diff --git a/js-without-timezones/common/test/TimezonesWithoutDatabaseTest.kt b/js-without-timezones/common/test/TimezonesWithoutDatabaseTest.kt new file mode 100644 index 000000000..153b505cd --- /dev/null +++ b/js-without-timezones/common/test/TimezonesWithoutDatabaseTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * 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.test + +import kotlinx.datetime.* +import kotlin.test.* + +class TimezonesWithoutDatabaseTest { + @Test + fun system() { + val tz = TimeZone.currentSystemDefault() + assertEquals("SYSTEM", tz.id) + assertEquals("SYSTEM", tz.toString()) + val now = Clock.System.now() + assertEquals(now, now.toLocalDateTime(tz).toInstant(tz)) + val offset = now.offsetIn(tz) + println("$now = ${now.toLocalDateTime(tz)}$offset") + assertEquals(now, now.toLocalDateTime(tz).toInstant(offset)) + assertEquals(Instant.DISTANT_PAST, Instant.DISTANT_PAST.toLocalDateTime(tz).toInstant(tz)) + assertEquals(Instant.DISTANT_FUTURE, Instant.DISTANT_FUTURE.toLocalDateTime(tz).toInstant(tz)) + val today = now.toLocalDateTime(tz).date + assertEquals(today.atTime(0, 0).toInstant(tz), today.atStartOfDayIn(tz)) + } + + @Test + fun utc() { + val utc: FixedOffsetTimeZone = TimeZone.UTC + println(utc) + assertEquals("Z", utc.id) + assertEquals(UtcOffset.ZERO, utc.offset) + assertEquals(0, utc.offset.totalSeconds) + assertEquals(utc.offset, utc.offsetAt(Clock.System.now())) + } + + @Test + fun available() { + assertEquals(setOf("UTC"), TimeZone.availableZoneIds) + } + + @Test + fun of() { + assertFailsWith { TimeZone.of("Europe/Moscow") } + assertSame(TimeZone.currentSystemDefault(), TimeZone.of("SYSTEM")) + } + + // from 310bp + @Test + fun timeZoneEquals() { + val test1 = TimeZone.of("SYSTEM") + val test2 = TimeZone.of("UTC") + val test2b = TimeZone.of("UTC+00:00") + assertEquals(false, test1 == test2) + assertEquals(false, test2 == test1) + + assertEquals(true, test1 == test1) + assertEquals(true, test2 == test2) + assertEquals(true, test2 == test2b) + + assertEquals(test1.hashCode(), test1.hashCode()) + assertEquals(test2.hashCode(), test2.hashCode()) + assertEquals(test2.hashCode(), test2b.hashCode()) + } + + // from 310bp + @Test + fun timeZoneToString() { + val idToString = arrayOf( + Pair("Z", "Z"), + Pair("UTC", "UTC"), + Pair("UTC+01:00", "UTC+01:00"), + Pair("GMT+01:00", "GMT+01:00"), + Pair("UT+01:00", "UT+01:00")) + for ((id, str) in idToString) { + assertEquals(str, TimeZone.of(id).toString()) + } + } + + @Test + fun utcOffsetNormalization() { + val sameOffsetTZs = listOf("+04", "+04:00", "UTC+4", "UT+04", "GMT+04:00:00").map { TimeZone.of(it) } + for (tz in sameOffsetTZs) { + assertIs(tz) + } + val offsets = sameOffsetTZs.map { (it as FixedOffsetTimeZone).offset } + val zoneIds = sameOffsetTZs.map { it.id } + + assertTrue(offsets.distinct().size == 1, "Expected all offsets to be equal: $offsets") + assertTrue(offsets.map { it.toString() }.distinct().size == 1, "Expected all offsets to have the same string representation: $offsets") + + assertTrue(zoneIds.distinct().size > 1, "Expected some fixed offset zones to have different ids: $zoneIds") + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 33b0f06c0..1a9b35c10 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,5 +20,6 @@ include(":timezones/full") project(":timezones/full").name = "kotlinx-datetime-zoneinfo" include(":serialization") project(":serialization").name = "kotlinx-datetime-serialization" +include(":js-without-timezones") +project(":js-without-timezones").name = "kotlinx-datetime-js-without-timezones" include(":benchmarks") - From 17bfddc25c8d896d62f3b994569a4231980ed79b Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 5 Nov 2024 12:46:18 +0100 Subject: [PATCH 07/14] Test the js-joda tzdb extraction --- core/commonJs/src/internal/Platform.kt | 12 ++-- .../test/JSJoda.module_@js-joda_core.kt | 32 +++++++++ core/commonJs/test/JsJodaTimezoneTest.kt | 66 +++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 core/commonJs/test/JSJoda.module_@js-joda_core.kt create mode 100644 core/commonJs/test/JsJodaTimezoneTest.kt diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 20d3dcf51..2606e1c49 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -36,7 +36,7 @@ private val tzdb: Result = runCatching { when (char) { in '0'..'9' -> char - '0' in 'a'..'z' -> char - 'a' + 10 - in 'A'..'Z' -> char - 'A' + 36 + in 'A'..'X' -> char - 'A' + 36 else -> throw IllegalArgumentException("Invalid character: $char") } @@ -76,9 +76,10 @@ private val tzdb: Result = runCatching { (unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds MILLIS_PER_ONE // but we only need seconds } + println("Zone ${components[0]}: $offsets with ${components[2]} raw data") zones[components[0]] = TimeZoneRules( transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take(indices.size - 1), - offsets = indices.map { UtcOffset(null, -offsets[it].roundToInt(), null) }, + offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) }, recurringZoneRules = null ) } @@ -132,14 +133,15 @@ internal actual fun currentSystemDefaultZone(): Pair { internal actual fun timeZoneById(zoneId: String): TimeZone { val id = if (zoneId == "SYSTEM") { val (name, zone) = currentSystemDefaultZone() - if (zone != null) return zone + zone?.let { return it } name } else zoneId - val rules = tzdb.getOrThrow()?.rulesForId(id) - if (rules != null) return RegionTimeZone(rules, id) + rulesForId(id)?.let { return RegionTimeZone(it, id) } throw IllegalTimeZoneException("js-joda timezone database is not available") } +internal fun rulesForId(zoneId: String): TimeZoneRules? = tzdb.getOrThrow()?.rulesForId(zoneId) + internal actual fun getAvailableZoneIds(): Set = tzdb.getOrThrow()?.availableTimeZoneIds() ?: setOf("UTC") diff --git a/core/commonJs/test/JSJoda.module_@js-joda_core.kt b/core/commonJs/test/JSJoda.module_@js-joda_core.kt new file mode 100644 index 000000000..ba81bb475 --- /dev/null +++ b/core/commonJs/test/JSJoda.module_@js-joda_core.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ +@file:kotlinx.datetime.internal.JsModule("@js-joda/core") +@file:kotlinx.datetime.internal.JsNonModule +package kotlinx.datetime.test.JSJoda + +import kotlinx.datetime.internal.InteropInterface + +external class ZonedDateTime : InteropInterface { + fun year(): Int + fun monthValue(): Int + fun dayOfMonth(): Int + fun hour(): Int + fun minute(): Int + fun second(): Int + fun nano(): Double +} + +external class Instant : InteropInterface { + fun atZone(zone: ZoneId): ZonedDateTime + companion object { + fun ofEpochMilli(epochMilli: Double): Instant + } +} + +external class ZoneId : InteropInterface { + companion object { + fun of(zoneId: String): ZoneId + } +} diff --git a/core/commonJs/test/JsJodaTimezoneTest.kt b/core/commonJs/test/JsJodaTimezoneTest.kt new file mode 100644 index 000000000..7ca8f9eca --- /dev/null +++ b/core/commonJs/test/JsJodaTimezoneTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * 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.test + +import kotlinx.datetime.* +import kotlinx.datetime.internal.rulesForId +import kotlin.math.roundToInt +import kotlin.test.* +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class JsJodaTimezoneTest { + @Test + fun system() { + val tz = TimeZone.currentSystemDefault() + assertNotEquals("SYSTEM", tz.id) + val systemTz = TimeZone.of("SYSTEM") + assertEquals(tz, systemTz) + assertEquals(tz.id, systemTz.id) + } + + @Test + fun iterateOverAllTimezones() { + for (id in TimeZone.availableZoneIds) { + val rules = rulesForId(id) ?: throw AssertionError("No rules for $id") + val jodaZone = kotlinx.datetime.test.JSJoda.ZoneId.of(id) + assertNull(rules.recurringZoneRules) + fun checkAtInstant(instant: Instant) { + val jodaInstant = kotlinx.datetime.test.JSJoda.Instant.ofEpochMilli(instant.toEpochMilliseconds().toDouble()) + val zdt = jodaInstant.atZone(jodaZone) + val offset = rules.infoAtInstant(instant) + val ourLdt = instant.toLocalDateTime(offset) + val theirLdt = LocalDateTime( + zdt.year(), + zdt.monthValue(), + zdt.dayOfMonth(), + zdt.hour(), + zdt.minute(), + zdt.second(), + zdt.nano().roundToInt() + ) + if ((ourLdt.toInstant(TimeZone.UTC) - theirLdt.toInstant(TimeZone.UTC)).absoluteValue > 1.seconds) { + // It seems that sometimes, js-joda interprets its data incorrectly by at most one second, + // and we don't want to replicate that. + // Example: America/Noronha at 1914-01-01T02:09:39.998Z: + // - Computed 1913-12-31T23:59:59.998 with offset -02:09:40 + // - 1914-01-01T00:00:00.998-02:09:39[America/Noronha] is js-joda's interpretation + // The raw data representing the offset is `29.E`, which is `2 * 60 + 9 + (ord 'E' - 29) / 60`, + // and `ord 'E'` is 69, so the offset is -2:09:40. + // Thus, we allow a difference of 1 second. + throw AssertionError("Failed for $id at $instant: computed $ourLdt with offset $offset, but $zdt is correct") + } + } + fun checkTransition(instant: Instant) { + checkAtInstant(instant - 2.milliseconds) + checkAtInstant(instant) + } + // check historical data + for (transition in rules.transitionEpochSeconds) { + checkTransition(Instant.fromEpochSeconds(transition)) + } + } + } +} From e1a2c61db2f7716a1b2e63a1cdbdb5293ee8e6c5 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 5 Nov 2024 12:54:39 +0100 Subject: [PATCH 08/14] Cleanup --- core/common/src/DateTimePeriod.kt | 17 +++++--- core/common/test/DateTimePeriodTest.kt | 2 +- core/common/test/InstantTest.kt | 10 +++-- core/commonJs/src/PlatformSpecifics.kt | 4 +- core/commonJs/src/internal/Platform.kt | 1 - core/commonJs/test/JsJodaTimezoneTest.kt | 50 ++++++++++++---------- core/commonKotlin/src/Instant.kt | 8 ++-- core/commonKotlin/src/internal/Platform.kt | 1 - js-without-timezones/build.gradle.kts | 21 --------- license/README.md | 14 +++--- license/thirdparty/js-joda_license.txt | 34 +++++++++++++++ 11 files changed, 93 insertions(+), 69 deletions(-) create mode 100644 license/thirdparty/js-joda_license.txt diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 25f008b35..3f1abd652 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -13,6 +13,7 @@ import kotlinx.datetime.serializers.DateTimePeriodComponentSerializer import kotlin.math.* import kotlin.time.Duration import kotlinx.serialization.Serializable +import kotlin.text.toLong /** * A difference between two [instants][Instant], decomposed into date and time components. @@ -448,7 +449,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) @@ -499,7 +501,11 @@ private class DateTimePeriodImpl( override val totalNanoseconds: Long, ) : DateTimePeriod() -private fun totalMonths(years: Int, months: Int): Long = years.toLong() * 12 + months.toLong() +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 @@ -536,9 +542,10 @@ internal fun buildDateTimePeriod(totalMonths: Long = 0, days: Int = 0, totalNano * (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( diff --git a/core/common/test/DateTimePeriodTest.kt b/core/common/test/DateTimePeriodTest.kt index 3c93e4a9d..85d4345b9 100644 --- a/core/common/test/DateTimePeriodTest.kt +++ b/core/common/test/DateTimePeriodTest.kt @@ -114,7 +114,7 @@ class DateTimePeriodTest { assertFailsWith { DateTimePeriod.parse("P") } - // overflow of `Long.MAX_VALUE` months + // overflow of `Int.MAX_VALUE` years assertFailsWith { DateTimePeriod.parse("P768614336404564651Y") } assertFailsWith { DateTimePeriod.parse("P1Y9223372036854775805M") } diff --git a/core/common/test/InstantTest.kt b/core/common/test/InstantTest.kt index 4788e2f68..171796260 100644 --- a/core/common/test/InstantTest.kt +++ b/core/common/test/InstantTest.kt @@ -11,6 +11,7 @@ import kotlinx.datetime.internal.* import kotlin.random.* import kotlin.test.* import kotlin.time.* +import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds @@ -268,9 +269,8 @@ class InstantTest { val offset3 = instant3.offsetIn(zone) assertEquals(offset1, offset3) - // TODO: fails on JS - // // without the minus, this test fails on JVM - // (Instant.MAX - (2 * 365).days).offsetIn(zone) + // without the minus, this test fails on JVM + (Instant.MAX - (2 * 365).days).offsetIn(zone) } @Test @@ -313,7 +313,9 @@ class InstantTest { val diff2 = date1.periodUntil(date2) if (diff1 != diff2) - println("start: $instant1, end: $instant2, diff by instants: $diff1, diff by dates: $diff2") + throw AssertionError( + "start: $instant1, end: $instant2, diff by instants: $diff1, diff by dates: $diff2" + ) } } } diff --git a/core/commonJs/src/PlatformSpecifics.kt b/core/commonJs/src/PlatformSpecifics.kt index 2813677da..cc65af61f 100644 --- a/core/commonJs/src/PlatformSpecifics.kt +++ b/core/commonJs/src/PlatformSpecifics.kt @@ -12,10 +12,10 @@ public expect interface InteropInterface @OptIn(ExperimentalMultiplatform::class) @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +@Target(AnnotationTarget.FILE) @OptionalExpectation public expect annotation class JsNonModule() @Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +@Target(AnnotationTarget.FILE) public expect annotation class JsModule(val import: String) diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 2606e1c49..86bd4cf82 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -76,7 +76,6 @@ private val tzdb: Result = runCatching { (unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds MILLIS_PER_ONE // but we only need seconds } - println("Zone ${components[0]}: $offsets with ${components[2]} raw data") zones[components[0]] = TimeZoneRules( transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take(indices.size - 1), offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) }, diff --git a/core/commonJs/test/JsJodaTimezoneTest.kt b/core/commonJs/test/JsJodaTimezoneTest.kt index 7ca8f9eca..138ed28ac 100644 --- a/core/commonJs/test/JsJodaTimezoneTest.kt +++ b/core/commonJs/test/JsJodaTimezoneTest.kt @@ -10,6 +10,8 @@ import kotlin.math.roundToInt import kotlin.test.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +import kotlinx.datetime.test.JSJoda.Instant as jtInstant +import kotlinx.datetime.test.JSJoda.ZoneId as jtZoneId class JsJodaTimezoneTest { @Test @@ -25,33 +27,35 @@ class JsJodaTimezoneTest { fun iterateOverAllTimezones() { for (id in TimeZone.availableZoneIds) { val rules = rulesForId(id) ?: throw AssertionError("No rules for $id") - val jodaZone = kotlinx.datetime.test.JSJoda.ZoneId.of(id) - assertNull(rules.recurringZoneRules) + val jodaZone = jtZoneId.of(id) + assertNull(rules.recurringZoneRules) // js-joda doesn't expose recurring rules fun checkAtInstant(instant: Instant) { - val jodaInstant = kotlinx.datetime.test.JSJoda.Instant.ofEpochMilli(instant.toEpochMilliseconds().toDouble()) - val zdt = jodaInstant.atZone(jodaZone) val offset = rules.infoAtInstant(instant) val ourLdt = instant.toLocalDateTime(offset) - val theirLdt = LocalDateTime( - zdt.year(), - zdt.monthValue(), - zdt.dayOfMonth(), - zdt.hour(), - zdt.minute(), - zdt.second(), - zdt.nano().roundToInt() - ) - if ((ourLdt.toInstant(TimeZone.UTC) - theirLdt.toInstant(TimeZone.UTC)).absoluteValue > 1.seconds) { - // It seems that sometimes, js-joda interprets its data incorrectly by at most one second, - // and we don't want to replicate that. - // Example: America/Noronha at 1914-01-01T02:09:39.998Z: - // - Computed 1913-12-31T23:59:59.998 with offset -02:09:40 - // - 1914-01-01T00:00:00.998-02:09:39[America/Noronha] is js-joda's interpretation - // The raw data representing the offset is `29.E`, which is `2 * 60 + 9 + (ord 'E' - 29) / 60`, - // and `ord 'E'` is 69, so the offset is -2:09:40. - // Thus, we allow a difference of 1 second. - throw AssertionError("Failed for $id at $instant: computed $ourLdt with offset $offset, but $zdt is correct") + val zdt = jtInstant.ofEpochMilli(instant.toEpochMilliseconds().toDouble()).atZone(jodaZone) + val theirLdt = with(zdt) { + LocalDateTime( + year(), + monthValue(), + dayOfMonth(), + hour(), + minute(), + second(), + nano().roundToInt() + ) } + // It seems that sometimes, js-joda interprets its data incorrectly by at most one second, + // and we don't want to replicate that. + // Example: America/Noronha at 1914-01-01T02:09:39.998Z: + // - Computed 1913-12-31T23:59:59.998 with offset -02:09:40 + // - 1914-01-01T00:00:00.998-02:09:39[America/Noronha] is js-joda's interpretation + // The raw data representing the offset is `29.E`, which is `2 * 60 + 9 + (ord 'E' - 29) / 60`, + // and `ord 'E'` is 69, so the offset is -2:09:40. + // Thus, we allow a difference of 1 second. + assertTrue( + (ourLdt.toInstant(TimeZone.UTC) - theirLdt.toInstant(TimeZone.UTC)).absoluteValue <= 1.seconds, + "Failed for $id at $instant: computed $ourLdt with offset $offset, but $zdt is correct" + ) } fun checkTransition(instant: Instant) { checkAtInstant(instant - 2.milliseconds) diff --git a/core/commonKotlin/src/Instant.kt b/core/commonKotlin/src/Instant.kt index 1a779606d..343404f08 100644 --- a/core/commonKotlin/src/Instant.kt +++ b/core/commonKotlin/src/Instant.kt @@ -242,12 +242,12 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT val otherLdt = other.toZonedDateTimeFailing(timeZone) val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails - thisLdt = thisLdt.plus(months.toLong(), DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid - val days = thisLdt.until(otherLdt, DateTimeUnit.DAY).toInt() // `until` on dates never fails - thisLdt = thisLdt.plus(days.toLong(), DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt + thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val days = thisLdt.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails + thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt val nanoseconds = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h - return buildDateTimePeriod(months, days, nanoseconds) + return buildDateTimePeriod(months, days.toInt(), nanoseconds) } public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = diff --git a/core/commonKotlin/src/internal/Platform.kt b/core/commonKotlin/src/internal/Platform.kt index 9f52b0b46..8227af253 100644 --- a/core/commonKotlin/src/internal/Platform.kt +++ b/core/commonKotlin/src/internal/Platform.kt @@ -8,7 +8,6 @@ package kotlinx.datetime.internal import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone -// RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId) internal expect fun timeZoneById(zoneId: String): TimeZone internal expect fun getAvailableZoneIds(): Set diff --git a/js-without-timezones/build.gradle.kts b/js-without-timezones/build.gradle.kts index 0e2b221f4..3593447f1 100644 --- a/js-without-timezones/build.gradle.kts +++ b/js-without-timezones/build.gradle.kts @@ -8,27 +8,12 @@ plugins { id("org.jetbrains.kotlinx.kover") } -val mainJavaToolchainVersion: String by project - -java { - toolchain { languageVersion.set(JavaLanguageVersion.of(mainJavaToolchainVersion)) } -} - kotlin { - js { nodejs { } - compilations.all { - kotlinOptions { - sourceMap = true - moduleKind = "umd" - metaInfo = true - } - } } - wasmJs { nodejs { } @@ -42,12 +27,6 @@ kotlin { resources.srcDir("$targetName/${suffix?.let { it + "Resources" } ?: "resources"}") } - targets.withType { - compilations["test"].kotlinOptions { - freeCompilerArgs += listOf("-trw") - } - } - sourceSets { commonMain { dependencies { diff --git a/license/README.md b/license/README.md index 62a8d071f..30a4c4c79 100644 --- a/license/README.md +++ b/license/README.md @@ -6,11 +6,11 @@ may apply: - Origin: implementation of date/time calculations is based on ThreeTen backport project. - License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp]) -- Path: `core/nativeMain/src` +- Path: `core/commonKotlin/src` - Origin: implementation of date/time entities is based on ThreeTen backport project. - License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp]) -- Path: `core/nativeTest/src` +- Path: `core/commonKotlin/src` - Origin: Derived from tests of ThreeTen backport project - License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp]) @@ -18,11 +18,7 @@ may apply: - Origin: Some tests are derived from tests of ThreeTen backport project - License: BSD 3-Clause ([license/thirdparty/threetenbp_license.txt][threetenbp]) -- Path: `thirdparty/date` - - Origin: https://github.com/HowardHinnant/date library - - License: MIT ([license/thirdparty/cppdate_license.txt](thirdparty/cppdate_license.txt)) - -- Path: `core/nativeMain/cinterop/public/windows_zones.hpp` +- Path: `core/windows/src/internal/WindowsZoneNames.kt` - Origin: time zone name mappings for Windows are generated from https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml - License: Unicode ([license/thirdparty/unicode_license.txt](thirdparty/unicode_license.txt)) @@ -31,4 +27,8 @@ may apply: - Origin: implementation is based on the bionic project. - License: BSD ([license/thirdparty/bionic_license.txt](thirdparty/bionic_license.txt)) +- Path: `core/commonJs/src` + - Origin: implementation accesses the internals of js-joda. + - License: BSD ([license/thirdparty/js-joda_license.txt](thirdparty/js-joda_license.txt)) + [threetenbp]: thirdparty/threetenbp_license.txt diff --git a/license/thirdparty/js-joda_license.txt b/license/thirdparty/js-joda_license.txt new file mode 100644 index 000000000..15646764b --- /dev/null +++ b/license/thirdparty/js-joda_license.txt @@ -0,0 +1,34 @@ +BSD License + +For js-joda software + +Copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of js-joda nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + From c75cc19cac257cd140fe9a13f9e1e2ae75dbd669 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 18 Nov 2024 12:29:14 +0100 Subject: [PATCH 09/14] Address the review --- core/common/src/DateTimePeriod.kt | 1 - core/common/src/LocalDate.kt | 2 -- core/jvm/src/LocalDate.kt | 11 +++++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 3f1abd652..978ff3dc0 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -13,7 +13,6 @@ import kotlinx.datetime.serializers.DateTimePeriodComponentSerializer import kotlin.math.* import kotlin.time.Duration import kotlinx.serialization.Serializable -import kotlin.text.toLong /** * A difference between two [instants][Instant], decomposed into date and time components. diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 02ded3244..f418462ef 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -365,8 +365,6 @@ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod * - Positive or zero if this date is later than the other. * - Exactly zero if this date is equal to the other. * - * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int. - * * @see LocalDate.periodUntil for the same operation with the order of arguments reversed. * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusDate */ diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index b817869e4..e3e1b18aa 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -84,6 +84,11 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value) public actual fun toEpochDays(): Long = value.toEpochDay() + + @PublishedApi + @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + @LowPriorityInOverloadResolution + internal fun toEpochDays(): Int = value.toEpochDay().clampToInt() } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) @@ -150,6 +155,12 @@ public actual fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased is DateTimeUnit.DayBased -> (this.value.until(other.value, ChronoUnit.DAYS) / unit.days) } +@PublishedApi +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@LowPriorityInOverloadResolution +internal fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = + until(other, unit).clampToInt() + public actual fun LocalDate.daysUntil(other: LocalDate): Int = this.value.until(other.value, ChronoUnit.DAYS).clampToInt() From 5e7501b151b8fd1016f417afcd6c3ca14ba2e047 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 18 Nov 2024 15:56:15 +0100 Subject: [PATCH 10/14] Address the review --- core/commonJs/src/internal/Platform.kt | 31 +++++++++----------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index 86bd4cf82..c7c905824 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -14,14 +14,21 @@ import kotlin.math.roundToLong private val tzdb: Result = runCatching { /** * References: + * - * - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/MomentZoneRulesProvider.js#L78-L94 * - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/unpack.js * - */ + fun charCodeToInt(char: Char): Int = when (char) { + in '0'..'9' -> char - '0' + in 'a'..'z' -> char - 'a' + 10 + in 'A'..'X' -> char - 'A' + 36 + else -> throw IllegalArgumentException("Invalid character: $char") + } fun unpackBase60(string: String): Double { var i = 0 var parts = string.split('.') - var whole = parts[0] + val whole = parts[0] var multiplier = 1.0 var out = 0.0 var sign = 1 @@ -32,14 +39,6 @@ private val tzdb: Result = runCatching { sign = -1 } - fun charCodeToInt(char: Char): Int = - when (char) { - in '0'..'9' -> char - '0' - in 'a'..'z' -> char - 'a' + 10 - in 'A'..'X' -> char - 'A' + 36 - else -> throw IllegalArgumentException("Invalid character: $char") - } - // handle digits before the decimal for (ix in i..whole.lastIndex) { out = 60 * out + charCodeToInt(whole[ix]) @@ -56,28 +55,18 @@ private val tzdb: Result = runCatching { return out * sign } - fun List.scanWithoutInitial(initial: R, operation: (acc: R, T) -> R): List = buildList { - var accumulator = initial - for (element in this@scanWithoutInitial) { - accumulator = operation(accumulator, element) - add(accumulator) - } - } - - fun List.partialSums(): List = scanWithoutInitial(0, Long::plus) - val zones = mutableMapOf() val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null for (zone in zonesPacked) { val components = zone.split('|') val offsets = components[2].split(' ').map { unpackBase60(it) } - val indices = components[3].map { it - '0' } + val indices = components[3].map { charCodeToInt(it) } val lengthsOfPeriodsWithOffsets = components[4].split(' ').map { (unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds MILLIS_PER_ONE // but we only need seconds } zones[components[0]] = TimeZoneRules( - transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take(indices.size - 1), + transitionEpochSeconds = lengthsOfPeriodsWithOffsets.runningReduce(Long::plus).take(indices.size - 1), offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) }, recurringZoneRules = null ) From b1dd16bf0f79440c874e8fd0c4f922e90a780a9c Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 18 Nov 2024 17:02:21 +0100 Subject: [PATCH 11/14] Avoid Double rounding in JS tzdb implementation --- core/commonJs/src/internal/Platform.kt | 55 ++++++++++++++------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index c7c905824..b979ccfce 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -8,8 +8,6 @@ package kotlinx.datetime.internal import kotlinx.datetime.* import kotlinx.datetime.UtcOffset import kotlinx.datetime.internal.JSJoda.ZoneId -import kotlin.math.roundToInt -import kotlin.math.roundToLong private val tzdb: Result = runCatching { /** @@ -25,49 +23,56 @@ private val tzdb: Result = runCatching { in 'A'..'X' -> char - 'A' + 36 else -> throw IllegalArgumentException("Invalid character: $char") } - fun unpackBase60(string: String): Double { + /** converts a base60 number of minutes to a whole number of seconds */ + fun base60MinutesInSeconds(string: String): Long { var i = 0 var parts = string.split('.') - val whole = parts[0] - var multiplier = 1.0 - var out = 0.0 - var sign = 1 // handle negative numbers - if (string.startsWith('-')) { + val sign = if (string.startsWith('-')) { i = 1 - sign = -1 + -1 + } else { + 1 } - // handle digits before the decimal + var wholeMinutes: Long = 0 + val whole = parts[0] + // handle digits before the decimal (whole minutes) for (ix in i..whole.lastIndex) { - out = 60 * out + charCodeToInt(whole[ix]) + wholeMinutes = 60 * wholeMinutes + charCodeToInt(whole[ix]) } - // handle digits after the decimal - parts.getOrNull(1)?.let { fractional -> - for (c in fractional) { - multiplier = multiplier / 60.0 - out += charCodeToInt(c) * multiplier + // handle digits after the decimal (seconds and less) + val seconds = parts.getOrNull(1)?.let { fractional -> + when (fractional.length) { + 1 -> charCodeToInt(fractional[0]) // single digit, representing seconds + 0 -> 0 // actually no fractional part + else -> { + charCodeToInt(fractional[0]) + charCodeToInt(fractional[1]).let { + if (it >= 30) 1 else 0 // rounding the seconds digit + } + } } - } + } ?: 0 - return out * sign + return (wholeMinutes * SECONDS_PER_MINUTE + seconds) * sign } val zones = mutableMapOf() val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null for (zone in zonesPacked) { val components = zone.split('|') - val offsets = components[2].split(' ').map { unpackBase60(it) } - val indices = components[3].map { charCodeToInt(it) } - val lengthsOfPeriodsWithOffsets = components[4].split(' ').map { - (unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds - MILLIS_PER_ONE // but we only need seconds + val offsets = components[2].split(' ').map { + UtcOffset(null, null, -base60MinutesInSeconds(it).toInt()) } + val indices = components[3].map { charCodeToInt(it) } + val lengthsOfPeriodsWithOffsets = components[4].split(' ').map(::base60MinutesInSeconds) zones[components[0]] = TimeZoneRules( - transitionEpochSeconds = lengthsOfPeriodsWithOffsets.runningReduce(Long::plus).take(indices.size - 1), - offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) }, + transitionEpochSeconds = lengthsOfPeriodsWithOffsets.runningReduce(Long::plus).let { + if (it.size == indices.size - 1) it else it.take(indices.size - 1) + }, + offsets = indices.map { offsets[it] }, recurringZoneRules = null ) } From f1d0d269a8a9fbc671f83ec5e8ecb46800334b50 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 19 Nov 2024 08:45:13 +0100 Subject: [PATCH 12/14] Address the review --- core/common/test/InstantTest.kt | 6 ++++++ core/commonKotlin/src/Instant.kt | 7 ++----- core/commonKotlin/src/TimeZone.kt | 6 +----- core/wasmJs/src/PlatformSpecifics.kt | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/core/common/test/InstantTest.kt b/core/common/test/InstantTest.kt index 171796260..c521fae6f 100644 --- a/core/common/test/InstantTest.kt +++ b/core/common/test/InstantTest.kt @@ -559,6 +559,12 @@ class InstantRangeTest { assertArithmeticFails("$instant") { instant.plus(Long.MAX_VALUE, DateTimeUnit.YEAR, UTC) } assertArithmeticFails("$instant") { instant.plus(Long.MIN_VALUE, DateTimeUnit.YEAR, UTC) } } + for (instant in smallInstants) { + instant.plus(2 * Int.MAX_VALUE.toLong(), DateTimeUnit.DAY, UTC) + instant.plus(2 * Int.MIN_VALUE.toLong(), DateTimeUnit.DAY, UTC) + instant.plus(2 * Int.MAX_VALUE.toLong(), DateTimeUnit.MONTH, UTC) + instant.plus(2 * Int.MIN_VALUE.toLong(), DateTimeUnit.MONTH, UTC) + } // Overflowing a LocalDateTime in input maxValidInstant.plus(-1, DateTimeUnit.NANOSECOND, UTC) minValidInstant.plus(1, DateTimeUnit.NANOSECOND, UTC) diff --git a/core/commonKotlin/src/Instant.kt b/core/commonKotlin/src/Instant.kt index 343404f08..86a5a34b2 100644 --- a/core/commonKotlin/src/Instant.kt +++ b/core/commonKotlin/src/Instant.kt @@ -192,7 +192,7 @@ private fun Instant.check(zone: TimeZone): Instant = this@check.also { public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { val withDate = toZonedDateTimeFailing(timeZone) - .run { if (totalMonths != 0L) plus(totalMonths.toLong(), DateTimeUnit.MONTH) else this } + .run { if (totalMonths != 0L) plus(totalMonths, DateTimeUnit.MONTH) else this } .run { if (days != 0) plus(days.toLong(), DateTimeUnit.DAY) else this } withDate.toInstant() .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } @@ -212,11 +212,8 @@ public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZo plus(-value.toLong(), unit, timeZone) public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try { when (unit) { - is DateTimeUnit.DateBased -> { - if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) - throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") + is DateTimeUnit.DateBased -> toZonedDateTimeFailing(timeZone).plus(value, unit).toInstant() - } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) } diff --git a/core/commonKotlin/src/TimeZone.kt b/core/commonKotlin/src/TimeZone.kt index 70cca29b2..4aa6aa5e3 100644 --- a/core/commonKotlin/src/TimeZone.kt +++ b/core/commonKotlin/src/TimeZone.kt @@ -21,11 +21,7 @@ public actual open class TimeZone internal constructor() { public actual fun currentSystemDefault(): TimeZone { // TODO: probably check if currentSystemDefault name is parseable as FixedOffsetTimeZone? val (name, zone) = currentSystemDefaultZone() - return if (zone == null) { - of(name) - } else { - zone - } + return zone ?: of(name) } public actual val UTC: FixedOffsetTimeZone = UtcOffset.ZERO.asTimeZone() diff --git a/core/wasmJs/src/PlatformSpecifics.kt b/core/wasmJs/src/PlatformSpecifics.kt index 02bbd7ef5..459c2a81d 100644 --- a/core/wasmJs/src/PlatformSpecifics.kt +++ b/core/wasmJs/src/PlatformSpecifics.kt @@ -4,7 +4,6 @@ */ package kotlinx.datetime.internal -import kotlinx.datetime.internal.JSJoda.ZoneId import kotlinx.datetime.internal.JSJoda.ZoneRulesProvider import kotlin.js.unsafeCast From 0b5a77f92a0d17f65262ffc949479ee93cf396be Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 19 Nov 2024 15:25:04 +0100 Subject: [PATCH 13/14] Address the review --- core/commonJs/src/internal/Platform.kt | 21 ++++++++++--------- .../commonKotlin/src/internal/MonthDayTime.kt | 3 +-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index b979ccfce..c5ba347b7 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -25,22 +25,23 @@ private val tzdb: Result = runCatching { } /** converts a base60 number of minutes to a whole number of seconds */ fun base60MinutesInSeconds(string: String): Long { - var i = 0 - var parts = string.split('.') + val parts = string.split('.') // handle negative numbers - val sign = if (string.startsWith('-')) { - i = 1 - -1 + val sign: Int + val minuteNumberStart: Int + if (string.startsWith('-')) { + minuteNumberStart = 1 + sign = -1 } else { - 1 + minuteNumberStart = 0 + sign = 1 } - var wholeMinutes: Long = 0 - val whole = parts[0] // handle digits before the decimal (whole minutes) - for (ix in i..whole.lastIndex) { - wholeMinutes = 60 * wholeMinutes + charCodeToInt(whole[ix]) + val whole = parts[0] + val wholeMinutes: Long = (minuteNumberStart..whole.lastIndex).map { charCodeToInt(whole[it]) }.fold(0L) { + acc, digit -> 60 * acc + digit } // handle digits after the decimal (seconds and less) diff --git a/core/commonKotlin/src/internal/MonthDayTime.kt b/core/commonKotlin/src/internal/MonthDayTime.kt index 5ac4f7b08..65d331dd9 100644 --- a/core/commonKotlin/src/internal/MonthDayTime.kt +++ b/core/commonKotlin/src/internal/MonthDayTime.kt @@ -28,12 +28,11 @@ internal interface DateOfYear { /** * The day of year, in the 0..365 range. During leap years, 29th February is counted as the 60th day of the year. - * The number 366 is not supported, as outside the leap years, there are only 365 days in a year. */ internal class JulianDayOfYear(val zeroBasedDayOfYear: Int) : DateOfYear { init { require(zeroBasedDayOfYear in 0..365) { - "Expected a value in 1..365 for the Julian day-of-year, but got $zeroBasedDayOfYear" + "Expected a value in 0..365 for the Julian day-of-year, but got $zeroBasedDayOfYear" } } override fun toLocalDate(year: Int): LocalDate = From c01263f8f1ecd1e0d25c2d3c01918691b9a20f0b Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 9 Dec 2024 14:14:15 +0100 Subject: [PATCH 14/14] Address the review --- core/common/src/DateTimePeriod.kt | 6 +++--- core/common/src/LocalDate.kt | 4 ++-- core/common/test/InstantTest.kt | 15 ++++++++------- settings.gradle.kts | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 978ff3dc0..756c9fbfc 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -465,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 - override val totalNanoseconds: Long get() = 0 + internal override val totalNanoseconds: Long get() = 0 public companion object { /** @@ -495,9 +495,9 @@ public class DatePeriod internal constructor( public fun String.toDatePeriod(): DatePeriod = DatePeriod.parse(this) private class DateTimePeriodImpl( - override val totalMonths: Long, + internal override val totalMonths: Long, override val days: Int, - override val totalNanoseconds: Long, + internal override val totalNanoseconds: Long, ) : DateTimePeriod() private fun totalMonths(years: Int, months: Int): Long = (years.toLong() * 12 + months.toLong()).also { diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index f418462ef..e9841575f 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.Serializable * is `2020-08-31` everywhere): see various [LocalDate.plus] and [LocalDate.minus] functions, as well * as [LocalDate.periodUntil] and various other [*until][LocalDate.daysUntil] functions. * - * The range of supported years is at least is enough to represent dates of all instants between + * The range of supported years is at least enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. * * ### Arithmetic operations @@ -168,7 +168,7 @@ public expect class LocalDate : Comparable { * The components [monthNumber] and [dayOfMonth] are 1-based. * * The supported ranges of components: - * - [year] the range is at least is enough to represent dates of all instants between + * - [year] the range is at least enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [monthNumber] `1..12` * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month diff --git a/core/common/test/InstantTest.kt b/core/common/test/InstantTest.kt index c521fae6f..8bc41f019 100644 --- a/core/common/test/InstantTest.kt +++ b/core/common/test/InstantTest.kt @@ -313,9 +313,7 @@ class InstantTest { val diff2 = date1.periodUntil(date2) if (diff1 != diff2) - throw AssertionError( - "start: $instant1, end: $instant2, diff by instants: $diff1, diff by dates: $diff2" - ) + fail("start: $instant1, end: $instant2, diff by instants: $diff1, diff by dates: $diff2") } } } @@ -560,10 +558,13 @@ class InstantRangeTest { assertArithmeticFails("$instant") { instant.plus(Long.MIN_VALUE, DateTimeUnit.YEAR, UTC) } } for (instant in smallInstants) { - instant.plus(2 * Int.MAX_VALUE.toLong(), DateTimeUnit.DAY, UTC) - instant.plus(2 * Int.MIN_VALUE.toLong(), DateTimeUnit.DAY, UTC) - instant.plus(2 * Int.MAX_VALUE.toLong(), DateTimeUnit.MONTH, UTC) - instant.plus(2 * Int.MIN_VALUE.toLong(), DateTimeUnit.MONTH, UTC) + fun roundTrip(value: Long, unit: DateTimeUnit) { + assertEquals(instant, instant.plus(value, unit, UTC).minus(value, unit, UTC)) + } + roundTrip(2L * Int.MAX_VALUE, DateTimeUnit.DAY) + roundTrip(2L * Int.MIN_VALUE, DateTimeUnit.DAY) + roundTrip(2L * Int.MAX_VALUE, DateTimeUnit.MONTH) + roundTrip(2L * Int.MIN_VALUE, DateTimeUnit.MONTH) } // Overflowing a LocalDateTime in input maxValidInstant.plus(-1, DateTimeUnit.NANOSECOND, UTC) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a9b35c10..a342b81fd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,5 +21,5 @@ project(":timezones/full").name = "kotlinx-datetime-zoneinfo" include(":serialization") project(":serialization").name = "kotlinx-datetime-serialization" include(":js-without-timezones") -project(":js-without-timezones").name = "kotlinx-datetime-js-without-timezones" +project(":js-without-timezones").name = "kotlinx-datetime-js-test-without-timezones" include(":benchmarks")