From 5a920eaee7d640f4f5f7c956eb6706f7b333de81 Mon Sep 17 00:00:00 2001 From: dkhalanskyjb <52952525+dkhalanskyjb@users.noreply.github.com> Date: Mon, 17 Jan 2022 18:11:29 +0300 Subject: [PATCH] Fill the gaps in the documentation (#157) Document time scale, leap second handling in ISO-8601 <-> Instant conversions Co-authored-by: Ilya Gorbunov --- core/common/src/Clock.kt | 17 +++ core/common/src/DateTimePeriod.kt | 139 ++++++++++++++++++ core/common/src/DateTimeUnit.kt | 119 ++++++++++++++- core/common/src/DayOfWeek.kt | 10 ++ core/common/src/Instant.kt | 58 ++++++-- core/common/src/LocalDate.kt | 27 +++- core/common/src/LocalDateTime.kt | 15 ++ core/common/src/Month.kt | 34 ++++- core/common/src/TimeZone.kt | 29 +++- core/common/src/UtcOffset.kt | 51 +++++++ .../serializers/DateTimePeriodSerializers.kt | 34 ++++- .../serializers/DateTimeUnitSerializers.kt | 32 +++- .../src/serializers/DayOfWeekSerializers.kt | 5 + .../src/serializers/InstantSerializers.kt | 13 ++ .../src/serializers/LocalDateSerializers.kt | 13 ++ .../serializers/LocalDateTimeSerializers.kt | 15 +- .../src/serializers/MonthSerializers.kt | 7 +- .../src/serializers/TimeZoneSerializers.kt | 18 +++ core/common/test/InstantTest.kt | 1 + core/native/src/Instant.kt | 2 +- 20 files changed, 602 insertions(+), 37 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index def6813d4..f91ee8369 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -7,9 +7,20 @@ package kotlinx.datetime import kotlin.time.* +/** + * A source of [Instant] values. + * + * See [Clock.System][Clock.System] for the clock instance that queries the operating system. + */ public interface Clock { + /** + * Returns the [Instant] corresponding to the current time, according to this clock. + */ public fun now(): Instant + /** + * The [Clock] instance that queries the operating system as its source of knowledge of time. + */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() } @@ -19,9 +30,15 @@ public interface Clock { } } +/** + * Returns the current date at the given [time zone][timeZone], according to [this Clock][this]. + */ public fun Clock.todayAt(timeZone: TimeZone): LocalDate = now().toLocalDateTime(timeZone).date +/** + * Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark. + */ @ExperimentalTime public fun Clock.asTimeSource(): TimeSource = object : TimeSource { override fun markNow(): TimeMark = InstantTimeMark(now(), this@asTimeSource) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index e1fbee79b..139dfcd09 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -11,23 +11,70 @@ import kotlin.math.* import kotlin.time.Duration import kotlinx.serialization.Serializable +/** + * A difference between two [instants][Instant], decomposed into date and time components. + * + * The date components are: [years], [months], [days]. + * + * The time components are: [hours], [minutes], [seconds], [nanoseconds]. + * + * A `DateTimePeriod` can be constructed using the same-named constructor function, + * [parsed][DateTimePeriod.parse] from a string, or returned as the result of instant arithmetic operations (see [Instant.periodUntil]). + * All these functions can return a [DatePeriod] value, which is a subtype of `DateTimePeriod`, + * a special case that only stores date components, if all time components of the result happen to be zero. + */ @Serializable(with = DateTimePeriodIso8601Serializer::class) // TODO: could be error-prone without explicitly named params public sealed class DateTimePeriod { internal abstract val totalMonths: Int + + /** + * The number of calendar days. + * + * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details. + */ public abstract val days: Int internal abstract val totalNanoseconds: Long + /** + * The number of whole years. + */ public val years: Int get() = totalMonths / 12 + + /** + * The number of months in this period that don't form a whole year, so this value is always in `(-11..11)`. + */ public val months: Int get() = totalMonths % 12 + + /** + * The number of whole hours in this period. + */ public open val hours: Int get() = (totalNanoseconds / 3_600_000_000_000).toInt() + + /** + * The number of whole minutes in this period that don't form a whole hour, so this value is always in `(-59..59)`. + */ public open val minutes: Int get() = ((totalNanoseconds % 3_600_000_000_000) / 60_000_000_000).toInt() + + /** + * The number of whole seconds in this period that don't form a whole minute, so this value is always in `(-59..59)`. + */ public open val seconds: Int get() = ((totalNanoseconds % 60_000_000_000) / NANOS_PER_ONE).toInt() + + /** + * The number of whole nanoseconds in this period that don't form a whole second, so this value is always in + * `(-999_999_999..999_999_999)`. + */ public open val nanoseconds: Int get() = (totalNanoseconds % NANOS_PER_ONE).toInt() private fun allNonpositive() = totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or days != 0 || totalNanoseconds != 0L) + /** + * Converts this period to the ISO-8601 string representation for durations. + * + * @see DateTimePeriod.parse + */ override fun toString(): String = buildString { val sign = if (allNonpositive()) { append('-'); -1 } else 1 append('P') @@ -70,6 +117,21 @@ public sealed class DateTimePeriod { } public companion object { + /** + * Parses a ISO-8601 duration string as a [DateTimePeriod]. + * If the time components are absent or equal to zero, returns a [DatePeriod]. + * + * Additionally, we support the `W` signifier to represent weeks. + * + * Examples of durations in the ISO-8601 format: + * - `P1Y40D` is one year and 40 days + * - `-P1DT1H` is minus (one day and one hour) + * - `P1DT-1H` is one day minus one hour + * - `-PT0.000000001S` is minus one nanosecond + * + * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are + * exceeded. + */ public fun parse(text: String): DateTimePeriod { fun parseException(message: String, position: Int): Nothing = throw DateTimeFormatException("Parse error at char $position: $message") @@ -234,8 +296,26 @@ public sealed class DateTimePeriod { } } +/** + * Parses the ISO-8601 duration representation as a [DateTimePeriod]. + * + * See [DateTimePeriod.parse] for examples. + * + * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are exceeded. + * + * @see DateTimePeriod.parse + */ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this) +/** + * A special case of [DateTimePeriod] that only stores date components and has all time components equal to zero. + * + * A `DatePeriod` is automatically returned from all constructor functions for [DateTimePeriod] if it turns out that + * the time components are zero. + * + * `DatePeriod` values are used in operations on [LocalDates][LocalDate] and are returned from operations on [LocalDates][LocalDate], + * but they also can be passed anywhere a [DateTimePeriod] is expected. + */ @Serializable(with = DatePeriodIso8601Serializer::class) public class DatePeriod internal constructor( internal override val totalMonths: Int, @@ -243,13 +323,31 @@ public class DatePeriod internal constructor( ) : DateTimePeriod() { public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days) // avoiding excessive computations + /** The number of whole hours in this period. Always equal to zero. */ override val hours: Int get() = 0 + + /** The number of whole minutes in this period. Always equal to zero. */ override val minutes: Int get() = 0 + + /** The number of whole seconds in this period. Always equal to zero. */ override val seconds: Int get() = 0 + + /** The number of nanoseconds in this period. Always equal to zero. */ override val nanoseconds: Int get() = 0 internal override val totalNanoseconds: Long get() = 0 public companion object { + /** + * Parses the ISO-8601 duration representation as a [DatePeriod]. + * + * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not + * zero. + * + * @throws IllegalArgumentException if the text cannot be parsed, the boundaries of [DatePeriod] are exceeded, + * or any time components are not zero. + * + * @see DateTimePeriod.parse + */ public fun parse(text: String): DatePeriod = when (val period = DateTimePeriod.parse(text)) { is DatePeriod -> period @@ -258,6 +356,17 @@ public class DatePeriod internal constructor( } } +/** + * Parses the ISO-8601 duration representation as a [DatePeriod]. + * + * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not + * zero. + * + * @throws IllegalArgumentException if the text cannot be parsed, the boundaries of [DatePeriod] are exceeded, + * or any time components are not zero. + * + * @see DateTimePeriod.parse + */ public fun String.toDatePeriod(): DatePeriod = DatePeriod.parse(this) private class DateTimePeriodImpl( @@ -295,6 +404,19 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos else DatePeriod(totalMonths, days) +/** + * Constructs a new [DateTimePeriod]. If all the time components are zero, returns a [DatePeriod]. + * + * It is recommended to always explicitly name the arguments when constructing this manually, + * like `DateTimePeriod(years = 1, months = 12)`. + * + * The passed numbers are not stored as is but are normalized instead for human readability, so, for example, + * `DateTimePeriod(months = 24)` becomes `DateTimePeriod(years = 2)`. + * + * @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int]. + * @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds] + * overflows a [Long]. + */ public fun DateTimePeriod( years: Int = 0, months: Int = 0, @@ -306,14 +428,31 @@ public fun DateTimePeriod( ): DateTimePeriod = buildDateTimePeriod(totalMonths(years, months), days, totalNanoseconds(hours, minutes, seconds, nanoseconds)) +/** + * Constructs a [DateTimePeriod] from a [Duration]. + * + * If the duration value is too big to be represented as a [Long] number of nanoseconds, + * the result will be [Long.MAX_VALUE] nanoseconds. + */ +// TODO: maybe it's more consistent to throw here on overflow? public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds) +/** + * Adds two [DateTimePeriod] instances. + * + * @throws DateTimeArithmeticException if arithmetic overflow happens. + */ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = buildDateTimePeriod( safeAdd(totalMonths, other.totalMonths), safeAdd(days, other.days), safeAdd(totalNanoseconds, other.totalNanoseconds), ) +/** + * Adds two [DatePeriod] instances. + * + * @throws DateTimeArithmeticException if arithmetic overflow happens. + */ public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( safeAdd(totalMonths, other.totalMonths), safeAdd(days, other.days), diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 06a4467f7..929edaa36 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -10,13 +10,38 @@ import kotlinx.serialization.Serializable import kotlin.time.* import kotlin.time.Duration.Companion.nanoseconds +/** + * A unit for measuring time. + * + * See the predefined constants for time units, like [DateTimeUnit.NANOSECOND], [DateTimeUnit.DAY], + * [DateTimeUnit.MONTH], and others. + * + * Two ways are provided to create custom [DateTimeUnit] instances: + * - By multiplying an existing unit on the right by an integer scalar: for example, `DateTimeUnit.NANOSECOND * 10`. + * - By constructing an instance manually with [TimeBased], [DayBased], or [MonthBased]: for example, + * `TimeBased(nanoseconds = 10)`. + * + * Note that a calendar day is not considered identical to 24 hours. See [DateTimeUnit.DayBased] for a discussion. + */ @Serializable(with = DateTimeUnitSerializer::class) public sealed class DateTimeUnit { + /** Produces a date-time unit that is a multiple of this unit times the specified integer [scalar] value. */ public abstract operator fun times(scalar: Int): DateTimeUnit + /** + * A date-time unit that has the precise time duration. + * + * Such units are independent of the time zone. + * Any such unit can be represented as some fixed number of nanoseconds. + */ @Serializable(with = TimeBasedDateTimeUnitSerializer::class) - public class TimeBased(public val nanoseconds: Long) : DateTimeUnit() { + public class TimeBased( + /** + * The length of this unit in nanoseconds. + */ + public val nanoseconds: Long + ) : DateTimeUnit() { private val unitName: String private val unitScale: Long @@ -53,17 +78,27 @@ public sealed class DateTimeUnit { override fun times(scalar: Int): TimeBased = TimeBased(safeMultiply(nanoseconds, scalar.toLong())) + /** + * The length of this unit as a [Duration]. + */ public val duration: Duration get() = nanoseconds.nanoseconds override fun equals(other: Any?): Boolean = - this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds) + this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds) override fun hashCode(): Int = nanoseconds.toInt() xor (nanoseconds shr Int.SIZE_BITS).toInt() override fun toString(): String = formatToString(unitScale, unitName) } + /** + * A date-time unit equal to some number of days or months. + * + * Operations involving `DateBased` units are performed on dates. The same operations on [Instants][Instant] + * require a [TimeZone] to find the corresponding [LocalDateTimes][LocalDateTime] first to perform + * the operation with the date component of these `LocalDateTime` values. + */ @Serializable(with = DateBasedDateTimeUnitSerializer::class) public sealed class DateBased : DateTimeUnit() { @Suppress("TOPLEVEL_TYPEALIASES_ONLY") @@ -74,8 +109,23 @@ public sealed class DateTimeUnit { public typealias MonthBased = DateTimeUnit.MonthBased } + /** + * A date-time unit equal to some number of calendar days. + * + * A calendar day is not considered identical to 24 hours, thus a `DayBased`-unit cannot be expressed as a multiple of some [TimeBased]-unit. + * + * The reason lies in time zone transitions, because of which some days can be 23 or 25 hours. + * For example, we say that exactly a whole day has passed between `2019-10-27T02:59` and `2019-10-28T02:59` + * in Berlin, despite the fact that the clocks were turned back one hour, so there are, in fact, 25 hours + * between the two date-times. + */ @Serializable(with = DayBasedDateTimeUnitSerializer::class) - public class DayBased(public val days: Int) : DateBased() { + public class DayBased( + /** + * The length of this unit in days. + */ + public val days: Int + ) : DateBased() { init { require(days > 0) { "Unit duration must be positive, but was $days days." } } @@ -93,8 +143,18 @@ public sealed class DateTimeUnit { formatToString(days, "DAY") } + /** + * A date-time unit equal to some number of months. + * + * Since different months have different number of days, a `MonthBased`-unit cannot be expressed a multiple of some [DayBased]-unit. + */ @Serializable(with = MonthBasedDateTimeUnitSerializer::class) - public class MonthBased(public val months: Int) : DateBased() { + public class MonthBased( + /** + * The length of this unit in months. + */ + public val months: Int + ) : DateBased() { init { require(months > 0) { "Unit duration must be positive, but was $months months." } } @@ -118,17 +178,68 @@ public sealed class DateTimeUnit { protected fun formatToString(value: Long, unit: String): String = if (value == 1L) unit else "$value-$unit" public companion object { + /** + * A nanosecond, which is `1/1_000_000_000` of a second. + */ public val NANOSECOND: TimeBased = TimeBased(nanoseconds = 1) + + /** + * A microsecond, which is `1/1_000_000` of a second, or `1_000` nanoseconds. + */ public val MICROSECOND: TimeBased = NANOSECOND * 1000 + + /** + * A millisecond, which is `1/1_000` of a second, or `1_000_000` nanoseconds. + */ public val MILLISECOND: TimeBased = MICROSECOND * 1000 + + /** + * A second. + */ public val SECOND: TimeBased = MILLISECOND * 1000 + + /** + * A minute, which is 60 seconds. + */ public val MINUTE: TimeBased = SECOND * 60 + + /** + * An hour, which is 60 minutes, or 3600 seconds. + */ public val HOUR: TimeBased = MINUTE * 60 + + /** + * A calendar day. + * + * Note that a calendar day is not the same as 24 hours, see [DateTimeUnit.DayBased] for details. + */ public val DAY: DayBased = DayBased(days = 1) + + /** + * A week, which is 7 [calendar days][DAY]. + */ public val WEEK: DayBased = DAY * 7 + + /** + * A month. + * + * Note that a month doesn't have a constant number of calendar days in it. + */ public val MONTH: MonthBased = MonthBased(months = 1) + + /** + * A quarter, which is three [months][MONTH]. + */ public val QUARTER: MonthBased = MONTH * 3 + + /** + * A year, which is 12 [months][MONTH]. + */ public val YEAR: MonthBased = MONTH * 12 + + /** + * A century, which is 100 [years][YEAR], or 1200 [months][MONTH]. + */ public val CENTURY: MonthBased = YEAR * 100 } } diff --git a/core/common/src/DayOfWeek.kt b/core/common/src/DayOfWeek.kt index a93486554..946d2f166 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -7,6 +7,9 @@ package kotlinx.datetime import kotlin.native.concurrent.* +/** + * The enumeration class representing the days of the week. + */ public expect enum class DayOfWeek { MONDAY, TUESDAY, @@ -17,10 +20,17 @@ public expect enum class DayOfWeek { SUNDAY; } +/** + * The ISO-8601 number of the given day of the week. Monday is 1, Sunday is 7. + */ public val DayOfWeek.isoDayNumber: Int get() = ordinal + 1 @SharedImmutable private val allDaysOfWeek = DayOfWeek.values().asList() + +/** + * Returns the [DayOfWeek] instance for the given ISO-8601 week day number. Monday is 1, Sunday is 7. + */ public fun DayOfWeek(isoDayNumber: Int): DayOfWeek { require(isoDayNumber in 1..7) return allDaysOfWeek[isoDayNumber - 1] diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 557e8592d..53de5477e 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -9,6 +9,27 @@ import kotlinx.datetime.serializers.InstantIso8601Serializer import kotlinx.serialization.Serializable import kotlin.time.* +/** + * A moment in time. + * + * A point in time must be uniquely identified, so that it is independent of a time zone. + * For example, `1970-01-01, 00:00:00` does not represent a moment in time, since this would happen at different times + * in different time zones: someone in Tokyo would think its already `1970-01-01` several hours earlier than someone in + * Berlin would. To represent such entities, use [LocalDateTime]. + * In contrast, "the moment the clocks in London first showed 00:00 on Jan 1, 2000" is a specific moment + * in time, as is "1970-01-01, 00:00:00 UTC+0", and so it can be represented as an [Instant]. + * + * `Instant` uses the UTC-SLS (smeared leap second) time scale. This time scale doesn't contain instants + * corresponding to leap seconds, but instead "smears" positive and negative leap seconds among the last 1000 seconds + * of the day when a leap second happens. + * + * Some ways in which [Instant] can be acquired are: + * - [Clock.now] can be used to query the current moment for the given clock. With [Clock.System], it is the current + * moment as the platform sees it. + * - [Instant.parse] parses an ISO-8601 string. + * - [Instant.fromEpochMilliseconds] and [Instant.fromEpochSeconds] construct the instant values from the amount of time + * since `1970-01-01T00:00:00Z` (the Unix epoch). + */ @Serializable(with = InstantIso8601Serializer::class) public expect class Instant : Comparable { @@ -18,6 +39,8 @@ public expect class Instant : Comparable { * The difference between the rounded number of seconds and the actual number of seconds * is returned by [nanosecondsOfSecond] property expressed in nanoseconds. * + * Note that this number doesn't include leap seconds added or removed since the epoch. + * * @see Instant.fromEpochSeconds */ public val epochSeconds: Long @@ -76,7 +99,7 @@ public expect class Instant : Comparable { /** * Compares `this` instant with the [other] instant. - * Returns zero if this instant represent the same moment as the other (i.e. equal to other), + * Returns zero if this instant represents the same moment as the other (i.e. equal to other), * a negative number if this instant is earlier than the other, * and a positive number if this instant is later than the other. */ @@ -85,6 +108,11 @@ public expect class Instant : Comparable { /** * Converts this instant to the ISO-8601 string representation. * + * The representation uses the UTC-SLS time scale, instead of UTC. + * In practice, this means that leap second handling will not be readjusted to the UTC. + * Leap seconds will not be added or skipped, so it is impossible to acquire a string + * where the component for seconds is 60, and for any day, it's possible to observe 23:59:59. + * * @see Instant.parse */ public override fun toString(): String @@ -133,6 +161,12 @@ public expect class Instant : Comparable { * - `2020-08-30T18:40.00+03:00` * - `2020-08-30T18:40.00+03:30:20` * + * The string is considered to represent time on the UTC-SLS time scale instead of UTC. + * In practice, this means that, even if there is a leap second on the given day, it will not affect how the + * time is parsed, even if it's in the last 1000 seconds of the day. + * Instead, even if there is a negative leap second on the given day, 23:59:59 is still considered valid time. + * 23:59:60 is invalid on UTC-SLS, so parsing it will fail. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded. */ public fun parse(isoString: String): Instant @@ -141,16 +175,16 @@ public expect class Instant : Comparable { /** * An instant value that is far in the past. * - * All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be converted to [LocalDateTime][Instant.toLocalDateTime] - * without exceptions on all supported platforms. + * All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be [converted][Instant.toLocalDateTime] to + * [LocalDateTime] without exceptions on all supported platforms. */ public val DISTANT_PAST: Instant // -100001-12-31T23:59:59.999999999Z /** * An instant value that is far in the future. * - * All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be converted to [LocalDateTime][Instant.toLocalDateTime] - * without exceptions on all supported platforms. + * All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be [converted][Instant.toLocalDateTime] to + * [LocalDateTime] without exceptions on all supported platforms. */ public val DISTANT_FUTURE: Instant // +100000-01-01T00:00:00Z @@ -159,11 +193,11 @@ public expect class Instant : Comparable { } } -/** Returns true if the instant is not later than [Instant.DISTANT_PAST]. */ +/** Returns true if the instant is [Instant.DISTANT_PAST] or earlier. */ public val Instant.isDistantPast: Boolean get() = this <= Instant.DISTANT_PAST -/** Returns true if the instant is not earlier than [Instant.DISTANT_FUTURE]. */ +/** Returns true if the instant is [Instant.DISTANT_FUTURE] or later. */ public val Instant.isDistantFuture: Boolean get() = this >= Instant.DISTANT_FUTURE @@ -171,7 +205,7 @@ public val Instant.isDistantFuture: Boolean * Converts this string representing an instant in ISO-8601 format including date and time components and * the time zone offset to an [Instant] value. * - * See [Instant.parse] for examples of instant string representations. + * See [Instant.parse] for examples of instant string representations and discussion of leap seconds. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded. */ @@ -215,8 +249,8 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = * - negative or zero if this instant is later than the other, * - exactly zero if this instant is equal to the other. * - * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. Also (only - * on JVM) if the number of months between the two dates exceeds an Int. + * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * Or (only on the JVM) if the number of months between the two dates exceeds an Int. */ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod @@ -298,8 +332,8 @@ public fun Instant.yearsUntil(other: Instant, timeZone: TimeZone): Int = * - positive or zero if this instant is later than the other, * - exactly zero if this instant is equal to the other. * - * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. Also (only - * on JVM) if the number of months between the two dates exceeds an Int. + * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * Or (only on the JVM) if the number of months between the two dates exceeds an Int. * @see Instant.periodUntil */ public fun Instant.minus(other: Instant, timeZone: TimeZone): DateTimePeriod = diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index bea808ef6..e3124e398 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -8,6 +8,17 @@ package kotlinx.datetime import kotlinx.datetime.serializers.LocalDateIso8601Serializer import kotlinx.serialization.Serializable +/** + * The date part of [LocalDateTime]. + * + * This class represents dates without a reference to a particular time zone. + * As such, these objects may denote different spans of time in different time zones: for someone in Berlin, + * `2020-08-30` started and ended at different moments from those for someone in Tokyo. + * + * The arithmetic on [LocalDate] values is defined independently of the time zone (so `2020-08-30` plus one day + * is `2020-08-31` everywhere): see various [LocalDate.plus] and [LocalDate.minus] functions, as well + * as [LocalDate.periodUntil] and various other [*until][LocalDate.daysUntil] functions. + */ @Serializable(with = LocalDateIso8601Serializer::class) public expect class LocalDate : Comparable { public companion object { @@ -36,8 +47,8 @@ public expect class LocalDate : Comparable { * - [monthNumber] `1..12` * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month * - * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the given [monthNumber] and - * [year]. + * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the + * given [monthNumber] and [year]. */ public constructor(year: Int, monthNumber: Int, dayOfMonth: Int) @@ -50,8 +61,8 @@ public expect class LocalDate : Comparable { * - [month] all values of the [Month] enum * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month * - * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the given [month] and - * [year]. + * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the + * given [month] and [year]. */ public constructor(year: Int, month: Month, dayOfMonth: Int) @@ -76,7 +87,6 @@ public expect class LocalDate : Comparable { */ public override fun compareTo(other: LocalDate): Int - /** * Converts this date to the ISO-8601 string representation. * @@ -95,7 +105,7 @@ public expect class LocalDate : Comparable { public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) /** - * Combines this date components with the specified time components into a [LocalDateTime] value. + * Combines this date's components with the specified time components into a [LocalDateTime] value. * * For finding an instant that corresponds to the start of a date in a particular time zone consider using * [LocalDate.atStartOfDayIn] function because a day does not always start at the fixed time 0:00:00. @@ -172,6 +182,11 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * - zero if this date is equal to the other. * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result. + * + * @see LocalDate.daysUntil + * @see LocalDate.monthsUntil + * @see LocalDate.yearsUntil + * */ public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index a7d206533..cd16c35f1 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -8,6 +8,20 @@ package kotlinx.datetime import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer import kotlinx.serialization.Serializable +/** + * The representation of a specific civil date and time without a reference to a particular time zone. + * + * This class does not describe specific *moments in time*, which are represented as [Instant] values. + * Instead, its instances can be thought of as clock readings, something that an observer in a particular time zone + * could witness. + * For example, `2020-08-30T18:43` is not a *moment in time*, since someone in Berlin and someone in Tokyo would witness + * this on their clocks at different times. + * + * The main purpose of this class is to provide human-readable representations of [Instant] values, or to transfer them + * as data. + * + * The arithmetic on [LocalDateTime] values is not provided, since without accounting for the time zone transitions it may give misleading results. + */ @Serializable(with = LocalDateTimeIso8601Serializer::class) public expect class LocalDateTime : Comparable { public companion object { @@ -99,6 +113,7 @@ public expect class LocalDateTime : Comparable { * a negative number if this value represents earlier civil time than the other, * and a positive number if this value represents later civil time than the other. */ + // TODO: add a note about pitfalls of comparing localdatetimes falling in the Autumn transition public override operator fun compareTo(other: LocalDateTime): Int /** diff --git a/core/common/src/Month.kt b/core/common/src/Month.kt index 8ee98566c..8966d6c10 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -7,32 +7,64 @@ package kotlinx.datetime import kotlin.native.concurrent.* +/** + * The enumeration class representing the 12 months of the year. + */ public expect enum class Month { + /** January, month #01, with 31 days. */ JANUARY, + + /** February, month #02, with 28 days, or 29 in leap years. */ FEBRUARY, + + /** March, month #03, with 31 days. */ MARCH, + + /** April, month #04, with 30 days. */ APRIL, + + /** May, month #05, with 31 days. */ MAY, + + /** June, month #06, with 30 days. */ JUNE, + + /** July, month #07, with 31 days. */ JULY, + + /** August, month #08, with 31 days. */ AUGUST, + + /** September, month #09, with 30 days. */ SEPTEMBER, + + /** October, month #10, with 31 days. */ OCTOBER, + + /** November, month #11, with 30 days. */ NOVEMBER, + + /** December, month #12, with 31 days. */ DECEMBER; // val value: Int // member missing in java.time.Month has to be an extension } +/** + * The number of the [Month]. January is 1, December is 12. + */ public val Month.number: Int get() = ordinal + 1 @SharedImmutable private val allMonths = Month.values().asList() +/** + * Returns the [Month] instance for the given month number. January is 1, December is 12. + */ public fun Month(number: Int): Month { require(number in 1..12) return allMonths[number - 1] } -// companion object members vs typealiasing to java.time.Month? +// companion object members vs type aliasing to java.time.Month? diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 8585c171c..137fa5206 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -11,6 +11,10 @@ package kotlinx.datetime import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable +/** + * A time zone, provides the conversion between [Instant] and [LocalDateTime] values + * using a collection of rules specifying which [LocalDateTime] value corresponds to each [Instant]. + */ @Serializable(with = TimeZoneSerializer::class) public expect open class TimeZone { /** @@ -20,6 +24,8 @@ public expect open class TimeZone { */ public val id: String + // TODO: Declare and document toString/equals/hashCode + public companion object { /** * Queries the current system time zone. @@ -56,7 +62,7 @@ public expect open class TimeZone { } /** - * Return a civil date/time value that this instant has in the time zone provided as an implicit receiver. + * Return the civil date/time value that this instant has in the time zone provided as an implicit receiver. * * Note that while this conversion is unambiguous, the inverse ([LocalDateTime.toInstant]) * is not necessary so. @@ -84,9 +90,16 @@ public expect open class TimeZone { public fun LocalDateTime.toInstant(): Instant } +/** + * A time zone that is known to always have the same offset from UTC. + */ @Serializable(with = FixedOffsetTimeZoneSerializer::class) public expect class FixedOffsetTimeZone : TimeZone { public constructor(offset: UtcOffset) + + /** + * The constant offset from UTC that this time zone has. + */ public val offset: UtcOffset @Deprecated("Use offset.totalSeconds", ReplaceWith("offset.totalSeconds")) @@ -105,7 +118,7 @@ public typealias ZoneOffset = FixedOffsetTimeZone public expect fun TimeZone.offsetAt(instant: Instant): UtcOffset /** - * Return a civil date/time value that this instant has in the specified [timeZone]. + * Returns a civil date/time value that this instant has in the specified [timeZone]. * * Note that while this conversion is unambiguous, the inverse ([LocalDateTime.toInstant]) * is not necessary so. @@ -116,9 +129,14 @@ public expect fun TimeZone.offsetAt(instant: Instant): UtcOffset */ public expect fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime +/** + * Returns a civil date/time value that this instant has in the specified [UTC offset][offset]. + * + * @see LocalDateTime.toInstant + * @see Instant.offsetIn + */ internal expect fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime - /** * Finds the offset from UTC the specified [timeZone] has at this instant of physical time. * @@ -144,6 +162,11 @@ public fun Instant.offsetIn(timeZone: TimeZone): UtcOffset = */ public expect fun LocalDateTime.toInstant(timeZone: TimeZone): Instant +/** + * Returns an instant that corresponds to this civil date/time value that happens at the specified [UTC offset][offset]. + * + * @see Instant.toLocalDateTime + */ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant /** diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 9dddc022f..0ef82951a 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -8,18 +8,69 @@ package kotlinx.datetime import kotlinx.datetime.serializers.UtcOffsetSerializer import kotlinx.serialization.Serializable +/** + * An offset from UTC. + * + * Examples of these values: + * - `Z`, an offset of zero; + * - `+05`, plus five hours; + * - `-02`, minus two hours; + * - `+03:30`, plus three hours and thirty minutes; + * - `+01:23:45`, plus one hour, 23 minutes, and 45 seconds. + */ @Serializable(with = UtcOffsetSerializer::class) public expect class UtcOffset { + /** + * The number of seconds from UTC. + * + * The larger the value, the earlier some specific civil date/time happens with the offset. + */ public val totalSeconds: Int + // TODO: Declare and document toString/equals/hashCode + public companion object { + /** + * The zero offset from UTC, `Z`. + */ public val ZERO: UtcOffset + + /** + * Parses a string that represents an offset in an ISO-8601 time shift extended format, also supporting + * specifying the number of seconds or not specifying the number of minutes. + * + * Examples of valid strings: + * - `Z` or `+00:00`, an offset of zero; + * - `+05`, five hours; + * - `-02`, minus two hours; + * - `+03:30`, three hours and thirty minutes; + * - `+01:23:45`, an hour, 23 minutes, and 45 seconds. + */ public fun parse(offsetString: String): UtcOffset } } + +/** + * Constructs a [UtcOffset] from hours, minutes, and seconds components. + * + * All components must have the same sign. + * + * The bounds are checked: it is invalid to pass something other than `±[0; 59]` as the number of seconds or minutes. + * For example, `UtcOffset(hours = 3, minutes = 61)` is invalid. + * + * However, the non-null component of the highest order can exceed these bounds, + * for example, `UtcOffset(minutes = 241)` is valid. + * + * @throws IllegalArgumentException if a component exceeds its bounds when a higher order component is specified. + * @throws IllegalArgumentException if components have different signs. + * @throws IllegalArgumentException if the resulting `UtcOffset` value is outside of range `±18:00`. + */ public expect fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset @Deprecated("Use UtcOffset.ZERO instead", ReplaceWith("UtcOffset.ZERO"), DeprecationLevel.ERROR) public fun UtcOffset(): UtcOffset = UtcOffset.ZERO +/** + * Returns the fixed-offset time zone with the given UTC offset. + */ public fun UtcOffset.asTimeZone(): FixedOffsetTimeZone = FixedOffsetTimeZone(this) \ No newline at end of file diff --git a/core/common/src/serializers/DateTimePeriodSerializers.kt b/core/common/src/serializers/DateTimePeriodSerializers.kt index 5e362850e..eb5b0814a 100644 --- a/core/common/src/serializers/DateTimePeriodSerializers.kt +++ b/core/common/src/serializers/DateTimePeriodSerializers.kt @@ -5,13 +5,17 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.DateTimePeriod +import kotlinx.datetime.* import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +/** + * A serializer for [DateTimePeriod] that uses a different field for each component, only listing non-zero components. + * + * JSON example: `{"days":1,"hours":-1}` + */ public object DateTimePeriodComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = @@ -66,6 +70,14 @@ public object DateTimePeriodComponentSerializer: KSerializer { } +/** + * A serializer for [DateTimePeriod] that represents it as an ISO-8601 duration string. + * + * JSON example: `"P1DT-1H"` + * + * @see DateTimePeriod.toString + * @see DateTimePeriod.parse + */ public object DateTimePeriodIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = @@ -80,6 +92,13 @@ public object DateTimePeriodIso8601Serializer: KSerializer { } +/** + * A serializer for [DatePeriod] that uses a different field for each component, only listing non-zero components. + * + * Deserializes the time components as well when they are present ensuring they are zero. + * + * JSON example: `{"months":1,"days":15}` + */ public object DatePeriodComponentSerializer: KSerializer { private fun unexpectedNonzero(fieldName: String, value: Long) { @@ -134,12 +153,21 @@ public object DatePeriodComponentSerializer: KSerializer { } +/** + * A serializer for [DatePeriod] that represents it as an ISO-8601 duration string. + * + * Deserializes the time components as well, as long as they are zero. + * + * JSON example: `"P2Y1M"` + * + * @see DatePeriod.toString + * @see DatePeriod.parse + */ public object DatePeriodIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DatePeriod", PrimitiveKind.STRING) - // TODO: consider whether should fail when parsing "P1YT0H0M0.0S" override fun deserialize(decoder: Decoder): DatePeriod = when (val period = DateTimePeriod.parse(decoder.decodeString())) { is DatePeriod -> period diff --git a/core/common/src/serializers/DateTimeUnitSerializers.kt b/core/common/src/serializers/DateTimeUnitSerializers.kt index 1874447b4..44a6c1215 100644 --- a/core/common/src/serializers/DateTimeUnitSerializers.kt +++ b/core/common/src/serializers/DateTimeUnitSerializers.kt @@ -5,7 +5,7 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.* import kotlinx.serialization.* import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -14,6 +14,11 @@ import kotlinx.serialization.encoding.* import kotlinx.serialization.internal.AbstractPolymorphicSerializer import kotlin.reflect.KClass +/** + * A serializer for [DateTimeUnit.TimeBased] unit that represents the unit as a [Long] number of nanoseconds. + * + * JSON example: `{"nanoseconds":1000000000}` + */ public object TimeBasedDateTimeUnitSerializer: KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TimeBased") { @@ -53,6 +58,11 @@ public object TimeBasedDateTimeUnitSerializer: KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DayBased") { @@ -92,6 +102,11 @@ public object DayBasedDateTimeUnitSerializer: KSerializer } } +/** + * A serializer for [DateTimeUnit.MonthBased] unit that represents the unit as an [Int] number of months. + * + * JSON example: `{"months":2}` + */ public object MonthBasedDateTimeUnitSerializer: KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MonthBased") { @@ -131,6 +146,11 @@ public object MonthBasedDateTimeUnitSerializer: KSerializer() { @@ -149,18 +169,22 @@ public object DateBasedDateTimeUnitSerializer: AbstractPolymorphicSerializer? = impl.findPolymorphicSerializerOrNull(encoder, value) - @OptIn(InternalSerializationApi::class) override val baseClass: KClass get() = DateTimeUnit.DateBased::class - @OptIn(InternalSerializationApi::class) override val descriptor: SerialDescriptor get() = impl.descriptor } +/** + * A polymorphic serializer for [DateTimeUnit] that represents the unit as the [Int] number of months or days, or + * the [Long] number of nanoseconds. + * + * JSON example: `{"type":"MonthBased","days":15}` + */ @Suppress("EXPERIMENTAL_API_USAGE_ERROR", "INVISIBLE_MEMBER") public object DateTimeUnitSerializer: AbstractPolymorphicSerializer() { @@ -177,12 +201,10 @@ public object DateTimeUnitSerializer: AbstractPolymorphicSerializer? = impl.findPolymorphicSerializerOrNull(encoder, value) - @OptIn(InternalSerializationApi::class) override val baseClass: KClass get() = DateTimeUnit::class - @OptIn(InternalSerializationApi::class) override val descriptor: SerialDescriptor get() = impl.descriptor diff --git a/core/common/src/serializers/DayOfWeekSerializers.kt b/core/common/src/serializers/DayOfWeekSerializers.kt index f03c2d427..ade979594 100644 --- a/core/common/src/serializers/DayOfWeekSerializers.kt +++ b/core/common/src/serializers/DayOfWeekSerializers.kt @@ -11,6 +11,11 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.internal.* +/** + * A serializer for [DayOfWeek] that represents the values as strings. + * + * JSON example: `"MONDAY"` + */ @Suppress("INVISIBLE_MEMBER") public object DayOfWeekSerializer: KSerializer { private val impl = EnumSerializer("Month", DayOfWeek.values()) diff --git a/core/common/src/serializers/InstantSerializers.kt b/core/common/src/serializers/InstantSerializers.kt index 0ebeb8213..d3e81dc55 100644 --- a/core/common/src/serializers/InstantSerializers.kt +++ b/core/common/src/serializers/InstantSerializers.kt @@ -10,6 +10,14 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +/** + * A serializer for [Instant] that uses the ISO-8601 representation. + * + * JSON example: `"2020-12-09T09:16:56.000124Z"` + * + * @see Instant.toString + * @see Instant.parse + */ public object InstantIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = @@ -24,6 +32,11 @@ public object InstantIso8601Serializer: KSerializer { } +/** + * A serializer for [Instant] that represents an `Instant` value as second and nanosecond components of the Unix time. + * + * JSON example: `{"epochSeconds":1607505416,"nanosecondsOfSecond":124000}` + */ public object InstantComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = diff --git a/core/common/src/serializers/LocalDateSerializers.kt b/core/common/src/serializers/LocalDateSerializers.kt index 21fd68bce..49b10f801 100644 --- a/core/common/src/serializers/LocalDateSerializers.kt +++ b/core/common/src/serializers/LocalDateSerializers.kt @@ -10,6 +10,14 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +/** + * A serializer for [LocalDate] that uses the ISO-8601 representation. + * + * JSON example: `"2020-01-01"` + * + * @see LocalDate.parse + * @see LocalDate.toString + */ public object LocalDateIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = @@ -24,6 +32,11 @@ public object LocalDateIso8601Serializer: KSerializer { } +/** + * A serializer for [LocalDate] that represents a value as its components. + * + * JSON example: `{"year":2020,"month":12,"day":9}` + */ public object LocalDateComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = diff --git a/core/common/src/serializers/LocalDateTimeSerializers.kt b/core/common/src/serializers/LocalDateTimeSerializers.kt index 7f7f76745..525e5c714 100644 --- a/core/common/src/serializers/LocalDateTimeSerializers.kt +++ b/core/common/src/serializers/LocalDateTimeSerializers.kt @@ -5,11 +5,19 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.* import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +/** + * A serializer for [LocalDateTime] that uses the ISO-8601 representation. + * + * JSON example: `"2007-12-31T23:59:01"` + * + * @see LocalDateTime.parse + * @see LocalDateTime.toString + */ public object LocalDateTimeIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = @@ -24,6 +32,11 @@ public object LocalDateTimeIso8601Serializer: KSerializer { } +/** + * A serializer for [LocalDateTime] that represents a value as its components. + * + * JSON example: `{"year":2008,"month":7,"day":5,"hour":2,"minute":1}` + */ public object LocalDateTimeComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = diff --git a/core/common/src/serializers/MonthSerializers.kt b/core/common/src/serializers/MonthSerializers.kt index d96ab158a..aea235cb1 100644 --- a/core/common/src/serializers/MonthSerializers.kt +++ b/core/common/src/serializers/MonthSerializers.kt @@ -5,12 +5,17 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.Month +import kotlinx.datetime.* import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.internal.* +/** + * A serializer for [Month] that represents the month value as a string. + * + * JSON example: `"JANUARY"` + */ @Suppress("INVISIBLE_MEMBER") public object MonthSerializer: KSerializer { private val impl = EnumSerializer("Month", Month.values()) diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt index b0a393802..a4961ea34 100644 --- a/core/common/src/serializers/TimeZoneSerializers.kt +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -12,6 +12,11 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* +/** + * A serializer for [TimeZone] that represents the time zone as its identifier. + * + * JSON example: `"Europe/Berlin"` + */ public object TimeZoneSerializer: KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TimeZone", PrimitiveKind.STRING) @@ -24,6 +29,11 @@ public object TimeZoneSerializer: KSerializer { } +/** + * A serializer for [FixedOffsetTimeZone] that represents the time zone as its identifier. + * + * JSON example: `"+02:00"` + */ public object FixedOffsetTimeZoneSerializer: KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("FixedOffsetTimeZone", PrimitiveKind.STRING) @@ -43,6 +53,14 @@ public object FixedOffsetTimeZoneSerializer: KSerializer { } +/** + * A serializer for [UtcOffset] that uses the ISO-8601 representation. + * + * JSON example: `"+02:00"` + * + * @see UtcOffset.parse + * @see UtcOffset.toString + */ public object UtcOffsetSerializer: KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UtcOffset", PrimitiveKind.STRING) diff --git a/core/common/test/InstantTest.kt b/core/common/test/InstantTest.kt index 96d81cdf7..05d7bbc32 100644 --- a/core/common/test/InstantTest.kt +++ b/core/common/test/InstantTest.kt @@ -83,6 +83,7 @@ class InstantTest { assertEquals(seconds.toLong() * 1000 + nanos / 1000000, instant.toEpochMilliseconds()) } + // TODO: assertInvalidFormat { Instant.parse("1970-01-01T23:59:60Z")} // fails on Native assertInvalidFormat { Instant.parse("x") } assertInvalidFormat { Instant.parse("12020-12-31T23:59:59.000000000Z") } // this string represents an Instant that is currently larger than Instant.MAX any of the implementations: diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 1280487f1..02039ff11 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -91,7 +91,7 @@ private val instantParser: Parser val (days, hours, min, seconds) = if (hoursVal == 24 && minutesVal == 0 && secondsVal == 0 && nano == 0) { listOf(1, 0, 0, 0) } else if (hoursVal == 23 && minutesVal == 59 && secondsVal == 60) { - // parsed a leap second, but it seems it isn't used + // TODO: throw an error on leap seconds to match what the other platforms do listOf(0, 23, 59, 59) } else { listOf(0, hoursVal, minutesVal, secondsVal)