Skip to content

Commit e1eaf93

Browse files
committed
Add LocalDate.createOrNull
1 parent 90be731 commit e1eaf93

File tree

7 files changed

+129
-9
lines changed

7 files changed

+129
-9
lines changed

core/api/kotlinx-datetime.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ public final class kotlinx/datetime/LocalDate : java/io/Serializable, java/lang/
335335

336336
public final class kotlinx/datetime/LocalDate$Companion {
337337
public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat;
338+
public final fun createOrNull (III)Lkotlinx/datetime/LocalDate;
339+
public final fun createOrNull (ILkotlinx/datetime/Month;I)Lkotlinx/datetime/LocalDate;
338340
public final fun fromEpochDays (I)Lkotlinx/datetime/LocalDate;
339341
public final fun fromEpochDays (J)Lkotlinx/datetime/LocalDate;
340342
public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalDate;

core/api/kotlinx-datetime.klib.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ final class kotlinx.datetime/LocalDate : kotlin/Comparable<kotlinx.datetime/Loca
431431

432432
final object Companion { // kotlinx.datetime/LocalDate.Companion|null[0]
433433
final fun Format(kotlin/Function1<kotlinx.datetime.format/DateTimeFormatBuilder.WithDate, kotlin/Unit>): kotlinx.datetime.format/DateTimeFormat<kotlinx.datetime/LocalDate> // kotlinx.datetime/LocalDate.Companion.Format|Format(kotlin.Function1<kotlinx.datetime.format.DateTimeFormatBuilder.WithDate,kotlin.Unit>){}[0]
434+
final fun createOrNull(kotlin/Int, kotlin/Int, kotlin/Int): kotlinx.datetime/LocalDate? // kotlinx.datetime/LocalDate.Companion.createOrNull|createOrNull(kotlin.Int;kotlin.Int;kotlin.Int){}[0]
435+
final fun createOrNull(kotlin/Int, kotlinx.datetime/Month, kotlin/Int): kotlinx.datetime/LocalDate? // kotlinx.datetime/LocalDate.Companion.createOrNull|createOrNull(kotlin.Int;kotlinx.datetime.Month;kotlin.Int){}[0]
434436
final fun fromEpochDays(kotlin/Int): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Int){}[0]
435437
final fun fromEpochDays(kotlin/Long): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Long){}[0]
436438
final fun parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat<kotlinx.datetime/LocalDate> = ...): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat<kotlinx.datetime.LocalDate>){}[0]

core/common/src/LocalDate.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,35 @@ import kotlin.internal.*
7272
@Serializable(with = LocalDateSerializer::class)
7373
public expect class LocalDate : Comparable<LocalDate> {
7474
public companion object {
75+
/**
76+
* Constructs a [LocalDate] instance from the given date components
77+
* or returns `null` if a value is out of range or invalid.
78+
*
79+
* The components [month] and [day] are 1-based.
80+
*
81+
* The supported ranges of components:
82+
* - [year] the range is at least enough to represent dates of all instants between
83+
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]
84+
* - [month] `1..12`
85+
* - [day] `1..31`, the upper bound can be less, depending on the month
86+
*
87+
* @sample kotlinx.datetime.test.samples.LocalDateSamples.createOrNullMonthNumber
88+
*/
89+
public fun createOrNull(year: Int, month: Int, day: Int): LocalDate?
90+
91+
/**
92+
* Constructs a [LocalDate] instance from the given date components
93+
* or returns `null` if a value is out of range or invalid.
94+
*
95+
* The supported ranges of components:
96+
* - [year] the range at least is enough to represent dates of all instants between
97+
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]
98+
* - [month] all values of the [Month] enum
99+
* - [day] `1..31`, the upper bound can be less, depending on the month
100+
*
101+
* @sample kotlinx.datetime.test.samples.LocalDateSamples.createOrNull
102+
*/
103+
public fun createOrNull(year: Int, month: Month, day: Int): LocalDate?
75104
/**
76105
* A shortcut for calling [DateTimeFormat.parse].
77106
*

core/common/test/LocalDateTest.kt

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,21 @@ class LocalDateTest {
231231
}
232232
}
233233

234+
@Test
235+
fun createOrNull() {
236+
validDates.forEach { (year, month, day) ->
237+
val expected = LocalDate(year, month, day)
238+
assertEquals(expected, LocalDate.createOrNull(year, month, day))
239+
assertEquals(expected, LocalDate.createOrNull(year, Month(month), day))
240+
}
241+
invalidDates.forEach { (year, month, day) ->
242+
assertNull(LocalDate.createOrNull(year, month, day))
243+
runCatching { Month(month) }.onSuccess { monthEnum ->
244+
assertNull(LocalDate.createOrNull(year, monthEnum, day))
245+
}
246+
}
247+
}
248+
234249
@Test
235250
fun fromEpochDays() {
236251
/** This test uses [LocalDate.next] and [LocalDate.previous] and not [LocalDate.plus] because, on Native,
@@ -286,17 +301,44 @@ class LocalDateTest {
286301
}
287302

288303
fun checkInvalidDate(constructor: (year: Int, month: Int, day: Int) -> LocalDate) {
289-
assertFailsWith<IllegalArgumentException> { constructor(2007, 2, 29) }
290-
assertEquals(29, constructor(2008, 2, 29).day)
291-
assertFailsWith<IllegalArgumentException> { constructor(2007, 4, 31) }
292-
assertFailsWith<IllegalArgumentException> { constructor(2007, 1, 0) }
293-
assertFailsWith<IllegalArgumentException> { constructor(2007,1, 32) }
294-
assertFailsWith<IllegalArgumentException> { constructor(Int.MIN_VALUE, 1, 1) }
295-
assertFailsWith<IllegalArgumentException> { constructor(2007, 1, 32) }
296-
assertFailsWith<IllegalArgumentException> { constructor(2007, 0, 1) }
297-
assertFailsWith<IllegalArgumentException> { constructor(2007, 13, 1) }
304+
invalidDates.forEach { (year, month, day) ->
305+
assertFailsWith<IllegalArgumentException> { constructor(year, month, day) }
306+
}
307+
validDates.forEach { (year, month, day) ->
308+
val date = constructor(year, month, day)
309+
assertEquals(year, date.year)
310+
assertEquals(month, date.month.number)
311+
assertEquals(day, date.day)
312+
}
298313
}
299314

315+
val invalidDates = listOf(
316+
Triple(2007, 2, 29),
317+
Triple(2007, 4, 31),
318+
Triple(2007, 1, 0),
319+
Triple(2007, 1, 32),
320+
Triple(Int.MIN_VALUE, 1, 1),
321+
Triple(2007, 1, 32),
322+
Triple(2007, 0, 1),
323+
Triple(2007, 13, 1),
324+
)
325+
326+
val validDates = listOf(
327+
Triple(2007, 1, 1),
328+
Triple(2007, 2, 28),
329+
Triple(2008, 2, 29),
330+
Triple(2007, 3, 31),
331+
Triple(2007, 4, 30),
332+
Triple(2007, 5, 31),
333+
Triple(2007, 6, 30),
334+
Triple(2007, 7, 31),
335+
Triple(2007, 8, 31),
336+
Triple(2007, 9, 30),
337+
Triple(2007, 10, 31),
338+
Triple(2007, 11, 30),
339+
Triple(2007, 12, 31),
340+
)
341+
300342
private val LocalDate.next: LocalDate get() =
301343
if (day != month.number.monthLength(isLeapYear(year))) {
302344
LocalDate(year, month.number, day + 1)

core/common/test/samples/LocalDateSamples.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ class LocalDateSamples {
6969
check(date.day == 16)
7070
}
7171

72+
@Test
73+
fun createOrNullMonthNumber() {
74+
// Constructing a LocalDate value using `createOrNull`
75+
val date = LocalDate.createOrNull(2024, 4, 16)
76+
// For valid values, `createOrNull` is equivalent to the constructor
77+
check(date == LocalDate(2024, 4, 16))
78+
// If a value can not be constructed, null is returned
79+
check(LocalDate.createOrNull(2024, 1, 99) == null)
80+
}
81+
82+
@Test
83+
fun createOrNull() {
84+
// Constructing a LocalDate value using `createOrNull`
85+
val date = LocalDate.createOrNull(2024, Month.APRIL, 16)
86+
// For valid values, `createOrNull` is equivalent to the constructor
87+
check(date == LocalDate(2024, Month.APRIL, 16))
88+
// If a value can not be constructed, null is returned
89+
check(LocalDate.createOrNull(2024, Month.FEBRUARY, 31) == null)
90+
}
91+
7292
@Test
7393
fun year() {
7494
// Getting the year

core/commonKotlin/src/LocalDate.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ public actual class LocalDate actual constructor(public actual val year: Int, mo
5353
@Deprecated("This overload is only kept for binary compatibility", level = DeprecationLevel.HIDDEN)
5454
public fun parse(isoString: String): LocalDate = parse(input = isoString)
5555

56+
public actual fun createOrNull(year: Int, month: Int, day: Int): LocalDate? =
57+
if (!isValidYear(year) || month !in 1..12 || day !in 1..31 ||
58+
(day > 28 && day > month.monthLength(isLeapYear(year)))) {
59+
null
60+
} else {
61+
LocalDate(year, month, day)
62+
}
63+
64+
public actual fun createOrNull(year: Int, month: Month, day: Int): LocalDate? =
65+
createOrNull(year, month.number, day)
66+
5667
// org.threeten.bp.LocalDate#toEpochDay
5768
public actual fun fromEpochDays(epochDays: Long): LocalDate {
5869
// LocalDate(-999_999_999, 1, 1).toEpochDay(), LocalDate(999_999_999, 12, 31).toEpochDay()

core/jvm/src/LocalDate.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ public actual class LocalDate internal constructor(
2222
internal val value: jtLocalDate
2323
) : Comparable<LocalDate>, java.io.Serializable {
2424
public actual companion object {
25+
public actual fun createOrNull(year: Int, month: Int, day: Int): LocalDate? =
26+
try {
27+
LocalDate(year, month, day)
28+
} catch (e: IllegalArgumentException) {
29+
null
30+
}
31+
32+
public actual fun createOrNull(year: Int, month: Month, day: Int): LocalDate? =
33+
try {
34+
LocalDate(year, month, day)
35+
} catch (e: IllegalArgumentException) {
36+
null
37+
}
38+
2539
public actual fun parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
2640
if (format === Formats.ISO) {
2741
try {

0 commit comments

Comments
 (0)