Skip to content

Commit 8960d11

Browse files
committed
Add LocalDate.createOrNull
1 parent 7d2764a commit 8960d11

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
@@ -297,6 +297,8 @@ public final class kotlinx/datetime/LocalDate : java/io/Serializable, java/lang/
297297

298298
public final class kotlinx/datetime/LocalDate$Companion {
299299
public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat;
300+
public final fun createOrNull (III)Lkotlinx/datetime/LocalDate;
301+
public final fun createOrNull (ILkotlinx/datetime/Month;I)Lkotlinx/datetime/LocalDate;
300302
public final fun fromEpochDays (I)Lkotlinx/datetime/LocalDate;
301303
public final fun fromEpochDays (J)Lkotlinx/datetime/LocalDate;
302304
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
@@ -360,6 +360,8 @@ final class kotlinx.datetime/LocalDate : kotlin/Comparable<kotlinx.datetime/Loca
360360

361361
final object Companion { // kotlinx.datetime/LocalDate.Companion|null[0]
362362
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]
363+
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]
364+
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]
363365
final fun fromEpochDays(kotlin/Int): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Int){}[0]
364366
final fun fromEpochDays(kotlin/Long): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDate.Companion.fromEpochDays|fromEpochDays(kotlin.Long){}[0]
365367
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
@@ -68,6 +68,35 @@ import kotlin.internal.*
6868
@Serializable(with = LocalDateIso8601Serializer::class)
6969
public expect class LocalDate : Comparable<LocalDate> {
7070
public companion object {
71+
/**
72+
* Constructs a [LocalDate] instance from the given date components
73+
* or returns `null` if a value is out of range or invalid.
74+
*
75+
* The components [month] and [day] are 1-based.
76+
*
77+
* The supported ranges of components:
78+
* - [year] the range is at least enough to represent dates of all instants between
79+
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]
80+
* - [month] `1..12`
81+
* - [day] `1..31`, the upper bound can be less, depending on the month
82+
*
83+
* @sample kotlinx.datetime.test.samples.LocalDateSamples.createOrNullMonthNumber
84+
*/
85+
public fun createOrNull(year: Int, month: Int, day: Int): LocalDate?
86+
87+
/**
88+
* Constructs a [LocalDate] instance from the given date components
89+
* or returns `null` if a value is out of range or invalid.
90+
*
91+
* The supported ranges of components:
92+
* - [year] the range at least is enough to represent dates of all instants between
93+
* [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]
94+
* - [month] all values of the [Month] enum
95+
* - [day] `1..31`, the upper bound can be less, depending on the month
96+
*
97+
* @sample kotlinx.datetime.test.samples.LocalDateSamples.createOrNull
98+
*/
99+
public fun createOrNull(year: Int, month: Month, day: Int): LocalDate?
71100
/**
72101
* A shortcut for calling [DateTimeFormat.parse].
73102
*

core/common/test/LocalDateTest.kt

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

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

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

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