Skip to content

Commit 9540fa0

Browse files
committed
Implement java.io.Serializable for YearMonth
1 parent 10a8a46 commit 9540fa0

File tree

10 files changed

+165
-44
lines changed

10 files changed

+165
-44
lines changed

core/api/kotlinx-datetime.api

+2-1
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ public final class kotlinx/datetime/Ser : java/io/Externalizable {
482482
public static final field DATE_TIME_TAG I
483483
public static final field TIME_TAG I
484484
public static final field UTC_OFFSET_TAG I
485+
public static final field YEAR_MONTH_TAG I
485486
public fun <init> ()V
486487
public fun <init> (ILjava/lang/Object;)V
487488
public fun readExternal (Ljava/io/ObjectInput;)V
@@ -554,7 +555,7 @@ public final class kotlinx/datetime/UtcOffsetKt {
554555
public static final fun format (Lkotlinx/datetime/UtcOffset;Lkotlinx/datetime/format/DateTimeFormat;)Ljava/lang/String;
555556
}
556557

557-
public final class kotlinx/datetime/YearMonth : java/lang/Comparable {
558+
public final class kotlinx/datetime/YearMonth : java/io/Serializable, java/lang/Comparable {
558559
public static final field Companion Lkotlinx/datetime/YearMonth$Companion;
559560
public fun <init> (II)V
560561
public fun <init> (ILkotlinx/datetime/Month;)V

core/common/src/YearMonth.kt

+14-40
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import kotlinx.serialization.Serializable
5858
* @sample kotlinx.datetime.test.samples.YearMonthSamples.customFormat
5959
*/
6060
@Serializable(with = YearMonthIso8601Serializer::class)
61-
public class YearMonth
61+
public expect class YearMonth
6262
/**
6363
* Constructs a [YearMonth] instance from the given year-month components.
6464
*
@@ -78,49 +78,37 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
7878
*
7979
* @sample kotlinx.datetime.test.samples.YearMonthSamples.year
8080
*/
81-
public val year: Int = year
82-
83-
/**
84-
* Returns the number-of-the-month (1..12) component of the year-month.
85-
*
86-
* Shortcut for `month.number`.
87-
*/
88-
internal val monthNumber: Int = month
89-
90-
init {
91-
require(month in 1..12) { "Month must be in 1..12, but was $month" }
92-
require(year in LocalDate.MIN.year..LocalDate.MAX.year) {
93-
"Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}"
94-
}
95-
}
81+
public val year: Int
9682

9783
/**
9884
* Returns the month ([Month]) component of the year-month.
9985
*
10086
* @sample kotlinx.datetime.test.samples.YearMonthSamples.month
10187
*/
102-
public val month: Month get() = Month(monthNumber)
88+
public val month: Month
10389

10490
/**
10591
* Returns the first day of the year-month.
10692
*
10793
* @sample kotlinx.datetime.test.samples.YearMonthSamples.firstAndLastDay
10894
*/
109-
public val firstDay: LocalDate get() = onDay(1)
95+
public val firstDay: LocalDate
11096

11197
/**
11298
* Returns the last day of the year-month.
11399
*
114100
* @sample kotlinx.datetime.test.samples.YearMonthSamples.firstAndLastDay
115101
*/
116-
public val lastDay: LocalDate get() = onDay(numberOfDays)
102+
public val lastDay: LocalDate
117103

118104
/**
119105
* Returns the number of days in the year-month.
120106
*
121107
* @sample kotlinx.datetime.test.samples.YearMonthSamples.numberOfDays
122108
*/
123-
public val numberOfDays: Int get() = monthNumber.monthLength(isLeapYear(year))
109+
public val numberOfDays: Int
110+
111+
internal val monthNumber: Int
124112

125113
/**
126114
* Returns the range of days in the year-month.
@@ -138,7 +126,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
138126
* @throws IllegalArgumentException if [year] is out of range.
139127
* @sample kotlinx.datetime.test.samples.YearMonthSamples.constructorFunction
140128
*/
141-
public constructor(year: Int, month: Month): this(year, month.number)
129+
public constructor(year: Int, month: Month)
142130

143131
public companion object {
144132
/**
@@ -155,8 +143,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
155143
* @see YearMonth.format for formatting using a custom format.
156144
* @sample kotlinx.datetime.test.samples.YearMonthSamples.parsing
157145
*/
158-
public fun parse(input: CharSequence, format: DateTimeFormat<YearMonth> = Formats.ISO): YearMonth =
159-
format.parse(input)
146+
public fun parse(input: CharSequence, format: DateTimeFormat<YearMonth> = Formats.ISO): YearMonth
160147

161148
/**
162149
* Creates a new format for parsing and formatting [YearMonth] values.
@@ -167,8 +154,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
167154
* @sample kotlinx.datetime.test.samples.YearMonthSamples.customFormat
168155
*/
169156
@Suppress("FunctionName")
170-
public fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat<YearMonth> =
171-
YearMonthFormat.build(block)
157+
public fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat<YearMonth>
172158
}
173159

174160
/**
@@ -194,9 +180,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
194180
*
195181
* @sample kotlinx.datetime.test.samples.YearMonthSamples.Formats.iso
196182
*/
197-
public val ISO: DateTimeFormat<YearMonth> = YearMonthFormat.build {
198-
year(); char('-'); monthNumber()
199-
}
183+
public val ISO: DateTimeFormat<YearMonth>
200184
}
201185

202186
/**
@@ -207,7 +191,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
207191
*
208192
* @sample kotlinx.datetime.test.samples.YearMonthSamples.compareToSample
209193
*/
210-
override fun compareTo(other: YearMonth): Int = compareValuesBy(this, other, YearMonth::year, YearMonth::month)
194+
override fun compareTo(other: YearMonth): Int
211195

212196
/**
213197
* Converts this year-month to the extended ISO 8601 string representation.
@@ -217,17 +201,7 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
217201
* @see YearMonth.format for formatting using a custom format.
218202
* @sample kotlinx.datetime.test.samples.YearMonthSamples.toStringSample
219203
*/
220-
override fun toString(): String = Formats.ISO.format(this)
221-
222-
/**
223-
* @suppress
224-
*/
225-
override fun equals(other: Any?): Boolean = other is YearMonth && year == other.year && month == other.month
226-
227-
/**
228-
* @suppress
229-
*/
230-
override fun hashCode(): Int = year * 31 + month.hashCode()
204+
override fun toString(): String
231205
}
232206

233207
/**

core/common/src/format/YearMonthFormat.kt

+7
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,10 @@ internal interface AbstractWithYearMonthBuilder : DateTimeFormatBuilder.WithYear
291291
}
292292

293293
private val emptyIncompleteYearMonth = IncompleteYearMonth()
294+
295+
// these are constants so that the formats are not recreated every time they are used
296+
internal val ISO_YEAR_MONTH by lazy {
297+
YearMonthFormat.build {
298+
year(); char('-'); monthNumber()
299+
}
300+
}

core/common/test/YearMonthTest.kt

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class YearMonthTest {
1818

1919
private fun checkComponents(value: YearMonth, year: Int, month: Int) {
2020
assertEquals(year, value.year)
21-
assertEquals(month, value.monthNumber)
2221
assertEquals(Month(month), value.month)
2322

2423
val fromComponents = YearMonth(year, month)

core/common/test/samples/YearMonthSamples.kt

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class YearMonthSamples {
4646
// Constructing a YearMonth value using its constructor
4747
val yearMonth = YearMonth(2024, 4)
4848
check(yearMonth.year == 2024)
49-
check(yearMonth.monthNumber == 4)
5049
check(yearMonth.month == Month.APRIL)
5150
}
5251

core/commonKotlin/src/YearMonth.kt

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
import kotlinx.datetime.format.*
9+
import kotlinx.datetime.internal.*
10+
import kotlinx.datetime.serializers.YearMonthIso8601Serializer
11+
import kotlinx.serialization.Serializable
12+
13+
@Serializable(with = YearMonthIso8601Serializer::class)
14+
public actual class YearMonth
15+
public actual constructor(public actual val year: Int, month: Int) : Comparable<YearMonth> {
16+
internal actual val monthNumber: Int = month
17+
18+
init {
19+
require(month in 1..12) { "Month must be in 1..12, but was $month" }
20+
require(year in LocalDate.MIN.year..LocalDate.MAX.year) {
21+
"Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}"
22+
}
23+
}
24+
25+
public actual val month: Month get() = Month(monthNumber)
26+
27+
public actual val firstDay: LocalDate get() = onDay(1)
28+
29+
public actual val lastDay: LocalDate get() = onDay(numberOfDays)
30+
31+
public actual val numberOfDays: Int get() = monthNumber.monthLength(isLeapYear(year))
32+
33+
// val days: LocalDateRange get() = firstDay..lastDay // no ranges yet
34+
35+
public actual constructor(year: Int, month: Month): this(year, month.number)
36+
37+
public actual companion object {
38+
public actual fun parse(input: CharSequence, format: DateTimeFormat<YearMonth>): YearMonth =
39+
format.parse(input)
40+
41+
@Suppress("FunctionName")
42+
public actual fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat<YearMonth> =
43+
YearMonthFormat.build(block)
44+
}
45+
46+
public actual object Formats {
47+
public actual val ISO: DateTimeFormat<YearMonth> get() = ISO_YEAR_MONTH
48+
}
49+
50+
actual override fun compareTo(other: YearMonth): Int =
51+
compareValuesBy(this, other, YearMonth::year, YearMonth::month)
52+
53+
actual override fun toString(): String = Formats.ISO.format(this)
54+
55+
override fun equals(other: Any?): Boolean = other is YearMonth && year == other.year && month == other.month
56+
57+
override fun hashCode(): Int = year * 31 + month.hashCode()
58+
}

core/darwin/src/Converters.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,6 @@ public fun LocalDateTime.toNSDateComponents(): NSDateComponents {
101101
public fun YearMonth.toNSDateComponents(): NSDateComponents {
102102
val components = NSDateComponents()
103103
components.year = year.convert()
104-
components.month = monthNumber.convert()
104+
components.month = month.number.convert()
105105
return components
106106
}

core/jvm/src/YearMonthJvm.kt

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
import kotlinx.datetime.format.*
9+
import kotlinx.datetime.internal.*
10+
import kotlinx.datetime.serializers.YearMonthIso8601Serializer
11+
import kotlinx.serialization.Serializable
12+
13+
@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+
}
25+
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))
30+
31+
// val days: LocalDateRange get() = firstDay..lastDay // no ranges yet
32+
33+
public actual constructor(year: Int, month: Month): this(year, month.number)
34+
35+
public actual companion object {
36+
public actual fun parse(input: CharSequence, format: DateTimeFormat<YearMonth>): YearMonth =
37+
format.parse(input)
38+
39+
@Suppress("FunctionName")
40+
public actual fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat<YearMonth> =
41+
YearMonthFormat.build(block)
42+
}
43+
44+
public actual object Formats {
45+
public actual val ISO: DateTimeFormat<YearMonth> get() = ISO_YEAR_MONTH
46+
}
47+
48+
actual override fun compareTo(other: YearMonth): Int =
49+
compareValuesBy(this, other, YearMonth::year, YearMonth::month)
50+
51+
actual override fun toString(): String = Formats.ISO.format(this)
52+
53+
override fun equals(other: Any?): Boolean = other is YearMonth && year == other.year && month == other.month
54+
55+
override fun hashCode(): Int = year * 31 + month.hashCode()
56+
57+
private fun writeReplace(): Any = Ser(Ser.YEAR_MONTH_TAG, this)
58+
}
59+
60+
internal fun YearMonth.toEpochMonths(): Long = (year - 1970L) * 12 + monthNumber - 1
61+
62+
internal fun YearMonth.Companion.fromEpochMonths(months: Long): YearMonth {
63+
val year = months.floorDiv(12) + 1970
64+
val month = months.mod(12) + 1
65+
return YearMonth(year.toInt(), month)
66+
}

core/jvm/src/internal/Ser.kt

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External
3333
value as UtcOffset
3434
out.writeInt(value.totalSeconds)
3535
}
36+
YEAR_MONTH_TAG -> {
37+
value as YearMonth
38+
out.writeLong(value.toEpochMonths())
39+
}
3640
else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value")
3741
}
3842
}
@@ -51,6 +55,8 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External
5155
)
5256
UTC_OFFSET_TAG ->
5357
UtcOffset(seconds = `in`.readInt())
58+
YEAR_MONTH_TAG ->
59+
YearMonth.fromEpochMonths(`in`.readLong())
5460
else -> throw IOException("Unknown type tag: $typeTag")
5561
}
5662
}
@@ -63,5 +69,6 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External
6369
const val TIME_TAG = 3
6470
const val DATE_TIME_TAG = 4
6571
const val UTC_OFFSET_TAG = 10
72+
const val YEAR_MONTH_TAG = 11
6673
}
6774
}

core/jvm/test/JvmSerializationTest.kt

+10
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ class JvmSerializationTest {
4242
expectedDeserialization(UtcOffset.parse("-04:15:30"), "050affffc41e")
4343
}
4444

45+
@Test
46+
fun serializeYearMonth() {
47+
roundTripSerialization(YearMonth(2022, 1))
48+
roundTripSerialization(YearMonth(1969, 7))
49+
roundTripSerialization(YearMonth(-999999999, 1))
50+
roundTripSerialization(YearMonth(999999999, 12))
51+
expectedDeserialization(YearMonth(2024, 8), "090b000000000000028f")
52+
expectedDeserialization(YearMonth(1970, 1), "090b0000000000000000")
53+
}
54+
4555
@Test
4656
fun serializeTimeZone() {
4757
assertFailsWith<NotSerializableException> {

0 commit comments

Comments
 (0)