Skip to content

Commit 48e41f2

Browse files
committed
Rewrite YearMonth on the JVM to wrap java.time.YearMonth
1 parent 9540fa0 commit 48e41f2

File tree

3 files changed

+50
-22
lines changed

3 files changed

+50
-22
lines changed

core/api/kotlinx-datetime.api

+1
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ public final class kotlinx/datetime/YearMonth : java/io/Serializable, java/lang/
559559
public static final field Companion Lkotlinx/datetime/YearMonth$Companion;
560560
public fun <init> (II)V
561561
public fun <init> (ILkotlinx/datetime/Month;)V
562+
public synthetic fun <init> (Ljava/time/YearMonth;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
562563
public synthetic fun compareTo (Ljava/lang/Object;)I
563564
public fun compareTo (Lkotlinx/datetime/YearMonth;)I
564565
public fun equals (Ljava/lang/Object;)Z

core/common/src/internal/util.kt

+3
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ internal fun removeLeadingZerosFromLongYearFormLocalDate(input: String) =
3838

3939
internal fun removeLeadingZerosFromLongYearFormLocalDateTime(input: String) =
4040
removeLeadingZerosFromLongYearForm(input.toString(), 12) // 12 = "-01-02T23:59".length
41+
42+
internal fun removeLeadingZerosFromLongYearFormYearMonth(input: String) =
43+
removeLeadingZerosFromLongYearForm(input.toString(), 3) // 3 = "-01".length

core/jvm/src/YearMonthJvm.kt

+46-22
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,49 @@ import kotlinx.datetime.format.*
99
import kotlinx.datetime.internal.*
1010
import kotlinx.datetime.serializers.YearMonthIso8601Serializer
1111
import kotlinx.serialization.Serializable
12+
import java.time.DateTimeException
13+
import java.time.format.DateTimeFormatterBuilder
14+
import java.time.format.DateTimeParseException
15+
import java.time.format.SignStyle
16+
import java.time.YearMonth as jtYearMonth
1217

1318
@Serializable(with = YearMonthIso8601Serializer::class)
14-
public actual class YearMonth
15-
public actual constructor(year: Int, month: Int) : Comparable<YearMonth>, java.io.Serializable {
16-
public actual val year: Int = year
17-
internal actual val monthNumber: Int = month
18-
19-
init {
20-
require(month in 1..12) { "Month must be in 1..12, but was $month" }
21-
require(year in LocalDate.MIN.year..LocalDate.MAX.year) {
22-
"Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}"
23-
}
24-
}
19+
public actual class YearMonth private constructor(
20+
private val value: jtYearMonth
21+
) : Comparable<YearMonth>, java.io.Serializable {
22+
public actual val year: Int get() = value.year
23+
internal actual val monthNumber: Int get() = value.monthValue
2524

26-
public actual val month: Month get() = Month(monthNumber)
27-
public actual val firstDay: LocalDate get() = onDay(1)
28-
public actual val lastDay: LocalDate get() = onDay(numberOfDays)
29-
public actual val numberOfDays: Int get() = monthNumber.monthLength(isLeapYear(year))
25+
public actual val month: Month get() = value.month.toKotlinMonth()
26+
public actual val firstDay: LocalDate get() = LocalDate(value.atDay(1))
27+
public actual val lastDay: LocalDate get() = LocalDate(value.atEndOfMonth())
28+
public actual val numberOfDays: Int get() = value.lengthOfMonth()
3029

3130
// val days: LocalDateRange get() = firstDay..lastDay // no ranges yet
3231

33-
public actual constructor(year: Int, month: Month): this(year, month.number)
32+
public actual constructor(year: Int, month: Int): this(try {
33+
jtYearMonth.of(year, month)
34+
} catch (e: DateTimeException) {
35+
throw IllegalArgumentException(e)
36+
})
37+
public actual constructor(year: Int, month: Month): this(try {
38+
jtYearMonth.of(year, month.toJavaMonth())
39+
} catch (e: DateTimeException) {
40+
throw IllegalArgumentException(e)
41+
})
3442

3543
public actual companion object {
3644
public actual fun parse(input: CharSequence, format: DateTimeFormat<YearMonth>): YearMonth =
37-
format.parse(input)
45+
if (format === Formats.ISO) {
46+
try {
47+
val sanitizedInput = removeLeadingZerosFromLongYearFormYearMonth(input.toString())
48+
jtYearMonth.parse(sanitizedInput).let(::YearMonth)
49+
} catch (e: DateTimeParseException) {
50+
throw DateTimeFormatException(e)
51+
}
52+
} else {
53+
format.parse(input)
54+
}
3855

3956
@Suppress("FunctionName")
4057
public actual fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat<YearMonth> =
@@ -45,14 +62,13 @@ public actual constructor(year: Int, month: Int) : Comparable<YearMonth>, java.i
4562
public actual val ISO: DateTimeFormat<YearMonth> get() = ISO_YEAR_MONTH
4663
}
4764

48-
actual override fun compareTo(other: YearMonth): Int =
49-
compareValuesBy(this, other, YearMonth::year, YearMonth::month)
65+
actual override fun compareTo(other: YearMonth): Int = value.compareTo(other.value)
5066

51-
actual override fun toString(): String = Formats.ISO.format(this)
67+
actual override fun toString(): String = isoFormat.format(value)
5268

53-
override fun equals(other: Any?): Boolean = other is YearMonth && year == other.year && month == other.month
69+
override fun equals(other: Any?): Boolean = this === other || other is YearMonth && value == other.value
5470

55-
override fun hashCode(): Int = year * 31 + month.hashCode()
71+
override fun hashCode(): Int = value.hashCode()
5672

5773
private fun writeReplace(): Any = Ser(Ser.YEAR_MONTH_TAG, this)
5874
}
@@ -64,3 +80,11 @@ internal fun YearMonth.Companion.fromEpochMonths(months: Long): YearMonth {
6480
val month = months.mod(12) + 1
6581
return YearMonth(year.toInt(), month)
6682
}
83+
84+
private val isoFormat by lazy {
85+
DateTimeFormatterBuilder().parseCaseInsensitive()
86+
.appendValue(java.time.temporal.ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
87+
.appendLiteral('-')
88+
.appendValue(java.time.temporal.ChronoField.MONTH_OF_YEAR, 2)
89+
.toFormatter()
90+
}

0 commit comments

Comments
 (0)