From 07fd02b25f1fc2e818aeaad01c6f03400b810e78 Mon Sep 17 00:00:00 2001 From: Pavel Kunyavskiy Date: Fri, 1 Mar 2024 22:42:11 +0100 Subject: [PATCH] Use formatters by kotlinx.datetime instead of ones from java time --- gradle/libs.versions.toml | 2 +- .../cds/plugins/cats/CATSDataSource.kt | 52 +++---- .../cds/plugins/ejudge/EjudgeDataSource.kt | 20 ++- .../cds/plugins/eolymp/EOlympDataSource.kt | 30 +--- .../icpclive/cds/plugins/nsu/NSUDataSource.kt | 13 +- .../cds/plugins/testsys/TestSysDataSource.kt | 19 ++- src/cds/utils/api/utils.api | 18 +++ .../serializers/FormatterInstantSerializer.kt | 19 +++ .../FormatterLocalDateSerializer.kt | 18 +++ .../kotlin/org/icpclive/clics/ClicsTime.kt | 124 ---------------- .../org/icpclive/clics/time/ClicsTime.kt | 73 ++++++++++ .../icpclive/clics/time/DurationSerializer.kt | 21 +++ .../icpclive/clics/time/InstantSerializer.kt | 21 +++ .../ClicsTimeTest.kt} | 134 ++++++++++-------- .../ksp/clics/FeedVersionsProcessor.kt | 4 +- 15 files changed, 306 insertions(+), 262 deletions(-) create mode 100644 src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterInstantSerializer.kt create mode 100644 src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterLocalDateSerializer.kt delete mode 100644 src/clics-api/src/main/kotlin/org/icpclive/clics/ClicsTime.kt create mode 100644 src/clics-api/src/main/kotlin/org/icpclive/clics/time/ClicsTime.kt create mode 100644 src/clics-api/src/main/kotlin/org/icpclive/clics/time/DurationSerializer.kt create mode 100644 src/clics-api/src/main/kotlin/org/icpclive/clics/time/InstantSerializer.kt rename src/clics-api/src/test/kotlin/org/icpclive/clics/{ClicksTimeTest.kt => time/ClicsTimeTest.kt} (52%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d40465033..5d1cbd9ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] ktor = "2.3.10" # https://ktor.io/ -datetime = "0.5.0" # https://github.com/Kotlin/kotlinx-datetime +datetime = "0.6.0" # https://github.com/Kotlin/kotlinx-datetime serialization = "1.6.3" # https://github.com/Kotlin/kotlinx.serialization kotlin = "2.0.0-RC1" ksp = "2.0.0-RC1-1.0.20" # https://github.com/google/ksp diff --git a/src/cds/plugins/cats/src/main/kotlin/org/icpclive/cds/plugins/cats/CATSDataSource.kt b/src/cds/plugins/cats/src/main/kotlin/org/icpclive/cds/plugins/cats/CATSDataSource.kt index 592fb1901..3ceb29b78 100644 --- a/src/cds/plugins/cats/src/main/kotlin/org/icpclive/cds/plugins/cats/CATSDataSource.kt +++ b/src/cds/plugins/cats/src/main/kotlin/org/icpclive/cds/plugins/cats/CATSDataSource.kt @@ -1,44 +1,36 @@ package org.icpclive.cds.plugins.cats import kotlinx.datetime.* +import kotlinx.datetime.format.* import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder import org.icpclive.cds.* import org.icpclive.cds.api.* import org.icpclive.ksp.cds.Builder import org.icpclive.cds.ktor.* import org.icpclive.cds.settings.* -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter +import org.icpclive.cds.util.serializers.* import kotlin.time.Duration.Companion.seconds -private object ContestTimeSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantConstand", PrimitiveKind.STRING) - private val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") - - override fun serialize(encoder: Encoder, value: LocalDateTime) { - encoder.encodeString(formatter.format(value.toJavaLocalDateTime())) - } - - override fun deserialize(decoder: Decoder): LocalDateTime { - return java.time.LocalDateTime.parse(decoder.decodeString(), formatter).toKotlinLocalDateTime() - } -} - -private object SubmissionTimeSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantConstand", PrimitiveKind.STRING) - private val formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssZ") - - override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(formatter.format(value.toJavaInstant())) - } - - override fun deserialize(decoder: Decoder): Instant { - return ZonedDateTime.parse(decoder.decodeString(), formatter).toInstant().toKotlinInstant() - } -} +private object ContestTimeSerializer : FormatterLocalDateSerializer(LocalDateTime.Format { + dayOfMonth() + char('.') + monthNumber() + char('.') + year() + char(' ') + hour() + char(':') + minute() +}) + +private object SubmissionTimeSerializer : FormatterInstantSerializer(DateTimeComponents.Format { + date(LocalDate.Formats.ISO_BASIC) + char('T') + hour() + minute() + second() + offset(UtcOffset.Formats.ISO_BASIC) +}) @Builder("cats") public sealed interface CatsSettings : CDSSettings { diff --git a/src/cds/plugins/ejudge/src/main/kotlin/org/icpclive/cds/plugins/ejudge/EjudgeDataSource.kt b/src/cds/plugins/ejudge/src/main/kotlin/org/icpclive/cds/plugins/ejudge/EjudgeDataSource.kt index c49568ff8..11dec9223 100644 --- a/src/cds/plugins/ejudge/src/main/kotlin/org/icpclive/cds/plugins/ejudge/EjudgeDataSource.kt +++ b/src/cds/plugins/ejudge/src/main/kotlin/org/icpclive/cds/plugins/ejudge/EjudgeDataSource.kt @@ -1,6 +1,8 @@ package org.icpclive.cds.plugins.ejudge import kotlinx.datetime.* +import kotlinx.datetime.format.alternativeParsing +import kotlinx.datetime.format.char import org.icpclive.cds.* import org.icpclive.cds.api.* import org.icpclive.ksp.cds.Builder @@ -10,7 +12,6 @@ import org.icpclive.cds.settings.UrlOrLocalPath import org.icpclive.cds.util.child import org.icpclive.cds.util.children import org.w3c.dom.Element -import java.time.format.DateTimeFormatter import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.nanoseconds @@ -71,14 +72,21 @@ internal class EjudgeDataSource(val settings: EjudgeSettings) : FullReloadContes ) }.toList() - private val timePattern: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + private val timePattern = LocalDateTime.Format { + year() + alternativeParsing({ char('/') }) { char('-') } + monthNumber() + alternativeParsing({ char('/') }) { char('-') } + dayOfMonth() + char(' ') + time(LocalTime.Formats.ISO) + } private fun parseEjudgeTime(time: String): Instant { - return java.time.LocalDateTime.parse( - time.replace("/", "-"), // snark's ejudge uses '/' instead of '-' + return LocalDateTime.parse( + time, timePattern - ).toKotlinLocalDateTime() - .toInstant(settings.timeZone) + ).toInstant(settings.timeZone) } private fun parseContestInfo(element: Element): ContestParseResult { diff --git a/src/cds/plugins/eolymp/src/main/kotlin/org/icpclive/cds/plugins/eolymp/EOlympDataSource.kt b/src/cds/plugins/eolymp/src/main/kotlin/org/icpclive/cds/plugins/eolymp/EOlympDataSource.kt index 18e7f192f..69c1a6c36 100644 --- a/src/cds/plugins/eolymp/src/main/kotlin/org/icpclive/cds/plugins/eolymp/EOlympDataSource.kt +++ b/src/cds/plugins/eolymp/src/main/kotlin/org/icpclive/cds/plugins/eolymp/EOlympDataSource.kt @@ -3,7 +3,8 @@ package org.icpclive.cds.plugins.eolymp import com.eolymp.graphql.* import com.expediagroup.graphql.client.ktor.GraphQLKtorClient import com.expediagroup.graphql.client.types.GraphQLClientRequest -import kotlinx.datetime.toKotlinInstant +import kotlinx.datetime.Instant +import kotlinx.datetime.format.DateTimeComponents import org.icpclive.cds.* import org.icpclive.cds.api.* import org.icpclive.ksp.cds.Builder @@ -12,10 +13,6 @@ import org.icpclive.cds.settings.CDSSettings import org.icpclive.cds.settings.Credential import org.icpclive.cds.util.getLogger import java.net.URL -import java.time.chrono.IsoChronology -import java.time.format.DateTimeFormatterBuilder -import java.time.format.ResolverStyle -import java.time.temporal.ChronoField import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.seconds @@ -91,27 +88,6 @@ internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContes else -> error("Unknown contest format: $format") } - private val dateTimeFormatter = DateTimeFormatterBuilder() - .parseCaseInsensitive() - .appendValue(ChronoField.YEAR, 4) - .appendLiteral('-') - .appendValue(ChronoField.MONTH_OF_YEAR, 2) - .appendLiteral('-') - .appendValue(ChronoField.DAY_OF_MONTH, 2) - .appendLiteral('T') - .appendValue(ChronoField.HOUR_OF_DAY, 2) - .appendLiteral(':') - .appendValue(ChronoField.MINUTE_OF_HOUR, 2) - .appendLiteral(':') - .appendValue(ChronoField.SECOND_OF_MINUTE, 2) - .optionalStart() - .appendFraction(ChronoField.NANO_OF_SECOND, 2, 9, true) - .optionalEnd() - .appendOffset("+HH:MM", "Z") - .toFormatter() - .withResolverStyle(ResolverStyle.STRICT) - .withChronology(IsoChronology.INSTANCE) - private var previousDays: List = emptyList() @OptIn(InefficientContestInfoApi::class) @@ -271,7 +247,7 @@ internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContes } } - private fun parseTime(s: String) = java.time.Instant.from(dateTimeFormatter.parse(s)).toKotlinInstant() + private fun parseTime(s: String) = Instant.parse(s, DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) companion object { val log by getLogger() diff --git a/src/cds/plugins/nsu/src/main/kotlin/org/icpclive/cds/plugins/nsu/NSUDataSource.kt b/src/cds/plugins/nsu/src/main/kotlin/org/icpclive/cds/plugins/nsu/NSUDataSource.kt index b96b5ba95..e28ebd113 100644 --- a/src/cds/plugins/nsu/src/main/kotlin/org/icpclive/cds/plugins/nsu/NSUDataSource.kt +++ b/src/cds/plugins/nsu/src/main/kotlin/org/icpclive/cds/plugins/nsu/NSUDataSource.kt @@ -9,6 +9,7 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.datetime.* +import kotlinx.datetime.format.char import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import org.icpclive.cds.* @@ -17,7 +18,6 @@ import org.icpclive.ksp.cds.Builder import org.icpclive.cds.settings.CDSSettings import org.icpclive.cds.settings.Credential import org.icpclive.cds.ktor.* -import java.time.format.DateTimeFormatter import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -198,13 +198,14 @@ internal class NSUDataSource(val settings: NSUSettings) : FullReloadContestDataS return verdict?.toICPCRunResult() } - private val timePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + private val timePattern = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + time(LocalTime.Formats.ISO) + } private fun parseNSUTime(time: String): Instant { - return java.time.LocalDateTime.parse( - time, timePattern - ).toKotlinLocalDateTime() - .toInstant(settings.timeZone) + return timePattern.parse(time).toInstant(settings.timeZone) } @Serializable diff --git a/src/cds/plugins/testsys/src/main/kotlin/org/icpclive/cds/plugins/testsys/TestSysDataSource.kt b/src/cds/plugins/testsys/src/main/kotlin/org/icpclive/cds/plugins/testsys/TestSysDataSource.kt index c0dc985e5..246c9cb3d 100644 --- a/src/cds/plugins/testsys/src/main/kotlin/org/icpclive/cds/plugins/testsys/TestSysDataSource.kt +++ b/src/cds/plugins/testsys/src/main/kotlin/org/icpclive/cds/plugins/testsys/TestSysDataSource.kt @@ -1,6 +1,7 @@ package org.icpclive.cds.plugins.testsys import kotlinx.datetime.* +import kotlinx.datetime.format.* import org.icpclive.cds.* import org.icpclive.cds.api.* import org.icpclive.ksp.cds.Builder @@ -8,7 +9,6 @@ import org.icpclive.cds.ktor.* import org.icpclive.cds.settings.CDSSettings import org.icpclive.cds.settings.UrlOrLocalPath import java.nio.charset.Charset -import java.time.format.DateTimeFormatter import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -122,10 +122,19 @@ internal class TestSysDataSource(val settings: TestSysSettings) : FullReloadCont add(builder.toString()) } - private fun String.toDate() = - java.time.LocalDateTime.parse(this, DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")) - .toKotlinLocalDateTime() - .toInstant(settings.timeZone) + private val dateFormat = LocalDateTime.Format { + dayOfMonth() + char('.') + monthNumber() + char('.') + year() + char(' ') + time(LocalTime.Formats.ISO) + } + + private fun String.toDate(): Instant { + return dateFormat.parse(this).toInstant(settings.timeZone) + } private fun String.toStatus() = when (this) { "RESULTS" -> ContestStatus.OVER diff --git a/src/cds/utils/api/utils.api b/src/cds/utils/api/utils.api index f5baa84e9..43d5423c2 100644 --- a/src/cds/utils/api/utils.api +++ b/src/cds/utils/api/utils.api @@ -72,6 +72,24 @@ public final class org/icpclive/cds/util/serializers/DurationInSecondsSerializer public fun serialize-HG0u8IE (Lkotlinx/serialization/encoding/Encoder;J)V } +public abstract class org/icpclive/cds/util/serializers/FormatterInstantSerializer : kotlinx/serialization/KSerializer { + public fun (Lkotlinx/datetime/format/DateTimeFormat;)V + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/datetime/Instant; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/datetime/Instant;)V +} + +public abstract class org/icpclive/cds/util/serializers/FormatterLocalDateSerializer : kotlinx/serialization/KSerializer { + public fun (Lkotlinx/datetime/format/DateTimeFormat;)V + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/datetime/LocalDateTime; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/datetime/LocalDateTime;)V +} + public final class org/icpclive/cds/util/serializers/HumanTimeSerializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lorg/icpclive/cds/util/serializers/HumanTimeSerializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; diff --git a/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterInstantSerializer.kt b/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterInstantSerializer.kt new file mode 100644 index 000000000..3897aef43 --- /dev/null +++ b/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterInstantSerializer.kt @@ -0,0 +1,19 @@ +package org.icpclive.cds.util.serializers + +import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +public abstract class FormatterInstantSerializer(private val formatter: DateTimeFormat) : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.format(formatter, TimeZone.currentSystemDefault().offsetAt(value))) + } + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString(), formatter) + } +} \ No newline at end of file diff --git a/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterLocalDateSerializer.kt b/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterLocalDateSerializer.kt new file mode 100644 index 000000000..b8449060d --- /dev/null +++ b/src/cds/utils/src/main/kotlin/org/icpclive/cds/util/serializers/FormatterLocalDateSerializer.kt @@ -0,0 +1,18 @@ +package org.icpclive.cds.util.serializers + +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +public abstract class FormatterLocalDateSerializer(private val formatter: DateTimeFormat) : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeString(formatter.format(value)) + } + override fun deserialize(decoder: Decoder): LocalDateTime { + return formatter.parse(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/src/clics-api/src/main/kotlin/org/icpclive/clics/ClicsTime.kt b/src/clics-api/src/main/kotlin/org/icpclive/clics/ClicsTime.kt deleted file mode 100644 index a72a46428..000000000 --- a/src/clics-api/src/main/kotlin/org/icpclive/clics/ClicsTime.kt +++ /dev/null @@ -1,124 +0,0 @@ -package org.icpclive.clics - -import kotlinx.datetime.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.regex.* -import kotlin.math.round -import kotlin.time.Duration -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds - -internal object ClicsTime { - // https://ccs-specs.icpc.io/2021-11/contest_api#json-attribute-types - private const val DATE_STR = "([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})" - private const val TIME_STR = "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}([.][0-9]{1,})?)" - private const val OPT_ZONE_STR = "(([-+])([0-9]{1,2}):?([0-9]{2})?)?[zZ]?" - private val TIME_PATTERN = Pattern.compile("^($DATE_STR)T($TIME_STR)($OPT_ZONE_STR)$") - fun parseTime(csTime: CharSequence): Instant { - val matcher = TIME_PATTERN.matcher(csTime) - return if (matcher.matches()) { - val yearStr = matcher.group(2) - val monthStr = matcher.group(3) - val dayStr = matcher.group(4) - var year = yearStr.toInt() - if (yearStr.length <= 2) { - // https://www.ibm.com/docs/en/i/7.2?topic=mcdtdi-conversion-2-digit-years-4-digit-years-centuries - year = if (year >= 40) 1900 + year else 2000 + year - } - val month = monthStr.toInt() - val day = dayStr.toInt() - val isoDate = String.format("%04d-%02d-%02d", year, month, day) - val hourStr = matcher.group(6) - val minuteStr = matcher.group(7) - val secondStr = matcher.group(8) - val hour = hourStr.toInt() - val minute = minuteStr.toInt() - val second = secondStr.toDouble() - val iSecond = second.toInt() - val nanoSecond = round(1e9 * (second - iSecond)).toInt() - val isoTime = String.format("%02d:%02d:%02d.%09d", hour, minute, iSecond, nanoSecond) - val offsetSignStr = matcher.group(12) - val offsetHourStr = matcher.group(13) - val offsetMinuteStr = matcher.group(14) - val offsetSign = if (offsetSignStr != null && offsetSignStr == "-") -1 else 1 - val offsetHour = offsetHourStr?.toInt() ?: 0 - val offsetMinute = offsetMinuteStr?.toInt() ?: 0 - val isoOffset = String.format("%c%02d:%02d", if (offsetSign == 1) '+' else '-', offsetHour, offsetMinute) - val isoDateTime = isoDate + "T" + isoTime + isoOffset - val zdt = ZonedDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME) - zdt.toInstant().toKotlinInstant() - } else { - throw IllegalArgumentException() - } - } - - private val RELATIVE_TIME_PATTERN = Pattern.compile("^([-+])?(([0-9]+):)?(([0-9]+):)?([0-9]+([.][0-9]+)?)$") - fun parseRelativeTime(csTime: CharSequence): Duration { - val matcher = RELATIVE_TIME_PATTERN.matcher(csTime) - return if (matcher.matches()) { - val signStr = matcher.group(1) - var hourStr = matcher.group(3) - var minuteStr = matcher.group(5) - if (minuteStr == null && hourStr != null) { - minuteStr = hourStr - hourStr = null - } - val secondStr = matcher.group(6) - val sign = if (signStr != null && signStr == "-") -1 else 1 - val hour = hourStr?.toInt() ?: 0 - val minute = minuteStr?.toInt() ?: 0 - val second = secondStr.toDouble() - (hour.hours + minute.minutes + second.seconds) * sign - } else { - throw IllegalArgumentException("Invalid time format $csTime") - } - } - - fun formatIso(duration: Duration) = - (if (duration.isPositive()) duration else -duration).toComponents { hours, minutes, seconds, nanoseconds -> - "%s%02d:%02d:%02d.%03d".format( - if (duration.isPositive()) "" else "-", - hours, - minutes, - seconds, - nanoseconds / 1000000 - ) - } - - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX") - fun formatIso(instant: Instant) = Instant.fromEpochMilliseconds(instant.toEpochMilliseconds()).toJavaInstant().atZone(ZoneId.systemDefault()).format( - formatter - ) - - object DurationSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ClicsDuration", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Duration) { - encoder.encodeString(formatIso(value)) - } - - override fun deserialize(decoder: Decoder): Duration { - return parseRelativeTime(decoder.decodeString()) - } - } - - object InstantSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ClicsInstant", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(formatIso(value)) - } - - override fun deserialize(decoder: Decoder): Instant { - return parseTime(decoder.decodeString()) - } - } - -} diff --git a/src/clics-api/src/main/kotlin/org/icpclive/clics/time/ClicsTime.kt b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/ClicsTime.kt new file mode 100644 index 000000000..3d594f7ca --- /dev/null +++ b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/ClicsTime.kt @@ -0,0 +1,73 @@ +package org.icpclive.clics.time + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +private fun DateTimeFormatBuilder.WithDateTimeComponents.formatBase(padding: Padding) { + year(padding) + char('-') + monthNumber(padding) + char('-') + dayOfMonth(padding) + char('T') + hour(padding) + char(':') + minute(padding) + char(':') + second(padding) + optional { + char('.') + secondFraction() + } + alternativeParsing({}, { char('z') }) { + optional(ifZero = "Z") { + offsetHours(padding) + optional { + alternativeParsing({}) { chars(":") } + offsetMinutesOfHour() + } + } + } +} + +private val format = DateTimeComponents.Format { + alternativeParsing({ formatBase(Padding.NONE) }) { + dateTimeComponents(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) + } +} + +internal fun parseClicsTime(csTime: CharSequence) = format.parse(csTime).toInstantUsingOffset() +internal fun formatClicsTime(instant: Instant) = instant.format(format, TimeZone.currentSystemDefault().offsetAt(instant)) + +private val RELATIVE_TIME_PATTERN = Regex("^([-+])?(([0-9]+):)?(([0-9]+):)?([0-9]+([.][0-9]+)?)$") +internal fun parseClicsRelativeTime(csTime: CharSequence): Duration { + val matcher = RELATIVE_TIME_PATTERN.matchAt(csTime, 0) ?: error("Invalid relative time format $csTime") + val signStr = matcher.groups[1]?.value + var hourStr = matcher.groups[3]?.value + var minuteStr = matcher.groups[5]?.value + if (minuteStr == null && hourStr != null) { + minuteStr = hourStr + hourStr = null + } + val secondStr = matcher.groups[6]!!.value + val sign = if (signStr != null && signStr == "-") -1 else 1 + val hour = hourStr?.toInt() ?: 0 + val minute = minuteStr?.toInt() ?: 0 + val second = secondStr.toDouble() + return (hour.hours + minute.minutes + second.seconds) * sign +} + +internal fun formatClicsRelativeTime(duration: Duration) = + (if (duration.isPositive()) duration else -duration).toComponents { hours, minutes, seconds, nanoseconds -> + "%s%02d:%02d:%02d.%03d".format( + if (duration.isPositive()) "" else "-", + hours, + minutes, + seconds, + nanoseconds / 1000000 + ) + } \ No newline at end of file diff --git a/src/clics-api/src/main/kotlin/org/icpclive/clics/time/DurationSerializer.kt b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/DurationSerializer.kt new file mode 100644 index 000000000..3da03c671 --- /dev/null +++ b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/DurationSerializer.kt @@ -0,0 +1,21 @@ +package org.icpclive.clics.time + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.icpclive.clics.time.formatClicsRelativeTime +import org.icpclive.clics.time.parseClicsRelativeTime +import kotlin.time.Duration + +internal object DurationSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ClicsDuration", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Duration) { + encoder.encodeString(formatClicsRelativeTime(value)) + } + + override fun deserialize(decoder: Decoder): Duration { + return parseClicsRelativeTime(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/src/clics-api/src/main/kotlin/org/icpclive/clics/time/InstantSerializer.kt b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/InstantSerializer.kt new file mode 100644 index 000000000..99528c906 --- /dev/null +++ b/src/clics-api/src/main/kotlin/org/icpclive/clics/time/InstantSerializer.kt @@ -0,0 +1,21 @@ +package org.icpclive.clics.time + +import kotlinx.datetime.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.icpclive.clics.time.formatClicsTime +import org.icpclive.clics.time.parseClicsTime + +internal object InstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ClicsInstant", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(formatClicsTime(value)) + } + + override fun deserialize(decoder: Decoder): Instant { + return parseClicsTime(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/src/clics-api/src/test/kotlin/org/icpclive/clics/ClicksTimeTest.kt b/src/clics-api/src/test/kotlin/org/icpclive/clics/time/ClicsTimeTest.kt similarity index 52% rename from src/clics-api/src/test/kotlin/org/icpclive/clics/ClicksTimeTest.kt rename to src/clics-api/src/test/kotlin/org/icpclive/clics/time/ClicsTimeTest.kt index f4db20056..dcdca7d24 100644 --- a/src/clics-api/src/test/kotlin/org/icpclive/clics/ClicksTimeTest.kt +++ b/src/clics-api/src/test/kotlin/org/icpclive/clics/time/ClicsTimeTest.kt @@ -1,11 +1,13 @@ -package org.icpclive.clics +package org.icpclive.clics.time +import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.icpclive.clics.time.* import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import kotlin.math.roundToLong +import kotlin.math.* import kotlin.test.* import kotlin.time.Duration import kotlin.time.Duration.Companion.hours @@ -13,72 +15,78 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit -object ClicksTimeTest { - var years = arrayOf(arrayOf("2001", 2001), arrayOf("2345", 2345)) +object ClicsTimeTest { + var years = arrayOf( + "2001" to 2001, + "2345" to 2345 + ) var months = arrayOf( - arrayOf("1", 1), - arrayOf("01", 1), - arrayOf("1", 1), - arrayOf("01", 1), - arrayOf("12", 12) + "1" to 1, + "01" to 1, + "1" to 1, + "01" to 1, + "12" to 12 + ) + var days = arrayOf( + "7" to 7, + "22" to 22 ) - var days = arrayOf(arrayOf("7", 7), arrayOf("22", 22)) var hours = arrayOf( - arrayOf("00", 0), - arrayOf("0", 0), - arrayOf("1", 1), - arrayOf("8", 8), - arrayOf("08", 8), - arrayOf("23", 23) + "00" to 0, + "0" to 0, + "1" to 1, + "8" to 8, + "08" to 8, + "23" to 23 ) var minutes = arrayOf( - arrayOf("00", 0), - arrayOf("0", 0), - arrayOf("8", 8), - arrayOf("08", 8), - arrayOf("59", 59) + "00" to 0, + "0" to 0, + "8" to 8, + "08" to 8, + "59" to 59 ) var seconds = arrayOf( - arrayOf("00", 0.0), - arrayOf("0", 0.0), - arrayOf("8", 8.0), - arrayOf("08", 8.0), - arrayOf("59", 59.0), - arrayOf("1.2", 1.2), - arrayOf("1.23", 1.23), - arrayOf("1.234", 1.234), - arrayOf("1.23456", 1.234) + "00" to 0.0, + "0" to 0.0, + "8" to 8.0, + "08" to 8.0, + "59" to 59.0, + "1.2" to 1.2, + "1.23" to 1.23, + "1.234" to 1.234, + "1.23456" to 1.234 ) var zones = arrayOf( - arrayOf("", "+00:00"), - arrayOf("Z", "+00:00"), - arrayOf("z", "+00:00"), - arrayOf("+12", "+12:00"), - arrayOf("+1", "+01:00"), - arrayOf("-730", "-07:30"), - arrayOf("+1234", "+12:34"), - arrayOf("-12:34", "-12:34") + "" to "+00:00", + "Z" to "+00:00", + "z" to "+00:00", + "+12" to "+12:00", + "+1" to "+01:00", + "-730" to "-07:30", + "+1234" to "+12:34", + "-12:34" to "-12:34" ) - var signs = arrayOf(arrayOf("", 1), arrayOf("+", 1), arrayOf("-", -1)) @Test fun parseTimeTest() { - for (YY in years) for (MM in months) for (DD in days) for (hh in hours) for (mm in minutes) for (ss in seconds) for (zz in zones) { - val testTime = String.format( - "%s-%s-%sT%s:%s:%s%s", - YY[0], MM[0], DD[0], hh[0], mm[0], ss[0], zz[0] - ) - val s = ss[1] as Double - val `is` = s.toInt() - val ns = Math.rint(1e9 * (s - `is`)).toInt() - val isoDateTime = String.format( - "%04d-%02d-%02dT%02d:%02d:%02d.%09d%s", - YY[1], MM[1], DD[1], hh[1], mm[1], `is`, ns, zz[1] - ) - val zdt = ZonedDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME) - val expect = zdt.toInstant().toEpochMilli() - val result: Long = ClicsTime.parseTime(testTime).toEpochMilliseconds() - assertEquals(expect, result) + for (YY in years) for (MM in months) for (DD in days) { + for (hh in hours) for (mm in minutes) for (ss in seconds) for (zz in zones) { + val testTime = "${YY.first}-${MM.first}-${DD.first}T${hh.first}:${mm.first}:${ss.first}${zz.first}" + val intS = floor(ss.second).toInt() + val ns = round(1e9 * (ss.second - intS)).toInt() + val isoDateTime = String.format( + "%04d-%02d-%02dT%02d:%02d:%02d.%09d%s", + YY.second, MM.second, DD.second, hh.second, mm.second, intS, ns, zz.second + ) + val zdt = ZonedDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME) + val expect = zdt.toInstant().toEpochMilli() + val result: Long = parseClicsTime(testTime).toEpochMilliseconds() + assertEquals(expect, result) + val formatted = formatClicsTime(Instant.fromEpochMilliseconds(expect)) + val parsedBack = ZonedDateTime.parse(formatted, DateTimeFormatter.ISO_DATE_TIME) + assertEquals(parsedBack.toInstant(), zdt.toInstant()) + } } } @@ -110,7 +118,11 @@ object ClicksTimeTest { "1.234" to 1.234, "1.23456" to 1.235 ) - var relSigns = arrayOf("" to 1, "+" to 1, "-" to -1) + var relSigns = arrayOf( + "" to 1, + "+" to 1, + "-" to -1 + ) @Test fun parseRelativeTimeTest() { @@ -132,7 +144,7 @@ object ClicksTimeTest { "%s%02d:%02d:%02d.%09d", if (pp.second == 1) "+" else "-", hh.second, mm.second, `is`, ns ) - val result: Long = ClicsTime.parseRelativeTime(testRelTime).toDouble(DurationUnit.MILLISECONDS).roundToLong() + val result: Long = parseClicsRelativeTime(testRelTime).toDouble(DurationUnit.MILLISECONDS).roundToLong() assertEquals(expect, result, "$testRelTime vs $isoInstant") } } @@ -141,16 +153,16 @@ object ClicksTimeTest { fun durationJsonSerialisation() { @Serializable data class ObjectWithDuration( - @Serializable(with = ClicsTime.DurationSerializer::class) - val dur: Duration + @Serializable(with = DurationSerializer::class) + val dur: Duration, ) for (pp in relSigns) for (hh in relHours) for (mm in relMinutes) for (ss in relSeconds) { hh.first ?: continue mm.first ?: continue val duration = (hh.second.hours + mm.second.minutes + ss.second.seconds) * pp.second - val durationString = ClicsTime.formatIso(duration) - ClicsTime.parseRelativeTime(durationString) + val durationString = formatClicsRelativeTime(duration) + parseClicsRelativeTime(durationString) val obj = ObjectWithDuration(duration) val encodedString = Json.encodeToString(obj) assertEquals("{\"dur\":\"$durationString\"}", encodedString) diff --git a/src/ksp/src/main/kotlin/org/icpclive/ksp/clics/FeedVersionsProcessor.kt b/src/ksp/src/main/kotlin/org/icpclive/ksp/clics/FeedVersionsProcessor.kt index 91a6ca1a4..be4a1167f 100644 --- a/src/ksp/src/main/kotlin/org/icpclive/ksp/clics/FeedVersionsProcessor.kt +++ b/src/ksp/src/main/kotlin/org/icpclive/ksp/clics/FeedVersionsProcessor.kt @@ -227,8 +227,8 @@ class FeedVersionsProcessor(private val generator: CodeGenerator, val logger: KS private fun MyCodeGenerator.serilizableWith(resolve: KSType) { when (resolve.declaration.qualifiedName!!.asString()) { "org.icpclive.clics.Url" -> +"@Contextual" - "kotlinx.datetime.Instant" -> serializable("org.icpclive.clics.ClicsTime.InstantSerializer") - "kotlin.time.Duration" -> serializable("org.icpclive.clics.ClicsTime.DurationSerializer") + "kotlinx.datetime.Instant" -> serializable("org.icpclive.clics.time.InstantSerializer") + "kotlin.time.Duration" -> serializable("org.icpclive.clics.time.DurationSerializer") } }