diff --git a/core/api/kotlinx-datetime.api b/core/api/kotlinx-datetime.api index 7ce03b034..c5d5f31aa 100644 --- a/core/api/kotlinx-datetime.api +++ b/core/api/kotlinx-datetime.api @@ -301,6 +301,8 @@ public final class kotlinx/datetime/LocalDate$Companion { public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat; public final fun fromEpochDays (I)Lkotlinx/datetime/LocalDate; public final fun fromEpochDays (J)Lkotlinx/datetime/LocalDate; + public final fun orNull (III)Lkotlinx/datetime/LocalDate; + public final fun orNull (ILkotlinx/datetime/Month;I)Lkotlinx/datetime/LocalDate; public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalDate; public final synthetic fun parse (Ljava/lang/String;)Lkotlinx/datetime/LocalDate; public static synthetic fun parse$default (Lkotlinx/datetime/LocalDate$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalDate; @@ -440,6 +442,10 @@ public final class kotlinx/datetime/LocalDateTime : java/io/Serializable, java/l public final class kotlinx/datetime/LocalDateTime$Companion { public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat; + public final fun orNull (IIIIIII)Lkotlinx/datetime/LocalDateTime; + public final fun orNull (ILkotlinx/datetime/Month;IIIII)Lkotlinx/datetime/LocalDateTime; + public static synthetic fun orNull$default (Lkotlinx/datetime/LocalDateTime$Companion;IIIIIIIILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; + public static synthetic fun orNull$default (Lkotlinx/datetime/LocalDateTime$Companion;ILkotlinx/datetime/Month;IIIIIILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalDateTime; public final synthetic fun parse (Ljava/lang/String;)Lkotlinx/datetime/LocalDateTime; public static synthetic fun parse$default (Lkotlinx/datetime/LocalDateTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; @@ -485,6 +491,8 @@ public final class kotlinx/datetime/LocalTime$Companion { public final fun fromMillisecondOfDay (I)Lkotlinx/datetime/LocalTime; public final fun fromNanosecondOfDay (J)Lkotlinx/datetime/LocalTime; public final fun fromSecondOfDay (I)Lkotlinx/datetime/LocalTime; + public final fun orNull (IIII)Lkotlinx/datetime/LocalTime; + public static synthetic fun orNull$default (Lkotlinx/datetime/LocalTime$Companion;IIIIILjava/lang/Object;)Lkotlinx/datetime/LocalTime; public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalTime; public final synthetic fun parse (Ljava/lang/String;)Lkotlinx/datetime/LocalTime; public static synthetic fun parse$default (Lkotlinx/datetime/LocalTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalTime; @@ -591,6 +599,8 @@ public final class kotlinx/datetime/UtcOffset : java/io/Serializable { public final class kotlinx/datetime/UtcOffset$Companion { public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat; public final fun getZERO ()Lkotlinx/datetime/UtcOffset; + public final fun orNull (Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Lkotlinx/datetime/UtcOffset; + public static synthetic fun orNull$default (Lkotlinx/datetime/UtcOffset$Companion;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lkotlinx/datetime/UtcOffset; public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/UtcOffset; public final synthetic fun parse (Ljava/lang/String;)Lkotlinx/datetime/UtcOffset; public static synthetic fun parse$default (Lkotlinx/datetime/UtcOffset$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/UtcOffset; diff --git a/core/api/kotlinx-datetime.klib.api b/core/api/kotlinx-datetime.klib.api index 9652179e2..8a25c2d1f 100644 --- a/core/api/kotlinx-datetime.klib.api +++ b/core/api/kotlinx-datetime.klib.api @@ -415,6 +415,8 @@ final class kotlinx.datetime/LocalDate : kotlin/Comparable): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime/LocalDate.Companion.Format|Format(kotlin.Function1){}[0] final fun fromEpochDays(kotlin/Int): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Int){}[0] final fun fromEpochDays(kotlin/Long): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Long){}[0] + final fun orNull(kotlin/Int, kotlin/Int, kotlin/Int): kotlinx.datetime/LocalDate? // kotlinx.datetime/LocalDate.Companion.orNull|orNull(kotlin.Int;kotlin.Int;kotlin.Int){}[0] + final fun orNull(kotlin/Int, kotlinx.datetime/Month, kotlin/Int): kotlinx.datetime/LocalDate? // kotlinx.datetime/LocalDate.Companion.orNull|orNull(kotlin.Int;kotlinx.datetime.Month;kotlin.Int){}[0] final fun parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun parse(kotlin/String): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.parse|parse(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/LocalDate.Companion.serializer|serializer(){}[0] @@ -487,6 +489,8 @@ final class kotlinx.datetime/LocalDateTime : kotlin/Comparable): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime/LocalDateTime.Companion.Format|Format(kotlin.Function1){}[0] + final fun orNull(kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlinx.datetime/LocalDateTime? // kotlinx.datetime/LocalDateTime.Companion.orNull|orNull(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] + final fun orNull(kotlin/Int, kotlinx.datetime/Month, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlinx.datetime/LocalDateTime? // kotlinx.datetime/LocalDateTime.Companion.orNull|orNull(kotlin.Int;kotlinx.datetime.Month;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/LocalDateTime // kotlinx.datetime/LocalDateTime.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun parse(kotlin/String): kotlinx.datetime/LocalDateTime // kotlinx.datetime/LocalDateTime.Companion.parse|parse(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/LocalDateTime.Companion.serializer|serializer(){}[0] @@ -523,6 +527,7 @@ final class kotlinx.datetime/LocalTime : kotlin/Comparable = ...): kotlinx.datetime/LocalTime // kotlinx.datetime/LocalTime.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun parse(kotlin/String): kotlinx.datetime/LocalTime // kotlinx.datetime/LocalTime.Companion.parse|parse(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/LocalTime.Companion.serializer|serializer(){}[0] @@ -547,6 +552,7 @@ final class kotlinx.datetime/UtcOffset { // kotlinx.datetime/UtcOffset|null[0] final fun (): kotlinx.datetime/UtcOffset // kotlinx.datetime/UtcOffset.Companion.ZERO.|(){}[0] final fun Format(kotlin/Function1): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime/UtcOffset.Companion.Format|Format(kotlin.Function1){}[0] + final fun orNull(kotlin/Int? = ..., kotlin/Int? = ..., kotlin/Int? = ...): kotlinx.datetime/UtcOffset? // kotlinx.datetime/UtcOffset.Companion.orNull|orNull(kotlin.Int?;kotlin.Int?;kotlin.Int?){}[0] final fun parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/UtcOffset // kotlinx.datetime/UtcOffset.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun parse(kotlin/String): kotlinx.datetime/UtcOffset // kotlinx.datetime/UtcOffset.Companion.parse|parse(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/UtcOffset.Companion.serializer|serializer(){}[0] diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index e0acaae67..87268cbe8 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -69,6 +69,41 @@ import kotlin.internal.* @Serializable(with = LocalDateSerializer::class) public expect class LocalDate : Comparable { public companion object { + /** + * Constructs a [LocalDate] instance from the given date components + * or returns `null` if a value is out of range or invalid. + * + * The components [month] and [day] are 1-based. + * + * The supported ranges of components: + * - [year] the range is at least enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] + * - [month] `1..12` + * - [day] `1..31`, the upper bound can be less, depending on the month + * + * Use `LocalDate(year, month, day) to throw an exception + * instead of returning `null` when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.orNullMonthNumber + */ + public fun orNull(year: Int, month: Int, day: Int): LocalDate? + + /** + * Constructs a [LocalDate] instance from the given date components + * or returns `null` if a value is out of range or invalid. + * + * The supported ranges of components: + * - [year] the range at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] + * - [month] all values of the [Month] enum + * - [day] `1..31`, the upper bound can be less, depending on the month + * + * Use `LocalDate(year, month, day) to throw an exception + * instead of returning `null` when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.orNull + */ + public fun orNull(year: Int, month: Month, day: Int): LocalDate? /** * A shortcut for calling [DateTimeFormat.parse]. * @@ -177,6 +212,8 @@ public expect class LocalDate : Comparable { * * @throws IllegalArgumentException if any parameter is out of range or if [day] is invalid for the * given [month] and [year]. + * @see orNull for a version that returns `null` instead of throwing an exception + * when the parameters are invalid. * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunctionMonthNumber */ public constructor(year: Int, month: Int, day: Int) @@ -192,6 +229,8 @@ public expect class LocalDate : Comparable { * * @throws IllegalArgumentException if any parameter is out of range or if [day] is invalid for the * given [month] and [year]. + * @see orNull for a version that returns `null` instead of throwing an exception + * when the parameters are invalid. * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunction */ public constructor(year: Int, month: Month, day: Int) diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index a6569df3a..ca427a122 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -114,6 +114,67 @@ import kotlin.jvm.JvmName public expect class LocalDateTime : Comparable { public companion object { + /** + * Constructs a [LocalDateTime] instance from the given date and time components + * or returns `null` if a value is out of range. + * + * The components [month] and [day] are 1-based. + * + * The supported ranges of components: + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] + * - [month] `1..12` + * - [day] `1..31`, the upper bound can be less, depending on the month + * - [hour] `0..23` + * - [minute] `0..59` + * - [second] `0..59` + * - [nanosecond] `0..999_999_999` + * + * Use `LocalDateTime(year, month, day, hour, minute, second, nanosecond)` + * to throw an exception instead of returning `null` when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.orNull + */ + public fun orNull( + year: Int, + month: Int, + day: Int, + hour: Int, + minute: Int, + second: Int = 0, + nanosecond: Int = 0 + ): LocalDateTime? + + /** + * Constructs a [LocalDateTime] instance from the given date and time components + * or returns `null` if a value is out of range. + * + * The supported ranges of components: + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] + * - [month] all values of the [Month] enum + * - [day] `1..31`, the upper bound can be less, depending on the month + * - [hour] `0..23` + * - [minute] `0..59` + * - [second] `0..59` + * - [nanosecond] `0..999_999_999` + * + * Use `LocalDateTime(year, month, day, hour, minute, second, nanosecond)` + * to throw an exception instead of returning `null` when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.orNullWithMonth + */ + public fun orNull( + year: Int, + month: Month, + day: Int, + hour: Int, + minute: Int, + second: Int = 0, + nanosecond: Int = 0 + ): LocalDateTime? + + /** * A shortcut for calling [DateTimeFormat.parse]. * @@ -213,6 +274,7 @@ public expect class LocalDateTime : Comparable { * * @throws IllegalArgumentException if any parameter is out of range * or if [day] is invalid for the given [monthNumber] and [year]. + * @see orNull for a version that returns `null` instead of throwing an exception when the parameters are invalid. * * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunctionWithMonthNumber */ @@ -241,6 +303,7 @@ public expect class LocalDateTime : Comparable { * * @throws IllegalArgumentException if any parameter is out of range, * or if [day] is invalid for the given [month] and [year]. + * @see orNull for a version that returns `null` instead of throwing an exception when the parameters are invalid. * * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunction */ @@ -257,6 +320,7 @@ public expect class LocalDateTime : Comparable { /** * Constructs a [LocalDateTime] instance by combining the given [date] and [time] parts. * + * @see orNull for a version that returns `null` instead of throwing an exception when the parameters are invalid. * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.fromDateAndTime */ public constructor(date: LocalDate, time: LocalTime) diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 934a8e1d5..fb9f47dec 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -84,6 +84,23 @@ import kotlin.jvm.JvmName public expect class LocalTime : Comparable { public companion object { + /** + * Constructs a [LocalTime] instance from the given time components + * or returns `null` if a value is out of range. + * + * The supported ranges of components: + * - [hour] `0..23` + * - [minute] `0..59` + * - [second] `0..59` + * - [nanosecond] `0..999_999_999` + * + * Use `LocalTime(hour, minute, second, nanosecond)` + * to throw an exception instead of returning `null` when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.orNull + */ + public fun orNull(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalTime? + /** * A shortcut for calling [DateTimeFormat.parse]. * @@ -230,6 +247,7 @@ public expect class LocalTime : Comparable { * - [nanosecond] `0..999_999_999` * * @throws IllegalArgumentException if any parameter is out of range. + * @see orNull for a version that returns `null` instead of throwing an exception when the parameters are invalid. * @sample kotlinx.datetime.test.samples.LocalTimeSamples.constructorFunction */ public constructor(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0) diff --git a/core/common/src/Month.kt b/core/common/src/Month.kt index 9163d668d..4b9d86463 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -72,6 +72,8 @@ public val Month.number: Int get() = ordinal + 1 * @sample kotlinx.datetime.test.samples.MonthSamples.constructorFunction */ public fun Month(number: Int): Month { - require(number in 1..12) + require(number in 1..12) { + "Month must be a number between 1 and 12, got $number" + } return Month.entries[number - 1] } diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 8a5cc7696..df48d05d7 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -77,6 +77,27 @@ public expect class UtcOffset { */ public val ZERO: UtcOffset + /** + * Constructs a [UtcOffset] from hours, minutes, and seconds components + * or returns `null` if a value is out of range or invalid. + * + * All components must have the same sign. + * Otherwise, `null` will be returned. + * + * The bounds are checked: it is invalid to pass something other than `±[0; 59]` as the number of seconds or minutes; + * `null` will be returned if this rule is violated. + * For example, `UtcOffset.orNull(hours = 3, minutes = 61)` returns `null`. + * + * However, the non-null component of the highest order can exceed these bounds, + * for example, `UtcOffset.orNull(minutes = 241)` and `UtcOffset.orNull(seconds = -3600)` are both valid. + * + * Use `UtcOffset(hours, minutes, seconds)` to throw an exception instead of returning `null` + * when the parameters are invalid. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.orNull + */ + public fun orNull(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset? + /** * A shortcut for calling [DateTimeFormat.parse]. * @@ -210,6 +231,7 @@ public fun UtcOffset.format(format: DateTimeFormat): String = format. * @throws IllegalArgumentException if a component exceeds its bounds when a higher order component is specified. * @throws IllegalArgumentException if components have different signs. * @throws IllegalArgumentException if the resulting `UtcOffset` value is outside of range `±18:00`. + * @see UtcOffset.orNull for a version that returns `null` instead of throwing an exception when the parameters are invalid. * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.constructorFunction */ public expect fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset diff --git a/core/common/test/LocalDateTest.kt b/core/common/test/LocalDateTest.kt index f6ded203e..951965411 100644 --- a/core/common/test/LocalDateTest.kt +++ b/core/common/test/LocalDateTest.kt @@ -230,6 +230,21 @@ class LocalDateTest { } } + @Test + fun orNull() { + validDates.forEach { (year, month, day) -> + val expected = LocalDate(year, month, day) + assertEquals(expected, LocalDate.orNull(year, month, day)) + assertEquals(expected, LocalDate.orNull(year, Month(month), day)) + } + invalidDates.forEach { (year, month, day) -> + assertNull(LocalDate.orNull(year, month, day)) + runCatching { Month(month) }.onSuccess { monthEnum -> + assertNull(LocalDate.orNull(year, monthEnum, day)) + } + } + } + @Test fun fromEpochDays() { /** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native, @@ -285,17 +300,44 @@ class LocalDateTest { } fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate) { - assertFailsWith { constructor(2007, 2, 29) } - assertEquals(29, constructor(2008, 2, 29).day) - 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) } + invalidDates.forEach { (year, month, day) -> + assertFailsWith { constructor(year, month, day) } + } + validDates.forEach { (year, month, day) -> + val date = constructor(year, month, day) + assertEquals(year, date.year) + assertEquals(month, date.month.number) + assertEquals(day, date.day) + } } +val invalidDates = listOf( + Triple(2007, 2, 29), + Triple(2007, 4, 31), + Triple(2007, 1, 0), + Triple(2007, 1, 32), + Triple(Int.MIN_VALUE, 1, 1), + Triple(2007, 1, 32), + Triple(2007, 0, 1), + Triple(2007, 13, 1), +) + +val validDates = listOf( + Triple(2007, 1, 1), + Triple(2007, 2, 28), + Triple(2008, 2, 29), + Triple(2007, 3, 31), + Triple(2007, 4, 30), + Triple(2007, 5, 31), + Triple(2007, 6, 30), + Triple(2007, 7, 31), + Triple(2007, 8, 31), + Triple(2007, 9, 30), + Triple(2007, 10, 31), + Triple(2007, 11, 30), + Triple(2007, 12, 31), +) + private val LocalDate.next: LocalDate get() = if (day != month.number.monthLength(isLeapYear(year))) { LocalDate(year, month.number, day + 1) diff --git a/core/common/test/LocalDateTimeTest.kt b/core/common/test/LocalDateTimeTest.kt index 7d04b9134..bacf2dda4 100644 --- a/core/common/test/LocalDateTimeTest.kt +++ b/core/common/test/LocalDateTimeTest.kt @@ -130,6 +130,40 @@ class LocalDateTimeTest { assertFailsWith { localTime(0, 0, 0, 1_000_000_000) } } + @Test + fun orNull() {// Test orNull with month number + LocalDateTime.orNull(2020, 1, 1, 12, 30, 45, 500_000_000)?.let { + checkComponents(it, 2020, 1, 1, 12, 30, 45, 500_000_000) + } ?: fail("LocalDateTime.orNull should not return null") + + // Test orNull with Month enum + LocalDateTime.orNull(2020, Month.FEBRUARY, 29, 23, 59, 59, 999_999_999)?.let { + checkComponents(it, 2020, 2, 29, 23, 59, 59, 999_999_999) + } ?: fail("LocalDateTime.orNull should not return null") + + // Test invalid date components + for ((year, month, day) in invalidDates) { + assertNull(LocalDateTime.orNull(year, month, day, 12, 30)) + runCatching { Month(month) }.onSuccess { monthEnum -> + assertNull(LocalDateTime.orNull(year, monthEnum, day, 12, 30)) + } + } + + // Test invalid time components + for (input in invalidTimes) { + when (input.size) { + 2 -> assertNull(LocalDateTime.orNull(2024, 1, 1, input[0], input[1])) + 3 -> assertNull(LocalDateTime.orNull(2024, 1, 1, input[0], input[1], input[2])) + 4 -> assertNull(LocalDateTime.orNull(2024, 1, 1, input[0], input[1], input[2], input[3])) + } + } + + // Test with Month enum + assertNull(LocalDateTime.orNull(2021, Month.FEBRUARY, 29, 12, 30)) // Invalid day (not a leap year) + assertNull(LocalDateTime.orNull(2020, Month.FEBRUARY, 30, 12, 30)) // Invalid day for February + + } + } fun checkComponents(value: LocalDateTime, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0, dayOfWeek: Int? = null, dayOfYear: Int? = null) { diff --git a/core/common/test/LocalTimeTest.kt b/core/common/test/LocalTimeTest.kt index 09f0f5f6e..d24efcbf3 100644 --- a/core/common/test/LocalTimeTest.kt +++ b/core/common/test/LocalTimeTest.kt @@ -87,6 +87,31 @@ class LocalTimeTest { assertFailsWith { LocalTime(0, 0, 0, 1_000_000_000) } } + @Test + fun orNull() { + // Valid times should be created correctly + LocalTime.orNull(12, 30, 45, 500_000_000)?.let { + checkComponents(it, 12, 30, 45, 500_000_000) + } ?: fail("LocalTime.orNull should not return null") + + LocalTime.orNull(0, 0)?.let { + checkComponents(it, 0, 0) + } ?: fail("LocalTime.orNull should not return null") + + LocalTime.orNull(23, 59, 59, 999_999_999)?.let { + checkComponents(it, 23, 59, 59, 999_999_999) + } ?: fail("LocalTime.orNull should not return null") + + // Invalid times should return null + for (input in invalidTimes) { + when (input.size) { + 2 -> assertNull(LocalTime.orNull(input[0], input[1])) + 3 -> assertNull(LocalTime.orNull(input[0], input[1], input[2])) + 4 -> assertNull(LocalTime.orNull(input[0], input[1], input[2], input[3])) + } + } + } + @Test fun fromNanosecondOfDay() { val data = mapOf( @@ -203,6 +228,17 @@ class LocalTimeTest { } } +val invalidTimes = listOf( + listOf(-1, 0), // invalid hour + listOf(24, 0), // invalid hour + listOf(0, -1), // invalid minute + listOf(0, 60), // invalid minute + listOf(0, 0, -1), // invalid second + listOf(0, 0, 60), // invalid second + listOf(0, 0, 0, -1), // invalid nanosecond + listOf(0, 0, 0, 1_000_000_000) // invalid nanosecond +) + fun checkComponents(value: LocalTime, hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0) { assertEquals(hour, value.hour, "hours") assertEquals(minute, value.minute, "minutes") diff --git a/core/common/test/UtcOffsetTest.kt b/core/common/test/UtcOffsetTest.kt index c9d488fc2..9fbaf096e 100644 --- a/core/common/test/UtcOffsetTest.kt +++ b/core/common/test/UtcOffsetTest.kt @@ -46,35 +46,48 @@ class UtcOffsetTest { val offset = UtcOffset(hours, minutes, seconds) val offsetSeconds = UtcOffset(seconds = totalSeconds) val offsetMinutes = UtcOffset(minutes = totalMinutes, seconds = seconds) + val offsetOrNull = UtcOffset.orNull(hours, minutes, seconds) + val offsetSecondsOrNull = UtcOffset.orNull(seconds = totalSeconds) + val offsetMinutesOrNull = UtcOffset.orNull(minutes = totalMinutes, seconds = seconds) assertEquals(totalSeconds, offset.totalSeconds) assertEquals(offset, offsetMinutes) assertEquals(offset, offsetSeconds) + assertEquals(offset, offsetOrNull) + assertEquals(offset, offsetSecondsOrNull) + assertEquals(offset, offsetMinutesOrNull) } } @Test fun constructionErrors() { + fun assertInvalidUtcOffset( + hours: Int? = null, + minutes: Int? = null, + seconds: Int? = null, + ) { + assertIllegalArgument { UtcOffset(hours, minutes, seconds) } + assertNull(UtcOffset.orNull(hours, minutes, seconds)) + } // total range - assertIllegalArgument { UtcOffset(hours = -19) } - assertIllegalArgument { UtcOffset(hours = +19) } - assertIllegalArgument { UtcOffset(hours = -18, minutes = -1) } - assertIllegalArgument { UtcOffset(hours = -18, seconds = -1) } - assertIllegalArgument { UtcOffset(hours = +18, seconds = +1) } - assertIllegalArgument { UtcOffset(hours = +18, seconds = +1) } - assertIllegalArgument { UtcOffset(seconds = offsetSecondsRange.first - 1) } - assertIllegalArgument { UtcOffset(seconds = offsetSecondsRange.last + 1) } + assertInvalidUtcOffset(hours = -19) + assertInvalidUtcOffset(hours = +19) + assertInvalidUtcOffset(hours = -18, minutes = -1) + assertInvalidUtcOffset(hours = -18, seconds = -1) + assertInvalidUtcOffset(hours = +18, seconds = +1) + assertInvalidUtcOffset(seconds = offsetSecondsRange.first - 1) + assertInvalidUtcOffset(seconds = offsetSecondsRange.last + 1) // component ranges - assertIllegalArgument { UtcOffset(hours = 0, minutes = 60) } - assertIllegalArgument { UtcOffset(hours = 0, seconds = -60) } - assertIllegalArgument { UtcOffset(minutes = 90, seconds = 90) } - assertIllegalArgument { UtcOffset(minutes = 0, seconds = 90) } + assertInvalidUtcOffset(hours = 0, minutes = 60) + assertInvalidUtcOffset(hours = 0, seconds = -60) + assertInvalidUtcOffset(minutes = 90, seconds = 90) + assertInvalidUtcOffset(minutes = 0, seconds = 90) // component signs - assertIllegalArgument { UtcOffset(hours = +1, minutes = -1) } - assertIllegalArgument { UtcOffset(hours = +1, seconds = -1) } - assertIllegalArgument { UtcOffset(hours = -1, minutes = +1) } - assertIllegalArgument { UtcOffset(hours = -1, seconds = +1) } - assertIllegalArgument { UtcOffset(minutes = +1, seconds = -1) } - assertIllegalArgument { UtcOffset(minutes = -1, seconds = +1) } + assertInvalidUtcOffset(hours = +1, minutes = -1) + assertInvalidUtcOffset(hours = +1, seconds = -1) + assertInvalidUtcOffset(hours = -1, minutes = +1) + assertInvalidUtcOffset(hours = -1, seconds = +1) + assertInvalidUtcOffset(minutes = +1, seconds = -1) + assertInvalidUtcOffset(minutes = -1, seconds = +1) } @Test diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index 5beaf9295..e9bc6d1a6 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -69,6 +69,26 @@ class LocalDateSamples { check(date.day == 16) } + @Test + fun orNullMonthNumber() { + // Constructing a LocalDate value using `orNull` + val date = LocalDate.orNull(2024, 4, 16) + // For valid values, `orNull` is equivalent to the constructor + check(date == LocalDate(2024, 4, 16)) + // If a value can not be constructed, null is returned + check(LocalDate.orNull(2024, 1, 99) == null) + } + + @Test + fun orNull() { + // Constructing a LocalDate value using `orNull` + val date = LocalDate.orNull(2024, Month.APRIL, 16) + // For valid values, `orNull` is equivalent to the constructor + check(date == LocalDate(2024, Month.APRIL, 16)) + // If a value can not be constructed, null is returned + check(LocalDate.orNull(2024, Month.FEBRUARY, 31) == null) + } + @Test fun year() { // Getting the year diff --git a/core/common/test/samples/LocalDateTimeSamples.kt b/core/common/test/samples/LocalDateTimeSamples.kt index eea04c301..93fad2098 100644 --- a/core/common/test/samples/LocalDateTimeSamples.kt +++ b/core/common/test/samples/LocalDateTimeSamples.kt @@ -183,6 +183,32 @@ class LocalDateTimeSamples { check(LocalDate(2024, 2, 15).atTime(16, 48, 15, 120_000_000).toString() == "2024-02-15T16:48:15.120") } + @Test + fun orNull() { + // Constructing a LocalDateTime value using `orNull` + val dateTime = LocalDateTime.orNull(2024, 2, 15, 16, 48, 59, 999_999_999) + // For valid values, `orNull` is equivalent to the constructor + check(dateTime == LocalDateTime(2024, 2, 15, 16, 48, 59, 999_999_999)) + // If a value can not be constructed, null is returned + check(LocalDateTime.orNull(2024, 2, 31, 16, 48) == null) // Invalid day + check(LocalDateTime.orNull(2024, 2, 15, 24, 48) == null) // Invalid hour + check(LocalDateTime.orNull(2024, 2, 15, 16, 60) == null) // Invalid minute + check(LocalDateTime.orNull(2024, 2, 15, 16, 48, 60) == null) // Invalid second + check(LocalDateTime.orNull(2024, 2, 15, 16, 48, 59, 1_000_000_000) == null) // Invalid nanosecond + } + + @Test + fun orNullWithMonth() { + // Constructing a LocalDateTime value using `orNull` with Month enum + val dateTime = LocalDateTime.orNull(2024, Month.FEBRUARY, 15, 16, 48, 59, 999_999_999) + // For valid values, `orNull` is equivalent to the constructor + check(dateTime == LocalDateTime(2024, Month.FEBRUARY, 15, 16, 48, 59, 999_999_999)) + // If a value can not be constructed, null is returned + check(LocalDateTime.orNull(2024, Month.FEBRUARY, 31, 16, 48) == null) // Invalid day + check(LocalDateTime.orNull(2024, Month.FEBRUARY, 15, 24, 48) == null) // Invalid hour + } + + @Test fun formatting() { // Formatting LocalDateTime values using predefined and custom formats diff --git a/core/common/test/samples/LocalTimeSamples.kt b/core/common/test/samples/LocalTimeSamples.kt index 3bbb19e89..1680dd4db 100644 --- a/core/common/test/samples/LocalTimeSamples.kt +++ b/core/common/test/samples/LocalTimeSamples.kt @@ -133,6 +133,19 @@ class LocalTimeSamples { check(timeWithoutSeconds.nanosecond == 0) } + @Test + fun orNull() { + // Constructing a LocalTime value using `orNull` + val time = LocalTime.orNull(8, 30, 15, 123_456_789) + // For valid values, `orNull` is equivalent to the constructor + check(time == LocalTime(8, 30, 15, 123_456_789)) + // If a value can not be constructed, null is returned + check(LocalTime.orNull(24, 30) == null) + check(LocalTime.orNull(8, 60) == null) + check(LocalTime.orNull(8, 30, 60) == null) + check(LocalTime.orNull(8, 30, 15, 1_000_000_000) == null) + } + @Test fun hour() { // Getting the number of whole hours shown on the clock diff --git a/core/common/test/samples/UtcOffsetSamples.kt b/core/common/test/samples/UtcOffsetSamples.kt index ec651c71d..7c91b4c1f 100644 --- a/core/common/test/samples/UtcOffsetSamples.kt +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -108,6 +108,24 @@ class UtcOffsetSamples { } } + @Test + fun orNull() { + // Using the orNull function to create UtcOffset values + val validOffset = UtcOffset.orNull(hours = 3, minutes = 30) + check(validOffset != null) + check(validOffset.totalSeconds == 12600) + + // For valid inputs, orNull returns a non-null value + check(UtcOffset.orNull(seconds = -3600) == UtcOffset(hours = -1)) + + // For invalid inputs, orNull returns null + check(UtcOffset.orNull(hours = 1, minutes = 60) == null) + // Since `hours` is positive, `minutes` must also be positive + check(UtcOffset.orNull(hours = 1, minutes = -30) == null) + // The total offset must be within the range ±18:00 + check(UtcOffset.orNull(hours = 19) == null) + } + class Formats { @Test fun isoBasic() { diff --git a/core/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index e4af60709..757b37f25 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -23,7 +23,14 @@ private fun isValidYear(year: Int): Boolean = year >= YEAR_MIN && year <= YEAR_MAX @Serializable(with = LocalDateSerializer::class) -public actual class LocalDate actual constructor(public actual val year: Int, month: Int, public actual val day: Int) : Comparable { +public actual class LocalDate private constructor( + public actual val year: Int, month: Int, public actual val day: Int, unit: Unit +) : Comparable { + + public actual constructor(year: Int, month: Int, day: Int): this(year, month, day, Unit) { + require(_month in 1..12) { "Invalid date: month must be a number between 1 and 12, got $_month" } + validateYearAndDay() + } private val _month: Int = month @Deprecated("Use the 'month' property instead", ReplaceWith("this.month.number"), level = DeprecationLevel.WARNING) @@ -31,28 +38,40 @@ public actual class LocalDate actual constructor(public actual val year: Int, mo @Deprecated("Use the 'day' property instead", ReplaceWith("this.day"), level = DeprecationLevel.WARNING) public actual val dayOfMonth: Int get() = day - init { + public actual constructor(year: Int, month: Month, day: Int) : this(year, month.number, day, Unit) { + validateYearAndDay() + } + + private fun validateYearAndDay() { // org.threeten.bp.LocalDate#create require(isValidYear(year)) { "Invalid date: the year is out of range" } - require(_month in 1..12) { "Invalid date: month must be a number between 1 and 12, got $_month" } require(day in 1..31) { "Invalid date: day of month must be a number between 1 and 31, got $day" } if (day > 28 && day > _month.monthLength(isLeapYear(year))) { if (day == 29) { throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year") } else { - throw IllegalArgumentException("Invalid date '${Month(month)} $day'") + throw IllegalArgumentException("Invalid date '$month $day'") } } } - public actual constructor(year: Int, month: Month, day: Int) : this(year, month.number, day) - public actual companion object { public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDate = format.parse(input) @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) public fun parse(isoString: String): LocalDate = parse(input = isoString) + public actual fun orNull(year: Int, month: Int, day: Int): LocalDate? = + if (!isValidYear(year) || month !in 1..12 || day !in 1..31 || + (day > 28 && day > month.monthLength(isLeapYear(year)))) { + null + } else { + LocalDate(year, month, day, Unit) + } + + public actual fun orNull(year: Int, month: Month, day: Int): LocalDate? = + orNull(year, month.number, day) + // org.threeten.bp.LocalDate#toEpochDay public actual fun fromEpochDays(epochDays: Long): LocalDate { // LocalDate(-999_999_999, 1, 1).toEpochDay(), LocalDate(999_999_999, 12, 31).toEpochDay() @@ -87,7 +106,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, mo val dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1 yearEst += marchMonth0 / 10 - return LocalDate(yearEst.toInt(), month, dom) + // The range of valid values was checked in the beginning + return LocalDate(yearEst.toInt(), month, dom, Unit) } public actual fun fromEpochDays(epochDays: Int): LocalDate = fromEpochDays(epochDays.toLong()) @@ -160,11 +180,13 @@ public actual class LocalDate actual constructor(public actual val year: Int, mo // org.threeten.bp.LocalDate#resolvePreviousValid /** + * May only be called if the year and month are valid. + * * @throws IllegalArgumentException if the result exceeds the boundaries */ private fun resolvePreviousValid(year: Int, month: Int, day: Int): LocalDate { val newDay = min(day, month.monthLength(isLeapYear(year))) - return LocalDate(year, month, newDay) + return LocalDate(year, month, newDay, Unit) } // org.threeten.bp.LocalDate#plusMonths diff --git a/core/commonKotlin/src/LocalDateTime.kt b/core/commonKotlin/src/LocalDateTime.kt index 4a7e23853..39539c546 100644 --- a/core/commonKotlin/src/LocalDateTime.kt +++ b/core/commonKotlin/src/LocalDateTime.kt @@ -17,6 +17,35 @@ import kotlinx.serialization.* public actual class LocalDateTime public actual constructor(public actual val date: LocalDate, public actual val time: LocalTime) : Comparable { public actual companion object { + public actual fun orNull( + year: Int, + month: Int, + day: Int, + hour: Int, + minute: Int, + second: Int, + nanosecond: Int + ): LocalDateTime? { + val date = LocalDate.orNull(year, month, day) ?: return null + val time = LocalTime.orNull(hour, minute, second, nanosecond) ?: return null + return LocalDateTime(date, time) + } + + public actual fun orNull( + year: Int, + month: Month, + day: Int, + hour: Int, + minute: Int, + second: Int, + nanosecond: Int + ): LocalDateTime? { + val date = LocalDate.orNull(year, month, day) ?: return null + val time = LocalTime.orNull(hour, minute, second, nanosecond) ?: return null + return LocalDateTime(date, time) + } + + public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDateTime = format.parse(input) @@ -36,10 +65,10 @@ public actual constructor(public actual val date: LocalDate, public actual val t } public actual constructor(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : - this(LocalDate(year, month, day), LocalTime.of(hour, minute, second, nanosecond)) + this(LocalDate(year, month, day), LocalTime(hour, minute, second, nanosecond)) public actual constructor(year: Int, month: Month, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : - this(LocalDate(year, month, day), LocalTime.of(hour, minute, second, nanosecond)) + this(LocalDate(year, month, day), LocalTime(hour, minute, second, nanosecond)) public actual val year: Int get() = date.year @Deprecated("Use the 'month' property instead", ReplaceWith("this.month.number"), level = DeprecationLevel.WARNING) @@ -77,7 +106,7 @@ public actual constructor(public actual val date: LocalDate, public actual val t // org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond internal fun toEpochSecond(offset: UtcOffset): Long { - val epochDay = date.toEpochDays().toLong() + val epochDay = date.toEpochDays() var secs: Long = epochDay * 86400 + time.toSecondOfDay() secs -= offset.totalSeconds return secs diff --git a/core/commonKotlin/src/LocalTime.kt b/core/commonKotlin/src/LocalTime.kt index 42f5c0177..87fba6a74 100644 --- a/core/commonKotlin/src/LocalTime.kt +++ b/core/commonKotlin/src/LocalTime.kt @@ -14,14 +14,17 @@ import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable @Serializable(LocalTimeSerializer::class) -public actual class LocalTime actual constructor( +public actual class LocalTime private constructor( public actual val hour: Int, public actual val minute: Int, public actual val second: Int, - public actual val nanosecond: Int - ) : Comparable { + public actual val nanosecond: Int, + unit: Unit, +) : Comparable { - init { + public actual constructor( + hour: Int, minute: Int, second: Int, nanosecond: Int + ) : this(hour, minute, second, nanosecond, Unit) { fun check(value: Int, lower: Int, upper: Int, str: String) = require(value in lower..upper) { "Invalid time: $str must be a number between $lower and $upper, got $value" @@ -33,6 +36,13 @@ public actual class LocalTime actual constructor( } public actual companion object { + public actual fun orNull(hour: Int, minute: Int, second: Int, nanosecond: Int): LocalTime? = + if (hour !in 0..23 || minute !in 0..59 || second !in 0..59 || nanosecond !in 0 until NANOS_PER_ONE) { + null + } else { + LocalTime(hour, minute, second, nanosecond, Unit) + } + public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalTime = format.parse(input) @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) @@ -49,22 +59,25 @@ public actual class LocalTime actual constructor( // org.threeten.bp.LocalTime#ofSecondOfDay(long, int) internal fun ofSecondOfDay(secondOfDay: Int, nanoOfSecond: Int): LocalTime { - require(secondOfDay in 0 until SECONDS_PER_DAY) - require(nanoOfSecond in 0 until NANOS_PER_ONE) + require(secondOfDay in 0 until SECONDS_PER_DAY) { + "Invalid time: secondOfDay must be between 0 and $SECONDS_PER_DAY, got $secondOfDay" + } + require(nanoOfSecond in 0 until NANOS_PER_ONE) { + "Invalid time: nanosecondOfSecond must be between 0 and $NANOS_PER_ONE, got $nanoOfSecond" + } val hours = (secondOfDay / SECONDS_PER_HOUR) val secondWithoutHours = secondOfDay - hours * SECONDS_PER_HOUR val minutes = (secondWithoutHours / SECONDS_PER_MINUTE) val second = secondWithoutHours - minutes * SECONDS_PER_MINUTE - return LocalTime(hours, minutes, second, nanoOfSecond) - } - - internal fun of(hour: Int, minute: Int, second: Int, nanosecond: Int): LocalTime { - return LocalTime(hour, minute, second, nanosecond) + // The range of valid values was checked in the require statements above + return LocalTime(hours, minutes, second, nanoOfSecond, Unit) } // org.threeten.bp.LocalTime#ofNanoOfDay internal fun ofNanoOfDay(nanoOfDay: Long): LocalTime { - require(nanoOfDay >= 0 && nanoOfDay < SECONDS_PER_DAY.toLong() * NANOS_PER_ONE) + require(nanoOfDay >= 0 && nanoOfDay < SECONDS_PER_DAY.toLong() * NANOS_PER_ONE) { + "Invalid time: nanosecondOfDay must be between 0 and 86_400_000_000_000, got $nanoOfDay" + } var newNanoOfDay = nanoOfDay val hours = (newNanoOfDay / NANOS_PER_HOUR).toInt() newNanoOfDay -= hours * NANOS_PER_HOUR @@ -72,7 +85,8 @@ public actual class LocalTime actual constructor( newNanoOfDay -= minutes * NANOS_PER_MINUTE val seconds = (newNanoOfDay / NANOS_PER_ONE).toInt() newNanoOfDay -= seconds * NANOS_PER_ONE - return LocalTime(hours, minutes, seconds, newNanoOfDay.toInt()) + // The range of valid values was checked in the require statement + return LocalTime(hours, minutes, seconds, newNanoOfDay.toInt(), Unit) } internal actual val MIN: LocalTime = LocalTime(0, 0, 0, 0) diff --git a/core/commonKotlin/src/UtcOffset.kt b/core/commonKotlin/src/UtcOffset.kt index e947e88fd..4eb9198c8 100644 --- a/core/commonKotlin/src/UtcOffset.kt +++ b/core/commonKotlin/src/UtcOffset.kt @@ -24,15 +24,25 @@ public actual class UtcOffset private constructor(public actual val totalSeconds public actual val ZERO: UtcOffset = UtcOffset(totalSeconds = 0) + public actual fun orNull(hours: Int?, minutes: Int?, seconds: Int?): UtcOffset? = when { + hours != null -> + ofHoursMinutesSecondsOrNull(hours, minutes ?: 0, seconds ?: 0) + minutes != null -> + ofHoursMinutesSecondsOrNull(minutes / MINUTES_PER_HOUR, minutes % MINUTES_PER_HOUR, seconds ?: 0) + else -> + ofSecondsOrNull(seconds ?: 0) + } + public actual fun parse(input: CharSequence, format: DateTimeFormat): UtcOffset = format.parse(input) @Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN) public fun parse(offsetString: String): UtcOffset = parse(input = offsetString) + private fun totalSecondsValid(totalSeconds: Int): Boolean = + totalSeconds in -18 * SECONDS_PER_HOUR..18 * SECONDS_PER_HOUR + private fun validateTotal(totalSeconds: Int) { - if (totalSeconds !in -18 * SECONDS_PER_HOUR .. 18 * SECONDS_PER_HOUR) { - throw IllegalArgumentException("Total seconds value is out of range: $totalSeconds") - } + require(totalSecondsValid(totalSeconds)) { "Total seconds value is out of range: $totalSeconds" } } // org.threeten.bp.ZoneOffset#validate @@ -65,23 +75,48 @@ public actual class UtcOffset private constructor(public actual val totalSeconds } } + private fun hoursMinutesSecondsValid(hours: Int, minutes: Int, seconds: Int): Boolean = + // valid range for hours, minutes and seconds + hours in -18..18 && minutes in -59..59 && seconds in -59..59 + // same sign for all components + && !(hours > 0 && (minutes < 0 || seconds < 0)) + && !(hours < 0 && (minutes > 0 || seconds > 0)) + // same sign for minutes and seconds + && !(minutes > 0 && seconds < 0 || minutes < 0 && seconds > 0) + // valid range for total seconds + && !(abs(hours) == 18 && (abs(minutes) > 0 || abs(seconds) > 0)) + // org.threeten.bp.ZoneOffset#ofHoursMinutesSeconds + internal fun ofHoursMinutesSecondsUnsafe(hours: Int, minutes: Int, seconds: Int): UtcOffset = + if (hours == 0 && minutes == 0 && seconds == 0) ZERO + else ofSeconds(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds) + internal fun ofHoursMinutesSeconds(hours: Int, minutes: Int, seconds: Int): UtcOffset { validate(hours, minutes, seconds) - return if (hours == 0 && minutes == 0 && seconds == 0) ZERO - else ofSeconds(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds) + return ofHoursMinutesSecondsUnsafe(hours, minutes, seconds) } - // org.threeten.bp.ZoneOffset#ofTotalSeconds - internal fun ofSeconds(seconds: Int): UtcOffset { - validateTotal(seconds) - return if (seconds % (15 * SECONDS_PER_MINUTE) == 0) { - utcOffsetCache[seconds] ?: UtcOffset(totalSeconds = seconds).also { utcOffsetCache[seconds] = it } + internal fun ofHoursMinutesSecondsOrNull(hours: Int, minutes: Int, seconds: Int): UtcOffset? = + if (hoursMinutesSecondsValid(hours, minutes, seconds)) { + ofHoursMinutesSecondsUnsafe(hours, minutes, seconds) } else { - UtcOffset(totalSeconds = seconds) + null } + + // org.threeten.bp.ZoneOffset#ofTotalSeconds + private fun ofSecondsUnsafe(seconds: Int): UtcOffset = if (seconds % (15 * SECONDS_PER_MINUTE) == 0) { + utcOffsetCache[seconds] ?: UtcOffset(totalSeconds = seconds).also { utcOffsetCache[seconds] = it } + } else { + UtcOffset(totalSeconds = seconds) } + internal fun ofSeconds(seconds: Int): UtcOffset = validateTotal(seconds).let { + ofSecondsUnsafe(seconds) + } + + internal fun ofSecondsOrNull(seconds: Int): UtcOffset? = + if (totalSecondsValid(seconds)) ofSecondsUnsafe(seconds) else null + @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithUtcOffset.() -> Unit): DateTimeFormat = UtcOffsetFormat.build(block) diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index 4e1c19a56..f51b16e9b 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -22,6 +22,20 @@ public actual class LocalDate internal constructor( internal val value: jtLocalDate ) : Comparable, java.io.Serializable { public actual companion object { + public actual fun orNull(year: Int, month: Int, day: Int): LocalDate? = + try { + LocalDate(year, month, day) + } catch (e: IllegalArgumentException) { + null + } + + public actual fun orNull(year: Int, month: Month, day: Int): LocalDate? = + try { + LocalDate(year, month, day) + } catch (e: IllegalArgumentException) { + null + } + public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDate = if (format === Formats.ISO) { try { diff --git a/core/jvm/src/LocalDateTimeJvm.kt b/core/jvm/src/LocalDateTimeJvm.kt index 30a710af2..4179cd0b6 100644 --- a/core/jvm/src/LocalDateTimeJvm.kt +++ b/core/jvm/src/LocalDateTimeJvm.kt @@ -85,6 +85,31 @@ public actual class LocalDateTime internal constructor( actual override fun compareTo(other: LocalDateTime): Int = this.value.compareTo(other.value) public actual companion object { + public actual fun orNull( + year: Int, + month: Int, + day: Int, + hour: Int, + minute: Int, + second: Int, + nanosecond: Int + ): LocalDateTime? = try { + jtLocalDateTime.of(year, month, day, hour, minute, second, nanosecond).let(::LocalDateTime) + } catch (e: DateTimeException) { + null + } + + public actual fun orNull( + year: Int, + month: Month, + day: Int, + hour: Int, + minute: Int, + second: Int, + nanosecond: Int + ): LocalDateTime? = orNull(year, month.number, day, hour, minute, second, nanosecond) + + public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDateTime = if (format === Formats.ISO) { try { diff --git a/core/jvm/src/LocalTimeJvm.kt b/core/jvm/src/LocalTimeJvm.kt index 9d815fe15..f58d4e737 100644 --- a/core/jvm/src/LocalTimeJvm.kt +++ b/core/jvm/src/LocalTimeJvm.kt @@ -47,6 +47,12 @@ public actual class LocalTime internal constructor( actual override fun compareTo(other: LocalTime): Int = this.value.compareTo(other.value) public actual companion object { + public actual fun orNull(hour: Int, minute: Int, second: Int, nanosecond: Int): LocalTime? = try { + jtLocalTime.of(hour, minute, second, nanosecond).let(::LocalTime) + } catch (_: DateTimeException) { + null + } + public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalTime = if (format === Formats.ISO) { try { diff --git a/core/jvm/src/UtcOffsetJvm.kt b/core/jvm/src/UtcOffsetJvm.kt index 40908fb33..526a06af4 100644 --- a/core/jvm/src/UtcOffsetJvm.kt +++ b/core/jvm/src/UtcOffsetJvm.kt @@ -27,6 +27,12 @@ public actual class UtcOffset( public actual companion object { public actual val ZERO: UtcOffset = UtcOffset(ZoneOffset.UTC) + public actual fun orNull(hours: Int?, minutes: Int?, seconds: Int?): UtcOffset? = try { + UtcOffset(hours, minutes, seconds) + } catch (_: IllegalArgumentException) { + null + } + public actual fun parse(input: CharSequence, format: DateTimeFormat): UtcOffset = when { format === Formats.ISO -> parseWithFormat(input, isoFormat) format === Formats.ISO_BASIC -> parseWithFormat(input, isoBasicFormat)