refactor: drop all uses of java.time

This commit is contained in:
Harsh Shandilya
2026-06-02 09:26:43 +05:30
parent 8adcef03d6
commit 784bda4105
21 changed files with 112 additions and 151 deletions
+1
View File
@@ -114,6 +114,7 @@ dependencies {
implementation(libs.kotlin.parcelize.runtime)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.stdlib)
@@ -39,9 +39,8 @@ import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import java.io.InputStream
import java.io.OutputStream
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -49,6 +48,9 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@Inject
@ViewModelKey
@@ -173,7 +175,7 @@ class ClawViewModel(
* solution.
*/
private fun String.toLocalDateTime(): LocalDateTime {
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
if (isEmpty()) return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
return Instant.parse(this).toLocalDateTime(TimeZone.currentSystemDefault())
}
}
@@ -12,8 +12,7 @@ import dev.msfjarvis.claw.database.local.SavedPost
import dev.zacsweers.metro.Inject
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlin.time.Instant
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
@@ -49,11 +48,8 @@ class DataTransferRepository(
}
suspend fun exportPostsAsHTML(output: OutputStream) {
fun computeTimestamp(post: SavedPost): Long {
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
val instant = Instant.from(temporal)
return instant.toEpochMilli()
}
fun computeTimestamp(post: SavedPost): Long =
Instant.parse(post.createdAt).toEpochMilliseconds()
val posts = savedPostsRepository.savedPosts.first()
val header =
+1
View File
@@ -28,5 +28,6 @@ dependencies {
testImplementation(libs.eithernet.test.fixtures)
testImplementation(libs.kotlin.reflect)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.kotlinx.datetime)
addTestDependencies(project)
}
@@ -14,8 +14,7 @@ import dev.burnoo.kspoon.Kspoon
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.util.TestUtils.assertIs
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlin.time.Instant
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@@ -54,7 +53,7 @@ class ApiTest {
assertThat(firstPost.tags).containsExactly("linux")
assertThat(firstPost.userIsAuthor).isTrue()
assertThat(firstPost.createdAt).isEqualTo("2026-05-29T09:08:12Z")
DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(firstPost.createdAt)
Instant.parse(firstPost.createdAt)
val secondPost = posts.value.posts[1]
assertThat(secondPost.shortId).isEqualTo("lc26ar")
@@ -143,7 +142,7 @@ class ApiTest {
val editedComment = postDetails.value.comments.first { it.shortId == "pcvbcd" }
assertThat(editedComment.edited).isTrue()
assertThat(Instant.from(editedComment.timestamp).epochSecond).isEqualTo(1658588955)
assertThat(editedComment.timestamp.epochSeconds).isEqualTo(1658588955)
}
@Test
+1
View File
@@ -64,6 +64,7 @@ dependencies {
implementation(libs.kotlinResult)
implementation(libs.kotlinResult.coroutines)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.datetime)
implementation(libs.metrox.viewmodel.compose)
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.extensions.coroutines)
@@ -61,8 +61,7 @@ import dev.msfjarvis.claw.common.ui.NetworkImage
import dev.msfjarvis.claw.common.ui.ThemedRichText
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.UIPost
import java.time.Instant
import java.time.temporal.TemporalAccessor
import kotlin.time.Instant
import kotlinx.collections.immutable.toImmutableList
@Composable
@@ -336,11 +335,11 @@ private fun displayScore(score: Int, initiallyUpvoted: Boolean, isUpvoted: Boole
}
}
private fun buildCommentAgeString(timestamp: TemporalAccessor, edited: Boolean): String {
private fun buildCommentAgeString(timestamp: Instant, edited: Boolean): String {
val now = System.currentTimeMillis()
val relativeTime =
DateUtils.getRelativeTimeSpanString(
Instant.from(timestamp).toEpochMilli(),
timestamp.toEpochMilliseconds(),
now,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
@@ -52,10 +52,12 @@ import dev.msfjarvis.claw.common.R
import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.model.Tag
import dev.zacsweers.metrox.viewmodel.metroViewModel
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Instant
import kotlinx.collections.immutable.ImmutableList
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@OptIn(ExperimentalFlexBoxApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -72,9 +74,9 @@ fun TagList(
selectedTagForDatePicker?.let { tagName ->
if (showDatePicker) {
val tomorrow = Instant.now().plusMillis(24 * 60 * 60 * 1000)
val tomorrow = Clock.System.now().plus(1.days)
val datePickerState =
rememberDatePickerState(initialSelectedDateMillis = tomorrow.toEpochMilli())
rememberDatePickerState(initialSelectedDateMillis = tomorrow.toEpochMilliseconds())
DatePickerDialog(
onDismissRequest = {
@@ -207,7 +209,7 @@ fun TagList(
}
private fun formatDate(millis: Long): String {
val instant = Instant.ofEpochMilli(millis)
val formatter = DateTimeFormatter.ofPattern("MMM dd").withZone(ZoneId.systemDefault())
return formatter.format(instant)
val date = Instant.fromEpochMilliseconds(millis).toLocalDateTime(TimeZone.currentSystemDefault())
val month = date.month.name.lowercase().take(3).replaceFirstChar(Char::titlecase)
return "$month ${date.day.toString().padStart(2, '0')}"
}
@@ -20,7 +20,7 @@ import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
import dev.msfjarvis.claw.model.Comment
import java.time.Instant
import kotlin.time.Clock
@PreviewTest
@DevicePreviews
@@ -72,7 +72,7 @@ private fun previewCommentNode(isUpvoted: Boolean = false, hasChildren: Boolean
comment =
"<p>This is a preview comment with enough content to evaluate spacing, metadata, and future vote affordances.</p>",
score = 42,
timestamp = Instant.now(),
timestamp = Clock.System.now(),
edited = false,
parentComment = null,
user = "Alice",
@@ -92,7 +92,7 @@ private fun previewCommentNode(isUpvoted: Boolean = false, hasChildren: Boolean
comment =
"<p>This child comment helps preview expanded and collapsed thread states.</p>",
score = 7,
timestamp = Instant.now(),
timestamp = Clock.System.now(),
edited = false,
parentComment = parent.comment.shortId,
user = "Bob",
@@ -9,7 +9,7 @@ package dev.msfjarvis.claw.common.comments
import com.google.common.truth.Truth.assertThat
import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.model.Comment
import java.time.Instant
import kotlin.time.Instant
import org.junit.jupiter.api.Test
class CommentsHandlerTest {
@@ -157,7 +157,7 @@ class CommentsHandlerTest {
comment = "Comment $shortId",
url = "https://lobste.rs/s/$shortId",
score = 1,
timestamp = Instant.EPOCH,
timestamp = Instant.fromEpochSeconds(0),
edited = false,
parentComment = parentComment,
user = "user-$shortId",
+1
View File
@@ -24,6 +24,7 @@ dependencies {
api(libs.retrofit)
implementation(platform(libs.okhttp.bom))
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.core)
implementation(libs.retrofit.kotlinxSerializationConverter)
}
@@ -9,12 +9,8 @@ package dev.msfjarvis.claw.core.network
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlinx.datetime.format.DateTimeComponents
import okhttp3.Interceptor
import okhttp3.Response
@@ -68,32 +64,23 @@ class RetryAfterInterceptor : Interceptor {
* Returns the delay in seconds, or 0 if parsing fails.
*/
private fun parseRetryAfter(retryAfter: String): Long {
// Try parsing as seconds first
retryAfter.toLongOrNull()?.let {
return it.coerceAtMost(MAX_RETRY_DELAY_SECONDS)
}
// Try parsing as HTTP-date
try {
val retryDate = ZonedDateTime.parse(retryAfter, HTTP_DATE_FORMATTER)
val now = Instant.now()
val retryInstant = retryDate.toInstant()
val delaySeconds = retryInstant.epochSecond - now.epochSecond
// Only return positive delays, capped at max
return if (delaySeconds > 0) delaySeconds.coerceAtMost(MAX_RETRY_DELAY_SECONDS) else 0
} catch (e: DateTimeParseException) {
// If we can't parse it, don't retry
return 0
}
val retryInstant = parseHttpDate(retryAfter) ?: return 0
val delaySeconds = (retryInstant.toEpochMilliseconds() - System.currentTimeMillis()) / 1000
return if (delaySeconds > 0) delaySeconds.coerceAtMost(MAX_RETRY_DELAY_SECONDS) else 0
}
private fun parseHttpDate(value: String) =
try {
DateTimeComponents.Formats.RFC_1123.parse(value).toInstantUsingOffset()
} catch (_: IllegalArgumentException) {
null
}
private companion object {
// Cap retry delay at 5 minutes to avoid indefinite waits
private const val MAX_RETRY_DELAY_SECONDS = 300L
// HTTP-date format as specified in RFC 7231
private val HTTP_DATE_FORMATTER =
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
}
}
+1
View File
@@ -20,6 +20,7 @@ dependencies {
api(projects.database.core)
implementation(libs.androidx.sqlite)
implementation(libs.kotlinx.datetime)
implementation(libs.sqldelight.androidDriver)
implementation(libs.sqldelight.primitiveAdapters)
implementation(libs.sqlite.android)
@@ -7,9 +7,8 @@
package dev.msfjarvis.claw.database.local
import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -114,13 +113,8 @@ class SavedPostQueriesTest {
@Test
fun `select posts from last N days`() {
val now = Instant.now()
val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
fun dateOffset(daysToSubtract: Long): String {
return formatter.format(
now.minus(daysToSubtract, ChronoUnit.DAYS).atOffset(java.time.ZoneOffset.UTC)
)
}
val now = Clock.System.now()
fun dateOffset(daysToSubtract: Long): String = (now - daysToSubtract.days).toString()
val recentPost = createPostWithDate(id = "recent_1", createdAt = dateOffset(5))
val oldPost = createPostWithDate(id = "old_1", createdAt = dateOffset(40))
@@ -176,13 +170,8 @@ class SavedPostQueriesTest {
@Test
fun `selectRecentPosts returns posts in descending date order`() {
val now = Instant.now()
val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
fun dateOffset(daysToSubtract: Long): String {
return formatter.format(
now.minus(daysToSubtract, ChronoUnit.DAYS).atOffset(java.time.ZoneOffset.UTC)
)
}
val now = Clock.System.now()
fun dateOffset(daysToSubtract: Long): String = (now - daysToSubtract.days).toString()
val oldest = createPostWithDate(id = "oldest", createdAt = dateOffset(30))
val middle = createPostWithDate(id = "middle", createdAt = dateOffset(15))
@@ -201,13 +190,8 @@ class SavedPostQueriesTest {
@Test
fun `selectAllPostsSortedByDate returns posts in descending date order`() {
val now = Instant.now()
val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
fun dateOffset(daysToSubtract: Long): String {
return formatter.format(
now.minus(daysToSubtract, ChronoUnit.DAYS).atOffset(java.time.ZoneOffset.UTC)
)
}
val now = Clock.System.now()
fun dateOffset(daysToSubtract: Long): String = (now - daysToSubtract.days).toString()
val post1 = createPostWithDate(id = "post_1", createdAt = dateOffset(20))
val post2 = createPostWithDate(id = "post_2", createdAt = dateOffset(5))
+1
View File
@@ -133,6 +133,7 @@ kotlinResult-coroutines = { module = "com.michael-bull.kotlin-result:kotlin-resu
kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.7.1"
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
metrox-android = { module = "dev.zacsweers.metro:metrox-android", version.ref = "metro" }
+1
View File
@@ -13,6 +13,7 @@ plugins {
}
dependencies {
api(libs.kotlinx.datetime)
api(libs.kotlinx.serialization.core)
api(libs.kspoon)
api(projects.database.core)
@@ -11,10 +11,7 @@ package dev.msfjarvis.claw.model
import dev.burnoo.kspoon.SelectorHtmlTextMode
import dev.burnoo.kspoon.annotation.Selector
import dev.drewhamilton.poko.Poko
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAccessor
import kotlin.time.Instant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -36,7 +33,7 @@ class Comment(
val score: Int = 1,
@Serializable(with = CommentInstantSerializer::class)
@Selector("div.byline a[href^=/c/] time", attr = "data-at-unix", defValue = "")
val timestamp: TemporalAccessor,
val timestamp: Instant,
@Serializable(with = CommentEditedSerializer::class)
@Selector("div.byline span", defValue = "")
val edited: Boolean = false,
@@ -51,27 +48,13 @@ class Comment(
val isUpvoted: Boolean = false,
)
internal object CommentInstantSerializer : KSerializer<TemporalAccessor> {
internal object CommentInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("CommentInstant", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): TemporalAccessor {
val value = decoder.decodeString()
val isoValue =
if (value.isBlank()) {
Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
} else if (value.all(Char::isDigit)) {
Instant.ofEpochSecond(value.toLong())
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
} else {
value
}
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(isoValue)
}
override fun deserialize(decoder: Decoder): Instant = decoder.decodeString().parseInstantOrEpoch()
override fun serialize(encoder: Encoder, value: TemporalAccessor) =
encoder.encodeString(value.toString())
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
}
internal object CommentScoreSerializer : KSerializer<Int> {
@@ -8,10 +8,6 @@ package dev.msfjarvis.claw.model
import com.fleeksoft.ksoup.nodes.Element
import dev.burnoo.kspoon.decoder.KspoonDecoder
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAccessor
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
@@ -55,7 +51,7 @@ internal object CommentsSerializer : KSerializer<List<Comment>> {
private fun Element.toComment(parentComment: String?): Comment {
val byline = selectFirst("div.byline")
val timestamp = byline?.selectFirst("a[href^=/c/] time")?.attr("data-at-unix").orEmpty()
val parsedTimestamp = timestamp.toTemporalAccessor()
val parsedTimestamp = timestamp.parseInstantOrEpoch()
val isEdited = byline?.text()?.contains("edited") == true
return Comment(
shortId = attr("data-shortid"),
@@ -83,20 +79,6 @@ internal object CommentsSerializer : KSerializer<List<Comment>> {
)
}
private fun String.toTemporalAccessor(): TemporalAccessor {
val isoValue =
if (isBlank()) {
Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
} else if (all(Char::isDigit)) {
Instant.ofEpochSecond(toLong())
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
} else {
this
}
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(isoValue)
}
override fun serialize(encoder: Encoder, value: List<Comment>) =
throw UnsupportedOperationException()
}
@@ -0,0 +1,44 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.model
import kotlin.time.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.toInstant
internal fun String.parseInstantOrEpoch(): Instant =
parseInstantOrNull() ?: Instant.fromEpochSeconds(0)
internal fun String.parseInstantOrNull(): Instant? {
return when {
isBlank() -> Instant.fromEpochSeconds(0)
all(Char::isDigit) -> toLongOrNull()?.let(Instant::fromEpochSeconds)
else -> parseIsoInstant() ?: parseLegacyLobstersDateTime()?.toInstant(TimeZone.UTC)
}
}
private fun String.parseIsoInstant(): Instant? =
try {
Instant.parse(this)
} catch (_: IllegalArgumentException) {
null
}
private fun String.parseLegacyLobstersDateTime(): LocalDateTime? =
try {
LEGACY_LOBSTERS_DATE_TIME_FORMAT.parse(this)
} catch (_: IllegalArgumentException) {
null
}
@OptIn(FormatStringsInDatetimeFormats::class)
private val LEGACY_LOBSTERS_DATE_TIME_FORMAT = LocalDateTime.Format {
byUnicodePattern("yyyy-MM-dd HH:mm:ss")
}
@@ -12,10 +12,6 @@ import dev.burnoo.kspoon.annotation.Selector
import dev.drewhamilton.poko.Poko
import io.mcarle.konvert.api.KonvertTo
import io.mcarle.konvert.api.Mapping
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -70,23 +66,7 @@ internal object CreatedAtSerializer : KSerializer<String> {
override fun deserialize(decoder: Decoder): String {
val value = decoder.decodeString()
return when {
value.isBlank() -> ""
value.all(Char::isDigit) ->
value.toLongOrNull()?.let {
Instant.ofEpochSecond(it)
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
} ?: ""
runCatching { DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(value) }.isSuccess -> value
else ->
runCatching {
LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
}
.getOrDefault("")
}
return if (value.isBlank()) "" else value.parseInstantOrNull()?.toString().orEmpty()
}
override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value)
@@ -6,8 +6,7 @@
*/
package dev.msfjarvis.claw.serialization
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAccessor
import kotlin.time.Instant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -15,14 +14,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
class JavaInstantSerializer : KSerializer<TemporalAccessor> {
class JavaInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): TemporalAccessor {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: TemporalAccessor) =
encoder.encodeString(value.toString())
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
}