diff --git a/core/commonMain/src/DateTimePeriod.kt b/core/commonMain/src/DateTimePeriod.kt index f444a1e31..bc52a732d 100644 --- a/core/commonMain/src/DateTimePeriod.kt +++ b/core/commonMain/src/DateTimePeriod.kt @@ -110,18 +110,18 @@ fun Duration.toDateTimePeriod(): DateTimePeriod = toComponents { hours, minutes, } operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = DateTimePeriod( - this.years + other.years, - this.months + other.months, - this.days + other.days, - this.hours + other.hours, - this.minutes + other.minutes, - this.seconds + other.seconds, - this.nanoseconds + other.nanoseconds + safeAdd(this.years, other.years), + safeAdd(this.months, other.months), + safeAdd(this.days, other.days), + safeAdd(this.hours, other.hours), + safeAdd(this.minutes, other.minutes), + safeAdd(this.seconds, other.seconds), + safeAdd(this.nanoseconds, other.nanoseconds) ) operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( - this.years + other.years, - this.months + other.months, - this.days + other.days + safeAdd(this.years, other.years), + safeAdd(this.months, other.months), + safeAdd(this.days, other.days) ) diff --git a/core/commonMain/src/DateTimeUnit.kt b/core/commonMain/src/DateTimeUnit.kt index addefd467..4b62ba661 100644 --- a/core/commonMain/src/DateTimeUnit.kt +++ b/core/commonMain/src/DateTimeUnit.kt @@ -50,7 +50,7 @@ sealed class DateTimeUnit { } } - override fun times(scalar: Int): TimeBased = TimeBased(nanoseconds * scalar) // TODO: prevent overflow + override fun times(scalar: Int): TimeBased = TimeBased(safeMultiply(nanoseconds, scalar.toLong())) @ExperimentalTime val duration: Duration = nanoseconds.nanoseconds @@ -70,7 +70,7 @@ sealed class DateTimeUnit { require(days > 0) { "Unit duration must be positive, but was $days days." } } - override fun times(scalar: Int): DayBased = DayBased(days * scalar) + override fun times(scalar: Int): DayBased = DayBased(safeMultiply(days, scalar)) internal override val calendarUnit: CalendarUnit get() = CalendarUnit.DAY internal override val calendarScale: Long get() = days.toLong() @@ -90,7 +90,7 @@ sealed class DateTimeUnit { require(months > 0) { "Unit duration must be positive, but was $months months." } } - override fun times(scalar: Int): MonthBased = MonthBased(months * scalar) + override fun times(scalar: Int): MonthBased = MonthBased(safeMultiply(months, scalar)) internal override val calendarUnit: CalendarUnit get() = CalendarUnit.MONTH internal override val calendarScale: Long get() = months.toLong() diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index 8164b2441..35f7891a0 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -62,6 +62,9 @@ public expect class Instant : Comparable { * @throws DateTimeFormatException if the text cannot be parsed or the boundaries of [Instant] are exceeded. */ fun parse(isoString: String): Instant + + internal val MIN: Instant + internal val MAX: Instant } } @@ -121,10 +124,6 @@ public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = until(other, DateTimeUnit.YEAR, zone).clampToInt() -// TODO: move to internal utils -internal fun Long.clampToInt(): Int = - if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() - public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other.periodUntil(this, zone) @@ -134,18 +133,25 @@ public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other public fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant = plus(unit.calendarScale, unit.calendarUnit, zone) -// TODO: safeMultiply /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant = - plus(value * unit.calendarScale, unit.calendarUnit, zone) + try { + plus(safeMultiply(value.toLong(), unit.calendarScale), unit.calendarUnit, zone) + } catch (e: ArithmeticException) { + throw DateTimeArithmeticException(e) + } /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = - plus(value * unit.calendarScale, unit.calendarUnit, zone) + try { + plus(safeMultiply(value, unit.calendarScale), unit.calendarUnit, zone) + } catch (e: ArithmeticException) { + throw DateTimeArithmeticException(e) + } public fun Instant.minus(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index 652543c6a..5c84caf7a 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -11,6 +11,9 @@ public expect class LocalDate : Comparable { * @throws DateTimeFormatException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded. */ public fun parse(isoString: String): LocalDate + + internal val MIN: LocalDate + internal val MAX: LocalDate } /** @@ -66,9 +69,19 @@ public expect fun LocalDate.yearsUntil(other: LocalDate): Int public fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plus(unit.calendarScale, unit.calendarUnit) public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = - plus(value * unit.calendarScale, unit.calendarUnit) + try { + plus(safeMultiply(value.toLong(), unit.calendarScale), unit.calendarUnit) + } catch (e: Exception) { + if (e !is ArithmeticException) throw e + throw DateTimeArithmeticException(e) + } public fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = - plus(value * unit.calendarScale, unit.calendarUnit) + try { + plus(safeMultiply(value, unit.calendarScale), unit.calendarUnit) + } catch (e: Exception) { + if (e !is ArithmeticException) throw e + throw DateTimeArithmeticException(e) + } public fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { is DateTimeUnit.DateBased.MonthBased -> (monthsUntil(other) / unit.months).toInt() diff --git a/core/commonMain/src/LocalDateTime.kt b/core/commonMain/src/LocalDateTime.kt index b995d7e16..f5b49bc66 100644 --- a/core/commonMain/src/LocalDateTime.kt +++ b/core/commonMain/src/LocalDateTime.kt @@ -14,6 +14,9 @@ public expect class LocalDateTime : Comparable { * exceeded. */ public fun parse(isoString: String): LocalDateTime + + internal val MIN: LocalDateTime + internal val MAX: LocalDateTime } /** diff --git a/core/commonMain/src/math.kt b/core/commonMain/src/math.kt new file mode 100644 index 000000000..2f58865c4 --- /dev/null +++ b/core/commonMain/src/math.kt @@ -0,0 +1,19 @@ +/* + * 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 fun Long.clampToInt(): Int = + when { + this > Int.MAX_VALUE -> Int.MAX_VALUE + this < Int.MIN_VALUE -> Int.MIN_VALUE + else -> toInt() + } + + +internal expect fun safeMultiply(a: Long, b: Long): Long +internal expect fun safeMultiply(a: Int, b: Int): Int +internal expect fun safeAdd(a: Long, b: Long): Long +internal expect fun safeAdd(a: Int, b: Int): Int diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index 803ce7199..c88665741 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -57,7 +57,7 @@ class InstantTest { * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ @Test - fun instantParsing() { + fun parseIsoString() { val instants = arrayOf( Triple("1970-01-01T00:00:00Z", 0, 0), Triple("1970-01-01t00:00:00Z", 0, 0), @@ -78,8 +78,12 @@ class InstantTest { val instant = Instant.parse(str) assertEquals(seconds.toLong() * 1000 + nanos / 1000000, instant.toEpochMilliseconds()) } - } + assertInvalidFormat { Instant.parse("x") } + assertInvalidFormat { Instant.parse("12020-12-31T23:59:59.000000000Z") } + // this string represents an Instant that is currently larger than Instant.MAX any of the implementations: + assertInvalidFormat { Instant.parse("+1000000001-12-31T23:59:59.000000000Z") } + } @OptIn(ExperimentalTime::class) @Test @@ -133,6 +137,29 @@ class InstantTest { assertEquals(0, instant6.minus(instant1, DateTimeUnit.DAY, zone)) } + @OptIn(ExperimentalTime::class) + @Test + fun unitMultiplesUntil() { + val unit1000days = DateTimeUnit.DAY * 1000 + val unit4years = DateTimeUnit.YEAR * 4 // longer than 1000-DAY + + val zone = TimeZone.UTC + val min = LocalDateTime.MIN.toInstant(zone) + val max = LocalDateTime.MAX.toInstant(zone) + val diffDays = min.until(max, unit1000days, zone) + val diffYears = min.until(max, unit4years, zone) + assertTrue(diffDays in 0..Int.MAX_VALUE, "difference in $unit1000days should fit in Int, was $diffDays") + assertTrue(diffDays > diffYears, "difference in $unit1000days unit must be more than in $unit4years unit, was $diffDays $diffYears") + + val unit500ns = DateTimeUnit.NANOSECOND * 500 + val start = Instant.parse("1700-01-01T00:00:00Z") + val end = start.plus(300, DateTimeUnit.YEAR, zone) + val diffNs = start.until(end, unit500ns, zone) + val diffUs = start.until(end, DateTimeUnit.MICROSECOND, zone) + // TODO: avoid clamping/overflowing in intermediate results +// assertEquals(diffUs * 2, diffNs) + } + @OptIn(ExperimentalTime::class) @Test fun instantOffset() { @@ -297,3 +324,173 @@ class InstantTest { } } + +@OptIn(ExperimentalTime::class) +class InstantRangeTest { + private val UTC = TimeZone.UTC + private val maxValidInstant = LocalDateTime.MAX.toInstant(UTC) + private val minValidInstant = LocalDateTime.MIN.toInstant(UTC) + + private val largePositiveLongs = listOf(Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 50) + private val largeNegativeLongs = listOf(Long.MIN_VALUE, Long.MIN_VALUE + 1, Long.MIN_VALUE + 50) + + private val largePositiveInstants = listOf(Instant.MAX, Instant.MAX - 1.seconds, Instant.MAX - 50.seconds) + private val largeNegativeInstants = listOf(Instant.MIN, Instant.MIN + 1.seconds, Instant.MIN + 50.seconds) + + private val smallInstants = listOf( + Instant.fromEpochMilliseconds(0), + Instant.fromEpochMilliseconds(1003), + Instant.fromEpochMilliseconds(253112) + ) + + + @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") + } + } + } + + @Test + fun epochSecondsClamping() { + // fromEpochSeconds + // On all platforms Long.MAX_VALUE of seconds is not a valid instant. + for (seconds in largePositiveLongs) { + assertEquals(Instant.MAX, Instant.fromEpochSeconds(seconds, 35)) + } + for (seconds in largeNegativeLongs) { + assertEquals(Instant.MIN, Instant.fromEpochSeconds(seconds, 35)) + } + for (instant in largePositiveInstants + smallInstants + largeNegativeInstants) { + assertEquals(instant, Instant.fromEpochSeconds(instant.epochSeconds, instant.nanosecondsOfSecond.toLong())) + } + } + + @Test + fun durationArithmeticClamping() { + val longDurations = listOf(Duration.INFINITE, Double.MAX_VALUE.nanoseconds, Long.MAX_VALUE.seconds) + + for (duration in longDurations) { + for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) { + assertEquals(Instant.MAX, instant + duration) + } + for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) { + assertEquals(Instant.MIN, instant - duration) + } + } + assertEquals(Instant.MAX, (Instant.MAX - 4.seconds) + 5.seconds) + assertEquals(Instant.MIN, (Instant.MIN + 10.seconds) - 12.seconds) + } + + @Test + fun periodArithmeticOutOfRange() { + // Instant.plus(DateTimePeriod(), TimeZone) + // Arithmetic overflow + for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) { + assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MAX_VALUE), UTC) } + assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MIN_VALUE), UTC) } + } + // Overflowing a LocalDateTime in input + maxValidInstant.plus(DateTimePeriod(nanoseconds = -1), UTC) + minValidInstant.plus(DateTimePeriod(nanoseconds = 1), UTC) + assertArithmeticFails { (maxValidInstant + 1.nanoseconds).plus(DateTimePeriod(nanoseconds = -2), UTC) } + assertArithmeticFails { (minValidInstant - 1.nanoseconds).plus(DateTimePeriod(nanoseconds = 2), UTC) } + // Overflowing a LocalDateTime in result + assertArithmeticFails { maxValidInstant.plus(DateTimePeriod(nanoseconds = 1), UTC) } + assertArithmeticFails { minValidInstant.plus(DateTimePeriod(nanoseconds = -1), UTC) } + // Overflowing a LocalDateTime in intermediate computations + assertArithmeticFails { maxValidInstant.plus(DateTimePeriod(seconds = 1, nanoseconds = -1_000_000_001), UTC) } + assertArithmeticFails { maxValidInstant.plus(DateTimePeriod(hours = 1, minutes = -61), UTC) } + assertArithmeticFails { maxValidInstant.plus(DateTimePeriod(days = 1, hours = -48), UTC) } + } + + @Test + fun unitArithmeticOutOfRange() { + // Instant.plus(Long, DateTimeUnit, TimeZone) + // Arithmetic overflow + for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) { + assertArithmeticFails("$instant") { instant.plus(Long.MAX_VALUE, DateTimeUnit.SECOND, UTC) } + assertArithmeticFails("$instant") { instant.plus(Long.MIN_VALUE, DateTimeUnit.SECOND, UTC) } + assertArithmeticFails("$instant") { instant.plus(Long.MAX_VALUE, DateTimeUnit.YEAR, UTC) } + assertArithmeticFails("$instant") { instant.plus(Long.MIN_VALUE, DateTimeUnit.YEAR, UTC) } + } + // Overflowing a LocalDateTime in input + maxValidInstant.plus(-1, DateTimeUnit.NANOSECOND, UTC) + minValidInstant.plus(1, DateTimeUnit.NANOSECOND, UTC) + assertArithmeticFails { (maxValidInstant + 1.nanoseconds).plus(-2, DateTimeUnit.NANOSECOND, UTC) } + assertArithmeticFails { (minValidInstant - 1.nanoseconds).plus(2, DateTimeUnit.NANOSECOND, UTC) } + // Overflowing a LocalDateTime in result + assertArithmeticFails { maxValidInstant.plus(1, DateTimeUnit.NANOSECOND, UTC) } + assertArithmeticFails { maxValidInstant.plus(1, DateTimeUnit.YEAR, UTC) } + assertArithmeticFails { minValidInstant.plus(-1, DateTimeUnit.NANOSECOND, UTC) } + assertArithmeticFails { minValidInstant.plus(-1, DateTimeUnit.YEAR, UTC) } + } + + @Test + fun periodUntilOutOfRange() { + // Instant.periodUntil + maxValidInstant.periodUntil(minValidInstant, UTC) + assertArithmeticFails { (maxValidInstant + 1.nanoseconds).periodUntil(minValidInstant, UTC) } + assertArithmeticFails { maxValidInstant.periodUntil(minValidInstant - 1.nanoseconds, UTC) } + } + + @Test + fun unitsUntilClamping() { + // Arithmetic overflow of the resulting number + assertEquals(Long.MAX_VALUE, minValidInstant.until(maxValidInstant, DateTimeUnit.NANOSECOND, UTC)) + assertEquals(Long.MIN_VALUE, maxValidInstant.until(minValidInstant, DateTimeUnit.NANOSECOND, UTC)) + } + + @Test + fun unitsUntilOutOfRange() { + // Instant.until + // Overflowing a LocalDateTime in input + assertArithmeticFails { (maxValidInstant + 1.nanoseconds).until(maxValidInstant, DateTimeUnit.NANOSECOND, UTC) } + assertArithmeticFails { maxValidInstant.until(maxValidInstant + 1.nanoseconds, DateTimeUnit.NANOSECOND, UTC) } + } +} + + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@kotlin.internal.InlineOnly +inline fun assertArithmeticFails(message: String? = null, f: () -> T) { + assertFailsWith(message) { + val result = f() + fail(result.toString()) + } +} + +@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") +@kotlin.internal.InlineOnly +inline fun assertInvalidFormat(message: String? = null, f: () -> T) { + assertFailsWith(message) { + val result = f() + fail(result.toString()) + } +} + diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index 9d4bc1b74..a8ccd9170 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -35,18 +35,20 @@ class LocalDateTest { } @Test - fun localDateParsing() { + fun parseIsoString() { fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int, dayOfYear: Int) { checkComponents(LocalDate.parse(value), year, month, day, dayOfWeek, dayOfYear) } checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274) checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60) - checkParsedComponents("2017-10-01", 2017, 10, 1, 7, 274) - assertFailsWith { LocalDate.parse("102017-10-01") } - assertFailsWith { LocalDate.parse("2017--10-01") } - assertFailsWith { LocalDate.parse("2017-+10-01") } - assertFailsWith { LocalDate.parse("2017-10-+01") } - assertFailsWith { LocalDate.parse("2017-10--01") } + checkParsedComponents("2017-10-01", 2017, 10, 1, 7, 274) + assertInvalidFormat { LocalDate.parse("102017-10-01") } + assertInvalidFormat { LocalDate.parse("2017--10-01") } + assertInvalidFormat { LocalDate.parse("2017-+10-01") } + assertInvalidFormat { LocalDate.parse("2017-10-+01") } + assertInvalidFormat { LocalDate.parse("2017-10--01") } + // this date is currently larger than the largest representable one any of the platforms: + assertInvalidFormat { LocalDate.parse("+1000000000-10-01") } } @Test @@ -65,7 +67,7 @@ class LocalDateTest { checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29) -// assertFailsWith { startDate + CalendarPeriod(hours = 7) } // won't compile +// assertFailsWith { startDate + DateTimePeriod(hours = 7) } // won't compile // assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile } @@ -99,7 +101,7 @@ class LocalDateTest { // based on threetenbp test for until() @Test - fun until() { + fun unitsUntil() { val data = listOf( Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.DAY, 0)), Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.WEEK, 0)), @@ -135,16 +137,72 @@ class LocalDateTest { } @Test - fun invalidDate() { - assertFailsWith { LocalDate(2007, 2, 29) } - LocalDate(2008, 2, 29) - assertFailsWith { LocalDate(2007, 4, 31) } - assertFailsWith { LocalDate(2007, 1, 0) } - assertFailsWith { LocalDate(2007,1, 32) } - assertFailsWith { LocalDate(Int.MIN_VALUE, 1, 1) } - assertFailsWith { LocalDate(2007, 1, 32) } - assertFailsWith { LocalDate(2007, 0, 1) } - assertFailsWith { LocalDate(2007, 13, 1) } + fun unitMultiplesUntil() { + val unit1000days = DateTimeUnit.DAY * 1000 + val unit4years = DateTimeUnit.YEAR * 4 // longer than 1000-DAY + + val diffDays = LocalDate.MIN.until(LocalDate.MAX, unit1000days) + val diffYears = LocalDate.MIN.until(LocalDate.MAX, unit4years) + assertTrue(diffDays in 0..Int.MAX_VALUE, "difference in $unit1000days should fit in Int, was $diffDays") + // TODO: make pass in JVM + // assertTrue(diffDays > diffYears, "difference in $unit1000days unit must be more than in $unit4years unit, was $diffDays $diffYears") } + @Test + fun constructInvalidDate() = checkInvalidDate(::LocalDate) + + @Test + fun unitArithmeticOutOfRange() { + // LocalDate.plus(Long, DateTimeUnit) + LocalDate.MAX.plus(-1, DateTimeUnit.DAY) + LocalDate.MIN.plus(1, DateTimeUnit.DAY) + // Arithmetic overflow + assertArithmeticFails { LocalDate.MAX.plus(Long.MAX_VALUE, DateTimeUnit.YEAR) } + assertArithmeticFails { LocalDate.MAX.plus(Long.MAX_VALUE - 2, DateTimeUnit.YEAR) } + assertArithmeticFails { LocalDate.MIN.plus(Long.MIN_VALUE, DateTimeUnit.YEAR) } + assertArithmeticFails { LocalDate.MIN.plus(Long.MIN_VALUE + 2, DateTimeUnit.YEAR) } + assertArithmeticFails { LocalDate.MIN.plus(Long.MAX_VALUE, DateTimeUnit.DAY) } + // Exceeding the boundaries of LocalDate + assertArithmeticFails { LocalDate.MAX.plus(1, DateTimeUnit.YEAR) } + assertArithmeticFails { LocalDate.MIN.plus(-1, DateTimeUnit.YEAR) } + } + + @Test + fun periodArithmeticOutOfRange() { + // LocalDate.plus(DatePeriod) + LocalDate.MAX.plus(DatePeriod(years = -2, months = 12, days = 31)) + // Exceeding the boundaries in result + assertArithmeticFails { + LocalDate.MAX.plus(DatePeriod(years = -2, months = 24, days = 1)) + } + // Exceeding the boundaries in intermediate computations + assertArithmeticFails { + LocalDate.MAX.plus(DatePeriod(years = -2, months = 25, days = -1000)) + } + } + + @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)) + } + } } + + + +fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate) { + assertFailsWith { constructor(2007, 2, 29) } + constructor(2008, 2, 29).let { date -> + assertEquals(29, date.dayOfMonth) + } + assertFailsWith { constructor(2007, 4, 31) } + assertFailsWith { constructor(2007, 1, 0) } + assertFailsWith { constructor(2007,1, 32) } + assertFailsWith { constructor(Int.MIN_VALUE, 1, 1) } + assertFailsWith { constructor(2007, 1, 32) } + assertFailsWith { constructor(2007, 0, 1) } + assertFailsWith { constructor(2007, 13, 1) } +} \ No newline at end of file diff --git a/core/commonTest/src/LocalDateTimeTest.kt b/core/commonTest/src/LocalDateTimeTest.kt index 18f6106ce..2acb59b9f 100644 --- a/core/commonTest/src/LocalDateTimeTest.kt +++ b/core/commonTest/src/LocalDateTimeTest.kt @@ -23,6 +23,9 @@ class LocalDateTimeTest { checkParsedComponents("2019-10-01T18:43:15", 2019, 10, 1, 18, 43, 15, 0, 2, 274) checkParsedComponents("2019-10-01T18:12", 2019, 10, 1, 18, 12, 0, 0, 2, 274) + assertFailsWith { LocalDateTime.parse("x") } + assertFailsWith { "+1000000000-03-26T04:00:00".toLocalDateTime() } + /* Based on the ThreeTenBp project. * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ @@ -41,6 +44,8 @@ class LocalDateTimeTest { val diff = with(TimeZone.UTC) { ldt2.toInstant() - ldt1.toInstant() } assertEquals(1.hours + 7.minutes - 15.seconds + 400100.microseconds, diff) + assertFailsWith { (Instant.MAX - 3.days).toLocalDateTime(TimeZone.UTC) } + assertFailsWith { (Instant.MIN + 6.hours).toLocalDateTime(TimeZone.UTC) } } @OptIn(ExperimentalTime::class) @@ -57,6 +62,7 @@ class LocalDateTimeTest { val instant = Instant.parse("2019-10-01T18:43:15.100500Z") val datetime = instant.toLocalDateTime(TimeZone.UTC) checkComponents(datetime, 2019, 10, 1, 18, 43, 15, 100500000) + assertFailsWith { Instant.MAX.toLocalDateTime(TimeZone.UTC) } } @Test @@ -95,18 +101,21 @@ class LocalDateTimeTest { } @Test - fun invalidTime() { + fun constructInvalidDate() = checkInvalidDate { year, month, day -> LocalDateTime(year, month, day, 0, 0, 0, 0).date } + + @Test + fun constructInvalidTime() { fun localTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalDateTime = LocalDateTime(2020, 1, 1, hour, minute, second, nanosecond) localTime(23, 59) - assertFailsWith { localTime(-1, 0) } - assertFailsWith { localTime(24, 0) } - assertFailsWith { localTime(0, -1) } - assertFailsWith { localTime(0, 60) } - assertFailsWith { localTime(0, 0, -1) } - assertFailsWith { localTime(0, 0, 60) } - assertFailsWith { localTime(0, 0, 0, -1) } - assertFailsWith { localTime(0, 0, 0, 1_000_000_000) } + assertFailsWith { localTime(-1, 0) } + assertFailsWith { localTime(24, 0) } + assertFailsWith { localTime(0, -1) } + assertFailsWith { localTime(0, 60) } + assertFailsWith { localTime(0, 0, -1) } + assertFailsWith { localTime(0, 0, 60) } + assertFailsWith { localTime(0, 0, 0, -1) } + assertFailsWith { localTime(0, 0, 0, 1_000_000_000) } } } diff --git a/core/commonTest/src/TimeZoneTest.kt b/core/commonTest/src/TimeZoneTest.kt index b885e0682..f9a934b5b 100644 --- a/core/commonTest/src/TimeZoneTest.kt +++ b/core/commonTest/src/TimeZoneTest.kt @@ -53,8 +53,8 @@ class TimeZoneTest { assertEquals("Europe/Moscow", tzm.id) // TODO: Check known offsets from UTC for particular moments - // TODO: assert exception type? - assertFails { TimeZone.of("Mars/Standard") } + assertFailsWith { TimeZone.of("Mars/Standard") } + assertFailsWith { TimeZone.of("UTC+X") } } diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index aaa972ce5..361c57f10 100644 --- a/core/jsMain/src/Instant.kt +++ b/core/jsMain/src/Instant.kt @@ -14,7 +14,9 @@ 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 -import kotlinx.datetime.internal.JSJoda.ZoneId +import kotlinx.datetime.internal.JSJoda.LocalTime +import kotlin.math.nextTowards +import kotlin.math.truncate @OptIn(kotlin.time.ExperimentalTime::class) public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { @@ -24,10 +26,23 @@ public actual class Instant internal constructor(internal val value: jtInstant) actual val nanosecondsOfSecond: Int get() = value.nano().toInt() - public actual fun toEpochMilliseconds(): Long = value.toEpochMilli().toLong() + public actual fun toEpochMilliseconds(): Long = epochSeconds * 1000 + nanosecondsOfSecond / 1_000_000 + + actual operator fun plus(duration: Duration): Instant { + val addSeconds = truncate(duration.inSeconds) + val addNanos = (duration.inNanoseconds % 1e9).toInt() + return try { + Instant(plusFix(addSeconds, addNanos)) + } catch (e: Throwable) { + if (!e.isJodaDateTimeException()) throw e + if (addSeconds > 0) MAX else MIN + } + } - actual operator fun plus(duration: Duration): Instant = duration.toComponents { seconds, nanoseconds -> - Instant(value.plusSeconds(seconds).plusNanos(nanoseconds.toLong())) + internal fun plusFix(seconds: Double, nanos: Int): jtInstant { + val newSeconds = value.epochSecond().toDouble() + seconds + val newNanos = value.nano().toDouble() + nanos + return jtInstant.ofEpochSecond(newSeconds, newNanos) } actual operator fun minus(duration: Duration): Instant = plus(-duration) @@ -51,47 +66,98 @@ public actual class Instant internal constructor(internal val value: jtInstant) actual fun now(): Instant = Instant(jtClock.systemUTC().instant()) - actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = - Instant(jtInstant.ofEpochMilli(epochMilliseconds.toDouble())) - - actual fun parse(isoString: String): Instant = - Instant(jtInstant.parse(isoString)) - - actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = - Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment)) + actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = try { + fromEpochSeconds(epochMilliseconds / 1000, epochMilliseconds % 1000 * 1000_000) + } catch (e: Throwable) { + if (!e.isJodaDateTimeException()) throw e + if (epochMilliseconds > 0) MAX else MIN + } + + actual fun parse(isoString: String): Instant = try { + Instant(jtInstant.parse(isoString)) + } catch (e: Throwable) { + if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) + throw e + } + + actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = try { + Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment)) + } catch (e: Throwable) { + if (!e.isJodaDateTimeException()) throw e + if (epochSeconds > 0) MAX else MIN + } + + internal actual val MIN: Instant = Instant(jtInstant.MIN) + internal actual val MAX: Instant = Instant(jtInstant.MAX) } } -public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant { +public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try { val thisZdt = this.value.atZone(zone.zoneId) - return with(period) { + with(period) { thisZdt .run { if (years != 0 && months == 0) plusYears(years) else this } .run { if (months != 0) plusMonths(years * 12.0 + months) else this } .run { if (days != 0) plusDays(days) as ZonedDateTime else this } .run { if (hours != 0) plusHours(hours) else this } .run { if (minutes != 0) plusMinutes(minutes) else this } - .run { if (seconds != 0L) plusSeconds(seconds.toDouble()) else this } - .run { if (nanoseconds != 0L) plusNanos(nanoseconds.toDouble()) else this } + .run { plusSecondsFix(seconds) } + .run { plusNanosFix(nanoseconds) } }.toInstant().let(::Instant) +} catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) + throw e } -internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = - when (unit) { - CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() - CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).let { it as ZonedDateTime }.toInstant() - CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() - CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() - CalendarUnit.SECOND -> this.value.plusSeconds(value) - CalendarUnit.MILLISECOND -> this.value.plusMillis(value) - CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000).toInt() * 1000) - CalendarUnit.NANOSECOND -> this.value.plusNanos(value) - }.let(::Instant) +// workaround for https://github.com/js-joda/js-joda/issues/431 +private fun ZonedDateTime.plusSecondsFix(seconds: Long): ZonedDateTime { + val value = seconds.toDouble() + return when { + value == 0.0 -> this + (value.unsafeCast() or 0) != 0 -> plusSeconds(value) + else -> { + val valueLittleLess = value.nextTowards(0.0) + plusSeconds(valueLittleLess).plusSeconds(value - valueLittleLess) + } + } +} + +// workaround for https://github.com/js-joda/js-joda/issues/431 +private fun ZonedDateTime.plusNanosFix(nanoseconds: Long): ZonedDateTime { + val value = nanoseconds.toDouble() + return when { + value == 0.0 -> this + (value.unsafeCast() or 0) != 0 -> plusNanos(value) + else -> { + val valueLittleLess = value.nextTowards(0.0) + plusNanos(valueLittleLess).plusNanos(value - valueLittleLess) + } + } +} + +private fun jtInstant.atZone(zone: TimeZone): ZonedDateTime = atZone(zone.zoneId) + +internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try { + val thisZdt = this.value.atZone(zone) + when (unit) { + CalendarUnit.YEAR -> thisZdt.plusYears(value).toInstant() + CalendarUnit.MONTH -> thisZdt.plusMonths(value).toInstant() + CalendarUnit.DAY -> thisZdt.plusDays(value).let { it as ZonedDateTime }.toInstant() + CalendarUnit.HOUR -> thisZdt.plusHours(value).toInstant() + CalendarUnit.MINUTE -> thisZdt.plusMinutes(value).toInstant() + CalendarUnit.SECOND -> this.plusFix(value.toDouble(), 0) + CalendarUnit.MILLISECOND -> this.plusFix((value / 1_000).toDouble(), (value % 1_000).toInt() * 1_000_000).also { it.atZone(zone) } + CalendarUnit.MICROSECOND -> this.plusFix((value / 1_000_000).toDouble(), (value % 1_000_000).toInt() * 1000).also { it.atZone(zone) } + CalendarUnit.NANOSECOND -> this.plusFix((value / 1_000_000_000).toDouble(), (value % 1_000_000_000).toInt()).also { it.atZone(zone) } + }.let(::Instant) +} catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) + throw e +} @OptIn(ExperimentalTime::class) -public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { +public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod = try { var thisZdt = this.value.atZone(zone.zoneId) val otherZdt = other.value.atZone(zone.zoneId) @@ -102,21 +168,36 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP time.toComponents { hours, minutes, seconds, nanoseconds -> return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) } +} catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) else throw e +} + +public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = try { + when (unit) { + is DateTimeUnit.DateBased -> + this.value.atZone(zone).until(other.value.atZone(zone), unit.calendarUnit.toChronoUnit()).toLong() / unit.calendarScale + is DateTimeUnit.TimeBased -> { + this.value.atZone(zone) + other.value.atZone(zone) + try { + // TODO: use fused multiplyAddDivide + safeAdd( + safeMultiply(other.epochSeconds - this.epochSeconds, LocalTime.NANOS_PER_SECOND.toLong()), + (other.nanosecondsOfSecond - this.nanosecondsOfSecond).toLong() + ) / unit.nanoseconds + } 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 } -public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = - until(other, unit.calendarUnit.toChronoUnit(), zone.zoneId) / unit.calendarScale -private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = - this.value.atZone(zone).until(other.value.atZone(zone), unit).toLong() private fun CalendarUnit.toChronoUnit(): ChronoUnit = when(this) { CalendarUnit.YEAR -> ChronoUnit.YEARS CalendarUnit.MONTH -> ChronoUnit.MONTHS CalendarUnit.DAY -> ChronoUnit.DAYS - CalendarUnit.HOUR -> ChronoUnit.HOURS - CalendarUnit.MINUTE -> ChronoUnit.MINUTES - CalendarUnit.SECOND -> ChronoUnit.SECONDS - CalendarUnit.MILLISECOND -> ChronoUnit.MILLIS - CalendarUnit.MICROSECOND -> ChronoUnit.MICROS - CalendarUnit.NANOSECOND -> ChronoUnit.NANOS + else -> error("CalendarUnit $this should not be used") } diff --git a/core/jsMain/src/JSJodaExceptions.kt b/core/jsMain/src/JSJodaExceptions.kt new file mode 100644 index 000000000..5e70f3d4c --- /dev/null +++ b/core/jsMain/src/JSJodaExceptions.kt @@ -0,0 +1,10 @@ +/* + * 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 fun Throwable.isJodaArithmeticException(): Boolean = this.asDynamic().name == "ArithmeticException" +internal fun Throwable.isJodaDateTimeException(): Boolean = this.asDynamic().name == "DateTimeException" +internal fun Throwable.isJodaDateTimeParseException(): Boolean = this.asDynamic().name == "DateTimeParseException" diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index 3763d2f19..192805311 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -10,13 +10,24 @@ import kotlinx.datetime.internal.JSJoda.LocalDate as jtLocalDate public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { actual companion object { - public actual fun parse(isoString: String): LocalDate { - return jtLocalDate.parse(isoString).let(::LocalDate) + public actual fun parse(isoString: String): LocalDate = try { + jtLocalDate.parse(isoString).let(::LocalDate) + } catch (e: Throwable) { + if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) + throw e } + + internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN) + internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX) } public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) : - this(jtLocalDate.of(year, monthNumber, dayOfMonth)) + this(try { + jtLocalDate.of(year, monthNumber, dayOfMonth) + } catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) + throw e + }) public actual val year: Int get() = value.year().toInt() public actual val monthNumber: Int get() = value.monthValue().toInt() @@ -36,18 +47,23 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa } -private fun LocalDate.plusNumber(value: Number, unit: CalendarUnit): LocalDate = - when (unit) { - CalendarUnit.YEAR -> this.value.plusYears(value) - CalendarUnit.MONTH -> this.value.plusMonths(value) - CalendarUnit.DAY -> this.value.plusDays(value) - CalendarUnit.HOUR, - CalendarUnit.MINUTE, - CalendarUnit.SECOND, - CalendarUnit.MILLISECOND, - CalendarUnit.MICROSECOND, - CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") - }.let(::LocalDate) +private fun LocalDate.plusNumber(value: Number, unit: CalendarUnit): LocalDate = try { + + when (unit) { + CalendarUnit.YEAR -> this.value.plusYears(value) + CalendarUnit.MONTH -> this.value.plusMonths(value) + CalendarUnit.DAY -> this.value.plusDays(value) + CalendarUnit.HOUR, + CalendarUnit.MINUTE, + CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, + CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") + }.let(::LocalDate) +} catch (e: Throwable) { + if (e.isJodaDateTimeException() || e.isJodaArithmeticException()) throw DateTimeArithmeticException(e) + throw e +} internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = plusNumber(value.toDouble(), unit) @@ -57,14 +73,18 @@ internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = -public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = - with(period) { - return@with value - .run { if (years != 0 && months == 0) plusYears(years) else this } - .run { if (months != 0) plusMonths(years.toDouble() * 12 + months) else this } - .run { if (days != 0) plusDays(days) else this } +public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = try { + with(period) { + return@with value + .run { if (years != 0 && months == 0) plusYears(years) else this } + .run { if (months != 0) plusMonths(years.toDouble() * 12 + months) else this } + .run { if (days != 0) plusDays(days) else this } - }.let(::LocalDate) + }.let(::LocalDate) +} catch (e: Throwable) { + if (e.isJodaDateTimeException() || e.isJodaArithmeticException()) throw DateTimeArithmeticException(e) + throw e +} diff --git a/core/jsMain/src/LocalDateTime.kt b/core/jsMain/src/LocalDateTime.kt index 3abc78ed3..2600000c6 100644 --- a/core/jsMain/src/LocalDateTime.kt +++ b/core/jsMain/src/LocalDateTime.kt @@ -4,16 +4,18 @@ */ package kotlinx.datetime -import kotlin.math.sign -import kotlin.time.* import kotlinx.datetime.internal.JSJoda.LocalDateTime as jtLocalDateTime -import kotlinx.datetime.internal.JSJoda.Period as jtPeriod 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(jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond)) + this(try { + jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) + } catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) + throw e + }) public actual val year: Int get() = value.year().toInt() public actual val monthNumber: Int get() = value.monthValue().toInt() @@ -39,16 +41,26 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc actual override fun compareTo(other: LocalDateTime): Int = this.value.compareTo(other.value).toInt() actual companion object { - public actual fun parse(isoString: String): LocalDateTime { - return jtLocalDateTime.parse(isoString).let(::LocalDateTime) + public actual fun parse(isoString: String): LocalDateTime = try { + jtLocalDateTime.parse(isoString).let(::LocalDateTime) + } catch (e: Throwable) { + if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e) + throw e } + + internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN) + internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX) } } -public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = - jtLocalDateTime.ofInstant(this.value, timeZone.zoneId).let(::LocalDateTime) +public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = try { + jtLocalDateTime.ofInstant(this.value, timeZone.zoneId).let(::LocalDateTime) +} catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) + throw e +} public actual fun Instant.offsetAt(timeZone: TimeZone): ZoneOffset = timeZone.zoneId.rules().offsetOfInstant(this.value).let(::ZoneOffset) diff --git a/core/jsMain/src/TimeZone.kt b/core/jsMain/src/TimeZone.kt index 5e5f2b8fb..a9ff17ac1 100644 --- a/core/jsMain/src/TimeZone.kt +++ b/core/jsMain/src/TimeZone.kt @@ -26,7 +26,14 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { actual companion object { actual fun currentSystemDefault(): TimeZone = ZoneId.systemDefault().let(::TimeZone) actual val UTC: TimeZone = jtZoneOffset.UTC.let(::TimeZone) - actual fun of(zoneId: String): TimeZone = ZoneId.of(zoneId).let(::TimeZone) + + actual fun of(zoneId: String): TimeZone = try { + ZoneId.of(zoneId).let(::TimeZone) + } catch (e: Throwable) { + if (e.isJodaDateTimeException()) throw IllegalTimeZoneException(e) + throw e + } + actual val availableZoneIds: Set get() = ZoneId.getAvailableZoneIds().toSet() } } diff --git a/core/jsMain/src/mathJs.kt b/core/jsMain/src/mathJs.kt new file mode 100644 index 000000000..fcd819262 --- /dev/null +++ b/core/jsMain/src/mathJs.kt @@ -0,0 +1,60 @@ +/* + * 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 + +/** + * 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() +} diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt index e82f46509..306704eb0 100644 --- a/core/jvmMain/src/Instant.kt +++ b/core/jvmMain/src/Instant.kt @@ -6,7 +6,9 @@ package kotlinx.datetime +import java.time.DateTimeException import java.time.ZoneId +import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import kotlin.time.* import java.time.Instant as jtInstant @@ -20,10 +22,19 @@ public actual class Instant internal constructor(internal val value: jtInstant) actual val nanosecondsOfSecond: Int get() = value.nano - public actual fun toEpochMilliseconds(): Long = value.toEpochMilli() + public actual fun toEpochMilliseconds(): Long = try { + value.toEpochMilli() + } catch (e: ArithmeticException) { + if (value.isAfter(java.time.Instant.EPOCH)) Long.MAX_VALUE else Long.MIN_VALUE + } actual operator fun plus(duration: Duration): Instant = duration.toComponents { seconds, nanoseconds -> - Instant(value.plusSeconds(seconds).plusNanos(nanoseconds.toLong())) + try { + Instant(value.plusSeconds(seconds).plusNanos(nanoseconds.toLong())) + } catch (e: java.lang.Exception) { + if (e !is ArithmeticException && e !is DateTimeException) throw e + if (duration.isPositive()) MAX else MIN + } } actual operator fun minus(duration: Duration): Instant = plus(-duration) @@ -49,45 +60,70 @@ public actual class Instant internal constructor(internal val value: jtInstant) actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = Instant(jtInstant.ofEpochMilli(epochMilliseconds)) - actual fun parse(isoString: String): Instant = - Instant(jtInstant.parse(isoString)) - - actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = - Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment)) + actual fun parse(isoString: String): Instant = try { + Instant(jtInstant.parse(isoString)) + } catch (e: DateTimeParseException) { + throw DateTimeFormatException(e) + } + + actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = try { + Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment)) + } catch (e: Exception) { + if (e !is ArithmeticException && e !is DateTimeException) throw e + if (epochSeconds > 0) MAX else MIN + } + + internal actual val MIN: Instant = Instant(jtInstant.MIN) + internal actual val MAX: Instant = Instant(jtInstant.MAX) } } +private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try { + value.atZone(zone.zoneId) +} catch (e: DateTimeException) { + throw DateTimeArithmeticException(e) +} + public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant { - val thisZdt = this.value.atZone(zone.zoneId) - return with(period) { - thisZdt - .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } - .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this } - .run { if (days != 0) plusDays(days.toLong()) else this } - .run { if (hours != 0) plusHours(hours.toLong()) else this } - .run { if (minutes != 0) plusMinutes(minutes.toLong()) else this } - .run { if (seconds != 0L) plusSeconds(seconds) else this } - .run { if (nanoseconds != 0L) plusNanos(nanoseconds) else this } - }.toInstant().let(::Instant) + try { + val thisZdt = atZone(zone) + return with(period) { + thisZdt + .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } + .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this } + .run { if (days != 0) plusDays(days.toLong()) else this } + .run { if (hours != 0) plusHours(hours.toLong()) else this } + .run { if (minutes != 0) plusMinutes(minutes.toLong()) else this } + .run { if (seconds != 0L) plusSeconds(seconds) else this } + .run { if (nanoseconds != 0L) plusNanos(nanoseconds) else this } + }.toInstant().let(::Instant) + } catch (e: DateTimeException) { + throw DateTimeArithmeticException(e) + } } -internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = - when (unit) { - CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() - CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).toInstant() - CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() - CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() - CalendarUnit.SECOND -> this.value.plusSeconds(value) - CalendarUnit.MILLISECOND -> this.value.plusMillis(value) - CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000) * 1000) - CalendarUnit.NANOSECOND -> this.value.plusNanos(value) - }.let(::Instant) +internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try { + val thisZdt = atZone(zone) + when (unit) { + CalendarUnit.YEAR -> thisZdt.plusYears(value).toInstant() + CalendarUnit.MONTH -> thisZdt.plusMonths(value).toInstant() + CalendarUnit.DAY -> thisZdt.plusDays(value).toInstant() + CalendarUnit.HOUR -> thisZdt.plusHours(value).toInstant() + CalendarUnit.MINUTE -> thisZdt.plusMinutes(value).toInstant() + CalendarUnit.SECOND -> this.value.plusSeconds(value).also { it.atZone(zone.zoneId) } + CalendarUnit.MILLISECOND -> this.value.plusMillis(value).also { it.atZone(zone.zoneId) } + CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000) * 1000).also { it.atZone(zone.zoneId) } + CalendarUnit.NANOSECOND -> this.value.plusNanos(value).also { it.atZone(zone.zoneId) } + }.let(::Instant) +} catch (e: Throwable) { + if (e !is DateTimeException && e !is ArithmeticException) throw e + throw DateTimeArithmeticException("Instant $this cannot be represented as local date when adding $value $unit to it", e) +} @OptIn(ExperimentalTime::class) public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { - var thisZdt = this.value.atZone(zone.zoneId) - val otherZdt = other.value.atZone(zone.zoneId) + var thisZdt = this.atZone(zone) + val otherZdt = other.atZone(zone) val months = thisZdt.until(otherZdt, ChronoUnit.MONTHS); thisZdt = thisZdt.plusMonths(months) val days = thisZdt.until(otherZdt, ChronoUnit.DAYS); thisZdt = thisZdt.plusDays(days) @@ -101,8 +137,13 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = until(other, unit.calendarUnit.toChronoUnit(), zone.zoneId) / unit.calendarScale -private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = - this.value.atZone(zone).until(other.value.atZone(zone), unit) +private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = try { + this.value.atZone(zone).until(other.value.atZone(zone), unit) +} catch (e: DateTimeException) { + throw DateTimeArithmeticException(e) +} catch (e: ArithmeticException) { + if (this.value < other.value) Long.MAX_VALUE else Long.MIN_VALUE +} private fun CalendarUnit.toChronoUnit(): ChronoUnit = when(this) { CalendarUnit.YEAR -> ChronoUnit.YEARS diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt index c815b6021..3315229af 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -5,19 +5,30 @@ @file:JvmName("LocalDateJvmKt") package kotlinx.datetime +import java.time.DateTimeException +import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import java.time.LocalDate as jtLocalDate public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { actual companion object { - public actual fun parse(isoString: String): LocalDate { - return jtLocalDate.parse(isoString).let(::LocalDate) + public actual fun parse(isoString: String): LocalDate = try { + jtLocalDate.parse(isoString).let(::LocalDate) + } catch (e: DateTimeParseException) { + throw DateTimeFormatException(e) } + + internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN) + internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX) } public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) : - this(jtLocalDate.of(year, monthNumber, dayOfMonth)) + this(try { + jtLocalDate.of(year, monthNumber, dayOfMonth) + } catch (e: DateTimeException) { + throw IllegalArgumentException(e) + }) public actual val year: Int get() = value.year public actual val monthNumber: Int get() = value.monthValue @@ -37,30 +48,47 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa } -internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = - when (unit) { - CalendarUnit.YEAR -> this.value.plusYears(value) - CalendarUnit.MONTH -> this.value.plusMonths(value) - CalendarUnit.DAY -> this.value.plusDays(value) - CalendarUnit.HOUR, - CalendarUnit.MINUTE, - CalendarUnit.SECOND, - CalendarUnit.MILLISECOND, - CalendarUnit.MICROSECOND, - CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") - }.let(::LocalDate) +internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = try { + when (unit) { + CalendarUnit.YEAR -> this.value.plusYears(value) + CalendarUnit.MONTH -> this.value.plusMonths(value) + CalendarUnit.DAY -> ofEpochDayChecked(safeAdd(this.value.toEpochDay(), value)) + CalendarUnit.HOUR, + CalendarUnit.MINUTE, + CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, + CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") + }.let(::LocalDate) +} catch(e: Exception) { + if (e is DateTimeException || e is ArithmeticException) + throw DateTimeArithmeticException("The result of adding $value of $unit to $this is out of LocalDate range.") + else + throw e +} +private val minEpochDay = java.time.LocalDate.MIN.toEpochDay() +private val maxEpochDay = java.time.LocalDate.MAX.toEpochDay() +private fun ofEpochDayChecked(epochDay: Long): java.time.LocalDate { + // LocalDate.ofEpochDay doesn't actually check that the argument doesn't overflow year calculation + if (epochDay !in minEpochDay..maxEpochDay) + throw DateTimeException("The resulting day $epochDay is out of supported LocalDate range.") + return java.time.LocalDate.ofEpochDay(epochDay) +} internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plus(value.toLong(), unit) -public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = - with(period) { - return@with value - .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } - .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this } - .run { if (days != 0) plusDays(days.toLong()) else this } +public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = try { + with(period) { + return@with value + .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } + .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this } + .run { if (days != 0) plusDays(days.toLong()) else this } - }.let(::LocalDate) + }.let(::LocalDate) +} catch (e: DateTimeException) { + throw DateTimeArithmeticException("The result of adding $value to $this is out of LocalDate range.") +} public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { diff --git a/core/jvmMain/src/LocalDateTime.kt b/core/jvmMain/src/LocalDateTime.kt index de9bd0b4c..016768273 100644 --- a/core/jvmMain/src/LocalDateTime.kt +++ b/core/jvmMain/src/LocalDateTime.kt @@ -5,6 +5,8 @@ @file:JvmName("LocalDateTimeJvmKt") package kotlinx.datetime +import java.time.DateTimeException +import java.time.format.DateTimeParseException import java.time.LocalDateTime as jtLocalDateTime @@ -14,7 +16,11 @@ public actual typealias DayOfWeek = java.time.DayOfWeek 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(jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond)) + this(try { + jtLocalDateTime.of(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) + } catch (e: DateTimeException) { + throw IllegalArgumentException(e) + }) public actual val year: Int get() = value.year public actual val monthNumber: Int get() = value.monthValue @@ -40,16 +46,24 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc actual override fun compareTo(other: LocalDateTime): Int = this.value.compareTo(other.value) actual companion object { - public actual fun parse(isoString: String): LocalDateTime { - return jtLocalDateTime.parse(isoString).let(::LocalDateTime) + public actual fun parse(isoString: String): LocalDateTime = try { + jtLocalDateTime.parse(isoString).let(::LocalDateTime) + } catch (e: DateTimeParseException) { + throw DateTimeFormatException(e) } + + internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN) + internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX) } } -public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = - jtLocalDateTime.ofInstant(this.value, timeZone.zoneId).let(::LocalDateTime) +public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = try { + jtLocalDateTime.ofInstant(this.value, timeZone.zoneId).let(::LocalDateTime) +} catch (e: DateTimeException) { + throw DateTimeArithmeticException(e) +} public actual fun Instant.offsetAt(timeZone: TimeZone): ZoneOffset = timeZone.zoneId.rules.getOffset(this.value).let(::ZoneOffset) diff --git a/core/jvmMain/src/TimeZone.kt b/core/jvmMain/src/TimeZone.kt index 98be0cd21..9f4d509f3 100644 --- a/core/jvmMain/src/TimeZone.kt +++ b/core/jvmMain/src/TimeZone.kt @@ -5,6 +5,7 @@ package kotlinx.datetime +import java.time.DateTimeException import java.time.ZoneId import java.time.ZoneOffset as jtZoneOffset @@ -27,7 +28,15 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { actual companion object { actual fun currentSystemDefault(): TimeZone = ZoneId.systemDefault().let(::TimeZone) actual val UTC: TimeZone = jtZoneOffset.UTC.let(::TimeZone) - actual fun of(zoneId: String): TimeZone = ZoneId.of(zoneId).let(::TimeZone) + + actual fun of(zoneId: String): TimeZone = try { + // TODO: Return ZoneOffset for j.t.ZoneOffset + ZoneId.of(zoneId).let(::TimeZone) + } catch (e: Exception) { + if (e is DateTimeException) throw IllegalTimeZoneException(e) + throw e + } + actual val availableZoneIds: Set get() = ZoneId.getAvailableZoneIds() } } diff --git a/core/jvmMain/src/mathJvm.kt b/core/jvmMain/src/mathJvm.kt new file mode 100644 index 000000000..074bd7987 --- /dev/null +++ b/core/jvmMain/src/mathJvm.kt @@ -0,0 +1,11 @@ +/* + * 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 safeMultiply(a: Long, b: Long): Long = Math.multiplyExact(a, b) +internal actual fun safeMultiply(a: Int, b: Int): Int = Math.multiplyExact(a, b) +internal actual fun safeAdd(a: Int, b: Int): Int = Math.addExact(a, b) +internal actual fun safeAdd(a: Long, b: Long): Long = Math.addExact(a, b) diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index f81d01e99..991659039 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -205,8 +205,8 @@ public actual class Instant internal constructor(actual val epochSeconds: Long, } actual companion object { - internal val MIN = Instant(MIN_SECOND, 0) - internal val MAX = Instant(MAX_SECOND, 999_999_999) + internal actual val MIN = Instant(MIN_SECOND, 0) + internal actual val MAX = Instant(MAX_SECOND, 999_999_999) @Deprecated("Use Clock.System.now() instead", ReplaceWith("Clock.System.now()", "kotlinx.datetime.Clock"), level = DeprecationLevel.ERROR) actual fun now(): Instant = memScoped { @@ -276,9 +276,13 @@ actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try { .run { if (years != 0 && months == 0) plus(years, DateTimeUnit.YEAR) else this } .run { if (months != 0) plus(safeAdd(safeMultiply(years, 12), months), DateTimeUnit.MONTH) else this } .run { if (days != 0) plus(days, DateTimeUnit.DAY) else this } - val secondsToAdd = safeAdd(seconds, - safeAdd(minutes.toLong() * SECONDS_PER_MINUTE, hours.toLong() * SECONDS_PER_HOUR)) - withDate.toInstant().plus(secondsToAdd, period.nanoseconds) + withDate.toInstant() + .run { if (hours != 0) + plus(hours.toLong() * SECONDS_PER_HOUR, 0).check(zone) else this } + .run { if (minutes != 0) + plus(minutes.toLong() * SECONDS_PER_MINUTE, 0).check(zone) else this } + .run { if (seconds != 0L) plus(seconds, 0).check(zone) else this } + .run { if (nanoseconds != 0L) plus(0, nanoseconds).check(zone) else this } }.check(zone) } catch (e: ArithmeticException) { throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e) diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index eedb7ac8b..7d2dc5c74 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -92,6 +92,9 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va return LocalDate(yearEst, month, dom) } + + internal actual val MIN = LocalDate(YEAR_MIN, 1, 1) + internal actual val MAX = LocalDate(YEAR_MAX, 12, 31) } // org.threeten.bp.LocalDate#toEpochDay diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt index aa3b0a348..881282cc4 100644 --- a/core/nativeMain/src/LocalDateTime.kt +++ b/core/nativeMain/src/LocalDateTime.kt @@ -23,6 +23,9 @@ public actual class LocalDateTime internal constructor( actual companion object { actual fun parse(isoString: String): LocalDateTime = localDateTimeParser.parse(isoString) + + internal actual val MIN: LocalDateTime = LocalDateTime(LocalDate.MIN, LocalTime.MIN) + internal actual val MAX: LocalDateTime = LocalDateTime(LocalDate.MAX, LocalTime.MAX) } actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : diff --git a/core/nativeMain/src/Util.kt b/core/nativeMain/src/Util.kt index 32f1da920..959b835bf 100644 --- a/core/nativeMain/src/Util.kt +++ b/core/nativeMain/src/Util.kt @@ -176,8 +176,6 @@ internal const val HOURS_PER_DAY = 24 */ internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY -internal const val MINUTES_PER_DAY: Int = MINUTES_PER_HOUR * HOURS_PER_DAY - internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong() internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong() @@ -193,7 +191,7 @@ internal const val SECONDS_0000_TO_1970 = (146097L * 5L - (30L * 365L + 7L)) * 8 * Safely adds two long values. * throws [ArithmeticException] if the result overflows a long */ -internal fun safeAdd(a: Long, b: Long): 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) { @@ -206,7 +204,7 @@ internal fun safeAdd(a: Long, b: Long): Long { * Safely adds two int values. * throws [ArithmeticException] if the result overflows an int */ -internal fun safeAdd(a: Int, b: Int): Int { +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) { @@ -215,23 +213,6 @@ internal fun safeAdd(a: Int, b: Int): Int { return sum } -/** - * Safely subtracts one long from another. - * - * @param a the first value - * @param b the second value to subtract from the first - * @return the result - * @throws ArithmeticException if the result overflows a long - */ -internal fun safeSubtract(a: Long, b: Long): Long { - val result = a - b - // check for a change of sign in the result when the inputs have the different signs - if (a xor result < 0 && a xor b < 0) { - throw ArithmeticException("Subtraction overflows a long: $a - $b") - } - return result -} - /** * Safely multiply a long by a long. * @@ -240,7 +221,7 @@ internal fun safeSubtract(a: Long, b: Long): Long { * @return the new total * @throws ArithmeticException if the result overflows a long */ -internal fun safeMultiply(a: Long, b: Long): Long { +internal actual fun safeMultiply(a: Long, b: Long): Long { if (b == 1L) { return a } @@ -265,7 +246,7 @@ internal fun safeMultiply(a: Long, b: Long): Long { * @return the new total * @throws ArithmeticException if the result overflows an int */ -internal fun safeMultiply(a: Int, b: Int): Int { +internal actual fun safeMultiply(a: Int, b: Int): Int { val total = a.toLong() * b.toLong() if (total < Int.MIN_VALUE || total > Int.MAX_VALUE) { throw ArithmeticException("Multiplication overflows an int: $a * $b")