From c935738bee32b2cc5635a1fad1e81f6a6ace7a24 Mon Sep 17 00:00:00 2001
From: Dmitry Khalanskiy <Dmitry.Khalanskiy@jetbrains.com>
Date: Wed, 22 Jul 2020 15:35:23 +0300
Subject: [PATCH] Common: add tests for exceptions and clamping

---
 core/commonMain/src/Instant.kt           |   3 +
 core/commonMain/src/LocalDate.kt         |   3 +
 core/commonMain/src/LocalDateTime.kt     |   3 +
 core/commonTest/src/InstantTest.kt       | 181 +++++++++++++++++++++++
 core/commonTest/src/LocalDateTest.kt     |  57 +++++--
 core/commonTest/src/LocalDateTimeTest.kt |  22 ++-
 core/commonTest/src/TimeZoneTest.kt      |   4 +-
 core/jsMain/src/Instant.kt               |   3 +
 core/jsMain/src/LocalDate.kt             |   3 +
 core/jsMain/src/LocalDateTime.kt         |   3 +
 core/jvmMain/src/Instant.kt              |   3 +
 core/jvmMain/src/LocalDate.kt            |   3 +
 core/jvmMain/src/LocalDateTime.kt        |   3 +
 core/nativeMain/src/Instant.kt           |  28 ++--
 core/nativeMain/src/LocalDate.kt         |   7 +-
 core/nativeMain/src/LocalDateTime.kt     |   3 +
 16 files changed, 291 insertions(+), 38 deletions(-)

diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt
index 8164b244..a89e0bcd 100644
--- a/core/commonMain/src/Instant.kt
+++ b/core/commonMain/src/Instant.kt
@@ -62,6 +62,9 @@ public expect class Instant : Comparable<Instant> {
          * @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
     }
 }
 
diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt
index 652543c6..8d3ced81 100644
--- a/core/commonMain/src/LocalDate.kt
+++ b/core/commonMain/src/LocalDate.kt
@@ -11,6 +11,9 @@ public expect class LocalDate : Comparable<LocalDate> {
          * @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
     }
 
     /**
diff --git a/core/commonMain/src/LocalDateTime.kt b/core/commonMain/src/LocalDateTime.kt
index b995d7e1..f5b49bc6 100644
--- a/core/commonMain/src/LocalDateTime.kt
+++ b/core/commonMain/src/LocalDateTime.kt
@@ -14,6 +14,9 @@ public expect class LocalDateTime : Comparable<LocalDateTime> {
          * exceeded.
          */
         public fun parse(isoString: String): LocalDateTime
+
+        internal val MIN: LocalDateTime
+        internal val MAX: LocalDateTime
     }
 
     /**
diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt
index 8700bf8b..3d0d3a38 100644
--- a/core/commonTest/src/InstantTest.kt
+++ b/core/commonTest/src/InstantTest.kt
@@ -287,4 +287,185 @@ class InstantTest {
         assertEquals("+19999-12-31T23:59:59.000000009Z", LocalDateTime(19999, 12, 31, 23, 59, 59, 9).toInstant(TimeZone.UTC).toString())
     }
 
+    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)
+    @OptIn(ExperimentalTime::class)
+    private val largePositiveInstants = listOf(Instant.MAX, Instant.MAX - 1.seconds, Instant.MAX - 50.seconds)
+    @OptIn(ExperimentalTime::class)
+    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 testParsingThrowing() {
+        assertFailsWith<DateTimeFormatException> { Instant.parse("x") }
+        assertFailsWith<DateTimeFormatException> { Instant.parse("12020-12-31T23:59:59.000000000Z") }
+        // this string represents an Instant that is currently larger than Instant.MAX any of the implementations:
+        assertFailsWith<DateTimeFormatException> { Instant.parse("+1000000001-12-31T23:59:59.000000000Z") }
+    }
+
+    @ExperimentalTime
+    @Test
+    fun testConstructorAccessorClamping() {
+        // 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")
+            }
+        }
+        // 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()))
+        }
+    }
+
+    @OptIn(ExperimentalTime::class)
+    @Test
+    fun testArithmeticClamping() {
+        for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
+            assertEquals(Instant.MAX, instant + Duration.INFINITE)
+        }
+        for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
+            assertEquals(Instant.MIN, instant - Duration.INFINITE)
+        }
+        assertEquals(Instant.MAX, (Instant.MAX - 4.seconds) + 5.seconds)
+        assertEquals(Instant.MIN, (Instant.MIN + 10.seconds) - 12.seconds)
+    }
+
+    @OptIn(ExperimentalTime::class)
+    @Test
+    fun testCalendarArithmeticThrowing() {
+        val maxValidInstant = LocalDateTime.MAX.toInstant(TimeZone.UTC)
+        val minValidInstant = LocalDateTime.MIN.toInstant(TimeZone.UTC)
+
+        // Instant.plus(DateTimePeriod(), TimeZone)
+        // Arithmetic overflow
+        for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(DateTimePeriod(seconds = Long.MAX_VALUE), TimeZone.UTC)
+            }
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(DateTimePeriod(seconds = Long.MIN_VALUE), TimeZone.UTC)
+            }
+        }
+        // Overflowing a LocalDateTime in input
+        maxValidInstant.plus(DateTimePeriod(nanoseconds = -1), TimeZone.UTC)
+        minValidInstant.plus(DateTimePeriod(nanoseconds = 1), TimeZone.UTC)
+        assertFailsWith<DateTimeArithmeticException> {
+            (maxValidInstant + 1.nanoseconds).plus(DateTimePeriod(nanoseconds = -2), TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            (minValidInstant - 1.nanoseconds).plus(DateTimePeriod(nanoseconds = 2), TimeZone.UTC)
+        }
+        // Overflowing a LocalDateTime in result
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(DateTimePeriod(nanoseconds = 1), TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            minValidInstant.plus(DateTimePeriod(nanoseconds = -1), TimeZone.UTC)
+        }
+        // Overflowing a LocalDateTime in intermediate computations
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(DateTimePeriod(seconds = 1, nanoseconds = -1_000_000_001), TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(DateTimePeriod(hours = 1, minutes = -61), TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(DateTimePeriod(days = 1, hours = -48), TimeZone.UTC)
+        }
+
+        // Instant.plus(Long, DateTimeUnit, TimeZone)
+        // Arithmetic overflow
+        for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(Long.MAX_VALUE, DateTimeUnit.SECOND, TimeZone.UTC)
+            }
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(Long.MIN_VALUE, DateTimeUnit.SECOND, TimeZone.UTC)
+            }
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(Long.MAX_VALUE, DateTimeUnit.YEAR, TimeZone.UTC)
+            }
+            assertFailsWith<DateTimeArithmeticException>("$instant") {
+                instant.plus(Long.MIN_VALUE, DateTimeUnit.YEAR, TimeZone.UTC)
+            }
+        }
+        // Overflowing a LocalDateTime in input
+        maxValidInstant.plus(-1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        minValidInstant.plus(1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        assertFailsWith<DateTimeArithmeticException> {
+            (maxValidInstant + 1.nanoseconds).plus(-2, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            (minValidInstant - 1.nanoseconds).plus(2, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+        // Overflowing a LocalDateTime in result
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.plus(1, DateTimeUnit.YEAR, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            minValidInstant.plus(-1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            minValidInstant.plus(-1, DateTimeUnit.YEAR, TimeZone.UTC)
+        }
+
+        // Instant.periodUntil
+        maxValidInstant.periodUntil(minValidInstant, TimeZone.UTC)
+        assertFailsWith<DateTimeArithmeticException> {
+            (maxValidInstant + 1.nanoseconds).periodUntil(minValidInstant, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.periodUntil(minValidInstant - 1.nanoseconds, TimeZone.UTC)
+        }
+
+        // Instant.until
+        // Arithmetic overflow
+        assertEquals(Long.MAX_VALUE, minValidInstant.until(maxValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC))
+        assertEquals(Long.MIN_VALUE, maxValidInstant.until(minValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC))
+        // Overflowing a LocalDateTime in input
+        assertFailsWith<DateTimeArithmeticException> {
+            (maxValidInstant + 1.nanoseconds).until(maxValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+        assertFailsWith<DateTimeArithmeticException> {
+            maxValidInstant.until(maxValidInstant + 1.nanoseconds, DateTimeUnit.NANOSECOND, TimeZone.UTC)
+        }
+    }
+
 }
diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt
index ec94ee05..616f0509 100644
--- a/core/commonTest/src/LocalDateTest.kt
+++ b/core/commonTest/src/LocalDateTest.kt
@@ -42,11 +42,13 @@ class LocalDateTest {
         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<Throwable> { LocalDate.parse("102017-10-01") }
-        assertFailsWith<Throwable> { LocalDate.parse("2017--10-01") }
-        assertFailsWith<Throwable> { LocalDate.parse("2017-+10-01") }
-        assertFailsWith<Throwable> { LocalDate.parse("2017-10-+01") }
-        assertFailsWith<Throwable> { LocalDate.parse("2017-10--01") }
+        assertFailsWith<DateTimeFormatException> { LocalDate.parse("102017-10-01") }
+        assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017--10-01") }
+        assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-+10-01") }
+        assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-10-+01") }
+        assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-10--01") }
+        // this date is currently larger than the largest representable one any of the platforms:
+        assertFailsWith<DateTimeFormatException> { 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<IllegalArgumentException> { startDate + CalendarPeriod(hours = 7) } // won't compile
+//        assertFailsWith<IllegalArgumentException> { startDate + DateTimePeriod(hours = 7) } // won't compile
 //        assertFailsWith<IllegalArgumentException> { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile
     }
 
@@ -136,15 +138,42 @@ class LocalDateTest {
 
     @Test
     fun invalidDate() {
-        assertFailsWith<Throwable> { LocalDate(2007, 2, 29) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 2, 29) }
         LocalDate(2008, 2, 29)
-        assertFailsWith<Throwable> { LocalDate(2007, 4, 31) }
-        assertFailsWith<Throwable> { LocalDate(2007, 1, 0) }
-        assertFailsWith<Throwable> { LocalDate(2007,1, 32) }
-        assertFailsWith<Throwable> { LocalDate(Int.MIN_VALUE, 1, 1) }
-        assertFailsWith<Throwable> { LocalDate(2007, 1, 32) }
-        assertFailsWith<Throwable> { LocalDate(2007, 0, 1) }
-        assertFailsWith<Throwable> { LocalDate(2007, 13, 1) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 4, 31) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 1, 0) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007,1, 32) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(Int.MIN_VALUE, 1, 1) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 1, 32) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 0, 1) }
+        assertFailsWith<IllegalArgumentException> { LocalDate(2007, 13, 1) }
+    }
+
+    @Test
+    fun testArithmeticThrowing() {
+        // LocalDate.plus(Long, DateTimeUnit)
+        LocalDate.MAX.plus(-1, DateTimeUnit.DAY)
+        LocalDate.MIN.plus(1, DateTimeUnit.DAY)
+        // Arithmetic overflow
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(Long.MAX_VALUE, DateTimeUnit.YEAR) }
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(Long.MAX_VALUE - 2, DateTimeUnit.YEAR) }
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MIN_VALUE, DateTimeUnit.YEAR) }
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MIN_VALUE + 2, DateTimeUnit.YEAR) }
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MAX_VALUE, DateTimeUnit.DAY) }
+        // Exceeding the boundaries of LocalDate
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(1, DateTimeUnit.YEAR) }
+        assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(-1, DateTimeUnit.YEAR) }
+
+        // LocalDate.plus(DatePeriod)
+        LocalDate.MAX.plus(DatePeriod(years = -2, months = 12, days = 31))
+        // Exceeding the boundaries in result
+        assertFailsWith<DateTimeArithmeticException> {
+            LocalDate.MAX.plus(DatePeriod(years = -2, months = 24, days = 1))
+        }
+        // Exceeding the boundaries in intermediate computations
+        assertFailsWith<DateTimeArithmeticException> {
+            LocalDate.MAX.plus(DatePeriod(years = -2, months = 25, days = -1000))
+        }
     }
 
 }
diff --git a/core/commonTest/src/LocalDateTimeTest.kt b/core/commonTest/src/LocalDateTimeTest.kt
index a383f20e..1bf2485b 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<DateTimeFormatException> { LocalDateTime.parse("x") }
+        assertFailsWith<DateTimeFormatException> { "+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<DateTimeArithmeticException> { (Instant.MAX - 3.days).toLocalDateTime(TimeZone.UTC) }
+        assertFailsWith<DateTimeArithmeticException> { (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<DateTimeArithmeticException> { Instant.MAX.toLocalDateTime(TimeZone.UTC) }
     }
 
     @Test
@@ -99,14 +105,14 @@ class LocalDateTimeTest {
         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<Throwable> { localTime(-1, 0) }
-        assertFailsWith<Throwable> { localTime(24, 0) }
-        assertFailsWith<Throwable> { localTime(0, -1) }
-        assertFailsWith<Throwable> { localTime(0, 60) }
-        assertFailsWith<Throwable> { localTime(0, 0, -1) }
-        assertFailsWith<Throwable> { localTime(0, 0, 60) }
-        assertFailsWith<Throwable> { localTime(0, 0, 0, -1) }
-        assertFailsWith<Throwable> { localTime(0, 0, 0, 1_000_000_000) }
+        assertFailsWith<IllegalArgumentException> { localTime(-1, 0) }
+        assertFailsWith<IllegalArgumentException> { localTime(24, 0) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, -1) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, 60) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, 0, -1) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, 0, 60) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, 0, 0, -1) }
+        assertFailsWith<IllegalArgumentException> { localTime(0, 0, 0, 1_000_000_000) }
     }
 
 }
diff --git a/core/commonTest/src/TimeZoneTest.kt b/core/commonTest/src/TimeZoneTest.kt
index 403c525d..4360cbb4 100644
--- a/core/commonTest/src/TimeZoneTest.kt
+++ b/core/commonTest/src/TimeZoneTest.kt
@@ -49,8 +49,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<IllegalTimeZoneException> { TimeZone.of("Mars/Standard") }
+        assertFailsWith<IllegalTimeZoneException> { TimeZone.of("UTC+X") }
 
     }
 
diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt
index aaa972ce..5e65eb88 100644
--- a/core/jsMain/src/Instant.kt
+++ b/core/jsMain/src/Instant.kt
@@ -59,6 +59,9 @@ public actual class Instant internal constructor(internal val value: jtInstant)
 
         actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant =
                 Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))
+
+        internal actual val MIN: Instant = Instant(jtInstant.MIN)
+        internal actual val MAX: Instant = Instant(jtInstant.MAX)
     }
 }
 
diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt
index 3763d2f1..3c7520ee 100644
--- a/core/jsMain/src/LocalDate.kt
+++ b/core/jsMain/src/LocalDate.kt
@@ -13,6 +13,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
         public actual fun parse(isoString: String): LocalDate {
             return jtLocalDate.parse(isoString).let(::LocalDate)
         }
+
+        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) :
diff --git a/core/jsMain/src/LocalDateTime.kt b/core/jsMain/src/LocalDateTime.kt
index 3abc78ed..653d132d 100644
--- a/core/jsMain/src/LocalDateTime.kt
+++ b/core/jsMain/src/LocalDateTime.kt
@@ -42,6 +42,9 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
         public actual fun parse(isoString: String): LocalDateTime {
             return jtLocalDateTime.parse(isoString).let(::LocalDateTime)
         }
+
+        internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN)
+        internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX)
     }
 
 }
diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt
index e82f4650..6c152f6d 100644
--- a/core/jvmMain/src/Instant.kt
+++ b/core/jvmMain/src/Instant.kt
@@ -54,6 +54,9 @@ public actual class Instant internal constructor(internal val value: jtInstant)
 
         actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant =
                 Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))
+
+        internal actual val MIN: Instant = Instant(jtInstant.MIN)
+        internal actual val MAX: Instant = Instant(jtInstant.MAX)
     }
 }
 
diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt
index c815b602..248fa67a 100644
--- a/core/jvmMain/src/LocalDate.kt
+++ b/core/jvmMain/src/LocalDate.kt
@@ -14,6 +14,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
         public actual fun parse(isoString: String): LocalDate {
             return jtLocalDate.parse(isoString).let(::LocalDate)
         }
+
+        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) :
diff --git a/core/jvmMain/src/LocalDateTime.kt b/core/jvmMain/src/LocalDateTime.kt
index de9bd0b4..736c2d2e 100644
--- a/core/jvmMain/src/LocalDateTime.kt
+++ b/core/jvmMain/src/LocalDateTime.kt
@@ -43,6 +43,9 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
         public actual fun parse(isoString: String): LocalDateTime {
             return jtLocalDateTime.parse(isoString).let(::LocalDateTime)
         }
+
+        internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN)
+        internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX)
     }
 
 }
diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt
index 252e4224..e203fb8a 100644
--- a/core/nativeMain/src/Instant.kt
+++ b/core/nativeMain/src/Instant.kt
@@ -219,8 +219,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 {
@@ -289,10 +289,14 @@ actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try {
             .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this }
             .run { if (days != 0) plusDays(days.toLong()) else this }
         // See [Instant.plus(Instant, long, CalendarUnit, TimeZone)] for an explanation of time inside day being special
-        val secondsToAdd = safeAdd(seconds,
-            safeAdd(minutes.toLong() * SECONDS_PER_MINUTE, hours.toLong() * SECONDS_PER_HOUR))
-        withDate.toInstant().plus(secondsToAdd, period.nanoseconds)
-    }.check(zone)
+        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 }
+    }
 } catch (e: ArithmeticException) {
     throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e)
 } catch (e: IllegalArgumentException) {
@@ -320,12 +324,12 @@ internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone
            4 and 5 are invertible by their form: their composition adds and subtracts the offset to and from
            the unix epoch. 1-3 can then be simplified to just adding the time to the instant directly.
          */
-        CalendarUnit.HOUR -> plus(safeMultiply(value, SECONDS_PER_HOUR.toLong()), 0).check(zone)
-        CalendarUnit.MINUTE -> plus(safeMultiply(value, SECONDS_PER_MINUTE.toLong()), 0).check(zone)
-        CalendarUnit.SECOND -> plus(value, 0).check(zone)
-        CalendarUnit.MILLISECOND -> plus(value / MILLIS_PER_ONE, (value % MILLIS_PER_ONE) * NANOS_PER_MILLI).check(zone)
-        CalendarUnit.MICROSECOND -> plus(value / MICROS_PER_ONE, (value % MICROS_PER_ONE) * NANOS_PER_MICRO).check(zone)
-        CalendarUnit.NANOSECOND -> plus(0, value).check(zone)
+        CalendarUnit.HOUR -> check(zone).plus(safeMultiply(value, SECONDS_PER_HOUR.toLong()), 0).check(zone)
+        CalendarUnit.MINUTE -> check(zone).plus(safeMultiply(value, SECONDS_PER_MINUTE.toLong()), 0).check(zone)
+        CalendarUnit.SECOND -> check(zone).plus(value, 0).check(zone)
+        CalendarUnit.MILLISECOND -> check(zone).plus(value / MILLIS_PER_ONE, (value % MILLIS_PER_ONE) * NANOS_PER_MILLI).check(zone)
+        CalendarUnit.MICROSECOND -> check(zone).plus(value / MICROS_PER_ONE, (value % MICROS_PER_ONE) * NANOS_PER_MICRO).check(zone)
+        CalendarUnit.NANOSECOND -> check(zone).plus(0, value).check(zone)
     }
 } catch (e: ArithmeticException) {
     throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e)
diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt
index 99b8697d..9395f1a3 100644
--- a/core/nativeMain/src/LocalDate.kt
+++ b/core/nativeMain/src/LocalDate.kt
@@ -92,13 +92,16 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
 
             return LocalDate(year, 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
     internal fun toEpochDay(): Long {
-        val y = year
+        val y = year.toLong()
         val m = monthNumber
-        var total = 0
+        var total = 0L
         total += 365 * y
         if (y >= 0) {
             total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt
index eea8420c..bbe1ff95 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) :