From b6a633879d7ef17ff6cd7c3f62e92272f6b1d169 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 12 Mar 2024 15:22:04 +0100 Subject: [PATCH 01/35] Improve the KDoc Fixes #347 --- core/common/src/Clock.kt | 21 +- core/common/src/DateTimePeriod.kt | 57 +++++- core/common/src/DateTimeUnit.kt | 49 ++++- core/common/src/DayOfWeek.kt | 3 + core/common/src/Exceptions.kt | 4 +- core/common/src/Instant.kt | 180 ++++++++++++++++-- core/common/src/LocalDate.kt | 45 ++++- core/common/src/LocalDateTime.kt | 98 ++++++++-- core/common/src/LocalTime.kt | 113 ++++++++++- core/common/src/Month.kt | 6 +- core/common/src/TimeZone.kt | 102 ++++++++-- core/common/src/UtcOffset.kt | 41 +++- .../src/serializers/TimeZoneSerializers.kt | 2 +- core/commonJs/src/TimeZone.kt | 4 +- core/commonJs/src/UtcOffset.kt | 3 +- core/jvm/src/TimeZoneJvm.kt | 4 +- core/jvm/src/UtcOffsetJvm.kt | 2 +- core/native/src/TimeZone.kt | 4 +- core/native/src/UtcOffset.kt | 2 +- 19 files changed, 644 insertions(+), 96 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 7849d6700..9e1fd14d0 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -15,11 +15,26 @@ import kotlin.time.* public interface Clock { /** * Returns the [Instant] corresponding to the current time, according to this clock. + * + * It is not guaranteed that calling [now] later will return a larger [Instant]. + * In particular, for [System], violations of this are completely expected and must be taken into account. + * See the documentation of [System] for details. */ public fun now(): Instant /** - * The [Clock] instance that queries the operating system as its source of knowledge of time. + * The [Clock] instance that queries the operating system as its source of time knowledge. + * + * Successive calls to [now] will not necessarily return increasing [Instant] values, and when they do, + * these increases will not necessarily correspond to the elapsed time. + * + * For example, when using [Clock.System], the following could happen: + * - [now] returns `2023-01-02T22:35:01Z`; + * - The system queries the Internet and recognizes that its clock needs adjusting; + * - [now] returns `2023-01-02T22:32:05Z`. + * + * When predictable intervals between successive measurements are needed, consider using + * [TimeSource.Monotonic]. */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() @@ -38,6 +53,10 @@ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = /** * Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark. + * + * **Pitfall**: using this function with [Clock.System] is error-prone, + * because [Clock.System] is not well suited for measuring time intervals. + * Please only use this conversion function on the [Clock] instances that are fully controlled programmatically. */ @ExperimentalTime public fun Clock.asTimeSource(): TimeSource.WithComparableMarks = object : TimeSource.WithComparableMarks { diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index bb17289f3..654c4efe5 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -8,6 +8,8 @@ package kotlinx.datetime import kotlinx.datetime.internal.* import kotlinx.datetime.serializers.DatePeriodIso8601Serializer import kotlinx.datetime.serializers.DateTimePeriodIso8601Serializer +import kotlinx.datetime.serializers.DatePeriodComponentSerializer +import kotlinx.datetime.serializers.DateTimePeriodComponentSerializer import kotlin.math.* import kotlin.time.Duration import kotlinx.serialization.Serializable @@ -19,10 +21,29 @@ import kotlinx.serialization.Serializable * * 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. + * ### Interaction with other entities + * + * [DateTimePeriod] can be returned from [Instant.periodUntil], representing the difference between two instants. + * Conversely, there is an [Instant.plus] overload that accepts a [DateTimePeriod] and returns a new instant. + * + * [DatePeriod] is a subtype of [DateTimePeriod] that only stores the date components and has all time components equal + * to zero. + * + * ### Construction, serialization, and deserialization + * + * When a [DateTimePeriod] is constructed in any way, a [DatePeriod] value, which is a subtype of [DateTimePeriod], + * will be returned if all time components happen to be zero. + * + * A `DateTimePeriod` can be constructed using the constructor function with the same name. + * + * [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the + * ISO 8601 extended format (for example, `P1Y2M6DT13H`). + * + * or returned as the result of instant arithmetic operations (see [Instant.periodUntil]). + * + * Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]: + * - [DateTimePeriodIso8601Serializer] for the ISO 8601 format; + * - [DateTimePeriodComponentSerializer] for an object with components. */ @Serializable(with = DateTimePeriodIso8601Serializer::class) // TODO: could be error-prone without explicitly named params @@ -33,6 +54,7 @@ public sealed class DateTimePeriod { * The number of calendar days. * * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details. + * Also, this field does not overflow into months, so values larger than 30 can be present. */ public abstract val days: Int internal abstract val totalNanoseconds: Long @@ -49,6 +71,8 @@ public sealed class DateTimePeriod { /** * The number of whole hours in this period. + * + * This field does not overflow into days, so values larger than 23 can be present. */ public open val hours: Int get() = (totalNanoseconds / 3_600_000_000_000).toInt() @@ -72,7 +96,7 @@ public sealed class DateTimePeriod { totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or days != 0 || totalNanoseconds != 0L) /** - * Converts this period to the ISO-8601 string representation for durations. + * Converts this period to the ISO-8601 string representation for durations, for example, `P2M1DT3H`. * * @see DateTimePeriod.parse */ @@ -304,10 +328,13 @@ public sealed class DateTimePeriod { 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 special case of [DateTimePeriod] that only stores the 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. + * Additionally, [DatePeriod] has its own constructor, the [parse] function that will fail if any of the time components + * are not zero, and [DatePeriodIso8601Serializer] and [DatePeriodComponentSerializer], mirroring those of + * [DateTimePeriod]. * * `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. @@ -317,6 +344,17 @@ public class DatePeriod internal constructor( internal override val totalMonths: Int, override val days: Int, ) : DateTimePeriod() { + /** + * Constructs a new [DatePeriod]. + * + * It is recommended to always explicitly name the arguments when constructing this manually, + * like `DatePeriod(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]. + */ 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. */ @@ -334,7 +372,7 @@ public class DatePeriod internal constructor( public companion object { /** - * Parses the ISO-8601 duration representation as a [DatePeriod]. + * Parses the ISO-8601 duration representation as a [DatePeriod], for example, `P1Y2M30D`. * * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not * zero. @@ -422,6 +460,11 @@ public fun DateTimePeriod( * * If the duration value is too big to be represented as a [Long] number of nanoseconds, * the result will be [Long.MAX_VALUE] nanoseconds. + * + * **Pitfall**: a [DateTimePeriod] obtained this way will always have zero date components. + * The reason is that even a [Duration] obtained via [Duration.Companion.days] just means a multiple of 24 hours, + * whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours. + * See [DateTimeUnit.DayBased] for details. */ // TODO: maybe it's more consistent to throw here on overflow? public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds) diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index a219cc7ae..0cfe76f9f 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -14,6 +14,28 @@ import kotlin.time.Duration.Companion.nanoseconds /** * A unit for measuring time. * + * This class is used to express arithmetic operations like addition and subtraction on date-time values: + * for example, adding 10 days to a date-time value, or subtracting 5 hours from a date-time value. + * + * ### Interaction with other entities + * + * Any [DateTimeUnit] can be used with [Instant.plus], [Instant.minus] to + * find an instant that is some number of units away from the given instant. + * Also, [Instant.until] can be used to find the number of the given units between two instants. + * + * [DateTimeUnit.TimeBased] can be used in the [Instant] operations without specifying the time zone, because + * [DateTimeUnit.TimeBased] is defined in terms of passage of real time, and is independent of the time zone. + * Note that a calendar day is not considered identical to 24 hours, so using it does require specifying the time zone. + * See [DateTimeUnit.DayBased] for a discussion. + * + * [DateTimeUnit.DateBased] units can be used in the [LocalDate] operations: [LocalDate.plus], [LocalDate.minus], and + * [LocalDate.until]. + * + * Arithmetic operations on [LocalDateTime] are not provided. + * Please see the [LocalDateTime] documentation for a discussion. + * + * ### Construction, serialization, and deserialization + * * See the predefined constants for time units, like [DateTimeUnit.NANOSECOND], [DateTimeUnit.DAY], * [DateTimeUnit.MONTH], and others. * @@ -22,19 +44,28 @@ import kotlin.time.Duration.Companion.nanoseconds * - 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. + * Also, [DateTimeUnit] can be serialized and deserialized using `kotlinx.serialization`: + * [DateTimeUnitSerializer], [DateBasedDateTimeUnitSerializer], [DayBasedDateTimeUnitSerializer], + * [MonthBasedDateTimeUnitSerializer], and [TimeBasedDateTimeUnitSerializer] are provided, with varying levels of + * specificity of the type they handle. */ @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. */ + /** + * Produces a date-time unit that is a multiple of this unit times the specified integer [scalar] value. + * + * @throws ArithmeticException if the result overflows. + */ public abstract operator fun times(scalar: Int): DateTimeUnit /** - * A date-time unit that has the precise time duration. + * A [date-time unit][DateTimeUnit] 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. + * + * @see DateTimeUnit for a discussion of date-time units in general. */ @Serializable(with = TimeBasedDateTimeUnitSerializer::class) public class TimeBased( @@ -94,11 +125,13 @@ public sealed class DateTimeUnit { } /** - * A date-time unit equal to some number of days or months. + * A [date-time unit][DateTimeUnit] 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. + * + * @see DateTimeUnit for a discussion of date-time units in general. */ @Serializable(with = DateBasedDateTimeUnitSerializer::class) public sealed class DateBased : DateTimeUnit() { @@ -111,7 +144,7 @@ public sealed class DateTimeUnit { } /** - * A date-time unit equal to some number of calendar days. + * A [date-time unit][DateTimeUnit] 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. * @@ -119,6 +152,8 @@ public sealed class DateTimeUnit { * 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. + * + * @see DateTimeUnit for a discussion of date-time units in general. */ @Serializable(with = DayBasedDateTimeUnitSerializer::class) public class DayBased( @@ -145,9 +180,11 @@ public sealed class DateTimeUnit { } /** - * A date-time unit equal to some number of months. + * A [date-time unit][DateTimeUnit] 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. + * + * @see DateTimeUnit for a discussion of date-time units in general. */ @Serializable(with = MonthBasedDateTimeUnitSerializer::class) public class MonthBased( diff --git a/core/common/src/DayOfWeek.kt b/core/common/src/DayOfWeek.kt index 496d61bef..1492497b1 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -7,6 +7,9 @@ package kotlinx.datetime /** * The enumeration class representing the days of the week. + * + * Usually acquired from [LocalDate.dayOfWeek], but can be constructed using the `DayOfWeek` factory function that + * accepts the ISO 8601 day number. This number can be obtained from the [isoDayNumber] property. */ public expect enum class DayOfWeek { MONDAY, diff --git a/core/common/src/Exceptions.kt b/core/common/src/Exceptions.kt index 419464018..a614c3df4 100644 --- a/core/common/src/Exceptions.kt +++ b/core/common/src/Exceptions.kt @@ -6,7 +6,7 @@ package kotlinx.datetime /** - * Thrown by date-time arithmetic operations if the result can not be computed or represented. + * Thrown by date-time arithmetic operations if the result cannot be computed or represented. */ public class DateTimeArithmeticException: RuntimeException { public constructor(): super() @@ -16,7 +16,7 @@ public class DateTimeArithmeticException: RuntimeException { } /** - * Thrown when attempting to construct a [TimeZone] with an invalid ID. + * Thrown when attempting to construct a [TimeZone] with an invalid ID or unavailable rules. */ public class IllegalTimeZoneException: IllegalArgumentException { public constructor(): super() diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 659fe5b7a..07b2c4e02 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -8,6 +8,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.internal.* import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.datetime.serializers.InstantComponentSerializer import kotlinx.serialization.Serializable import kotlin.time.* @@ -25,12 +26,128 @@ import kotlin.time.* * 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). + * ### Obtaining the current moment + * + * The [Clock] interface is the primary way to obtain the current moment: + * + * ``` + * val clock: Clock = Clock.System + * val instant = clock.now() + * ``` + * + * The [Clock.System] implementation uses the platform-specific system clock to obtain the current moment. + * Note that this clock is not guaranteed to be monotonic, and it may be adjusted by the user or the system at any time, + * so it should not be used for measuring time intervals. + * For measuring time intervals, consider [TimeSource.Monotonic]. + * + * ### Obtaining human-readable representations + * + * #### Date and time + * + * [Instant] is essentially the number of seconds and nanoseconds since a deesignated moment in time, + * stored as something like `1709898983.123456789`. + * [Instant] contains no information about what day or time it is, as this depends on the time zone. + * To obtain this information for a specific time zone, obtain a [LocalDateTime] using [Instant.toLocalDateTime]: + * + * ``` + * val instant = Instant.fromEpochSeconds(1709898983, 123456789) + * instant.toLocalDateTime(TimeZone.of("Europe/Berlin")) // 2024-03-08T12:56:23.123456789 + * instant.toLocalDateTime(TimeZone.UTC) // 2024-03-08T11:56:23.123456789 + * ``` + * + * For values very far in the past or the future, this conversion may fail. + * The specific range of values that can be converted to [LocalDateTime] is platform-specific, but at least + * [DISTANT_PAST], [DISTANT_FUTURE], and all values between them can be converted to [LocalDateTime] without exceptions. + * + * #### Date or time separately + * + * To obtain a [LocalDate] or [LocalTime], first, obtain a [LocalDateTime] and then use its [LocalDateTime.date] + * and [LocalDateTime.time] properties: + * + * ``` + * val instant = Instant.fromEpochSeconds(1709898983, 123456789) + * instant.toLocalDateTime(TimeZone.of("Europe/Berlin")).date // 2024-03-08 + * ``` + * + * ### Arithmetic operations + * + * #### Elapsed-time-based + * + * The [plus] and [minus] operators can be used to add and subtract [Duration]s from an [Instant]: + * + * ``` + * Clock.System.now() + Duration.seconds(5) // 5 seconds from now + * ``` + * + * Durations can also be represented as multiples of some [time-based date-time unit][DateTimeUnit.TimeBased]: + * + * ``` + * Clock.System.now().plus(4, DateTimeUnit.HOUR) // 4 hours from now + * ``` + * + * Also, there is a [minus] operator that returns a [Duration] representing the difference between two instants: + * + * ``` + * val start = Clock.System.now() + * val concertStart = LocalDateTime(2023, 1, 1, 20, 0, 0).toInstant(TimeZone.of("Europe/Berlin")) + * val timeUntilConcert = concertStart - start + * ``` + * + * #### Calendar-based + * + * Since [Instant] represents a point in time, it is always well-defined what the result of arithmetic operations on it + * is, including the cases when a calendar is used. + * This is not the case for [LocalDateTime], where the result of arithmetic operations depends on the time zone. + * See the documentation of [LocalDateTime] for more details. + * + * Adding and subtracting calendar-based units can be done using the [plus] and [minus] operators, + * requiring a [TimeZone]: + * + * ``` + * Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // one day from now in Berlin + * ``` + * + * The difference between [Instant] values in terms of calendar-based units can be obtained using the [periodUntil] + * method: + * + * ``` + * val start = Clock.System.now() + * val concertStart = LocalDateTime(2023, 1, 1, 20, 0, 0).toInstant(TimeZone.of("Europe/Berlin")) + * val timeUntilConcert = start.periodUntil(concertStart, TimeZone.of("Europe/Berlin")) + * // two months, three days, four hours, and five minutes until the concert + * ``` + * + * or [Instant.until] method, as well as [Instant.daysUntil], [Instant.monthsUntil], + * and [Instant.yearsUntil] extension functions: + * + * ``` + * val start = Clock.System.now() + * val concertStart = LocalDateTime(2023, 1, 1, 20, 0, 0).toInstant(TimeZone.of("Europe/Berlin")) + * val timeUntilConcert = start.until(concertStart, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) + * // 63 days until the concert, rounded down + * ``` + * + * ### Construction, serialization, and deserialization + * + * [fromEpochSeconds] can be used to construct an instant from the number of seconds since + * `1970-01-01T00:00:00Z` (the Unix epoch). + * [epochSeconds] and [nanosecondsOfSecond] can be used to obtain the number of seconds and nanoseconds since the epoch. + * + * [fromEpochMilliseconds] allows constructing an instant from the number of milliseconds since the epoch. + * [toEpochMilliseconds] can be used to obtain the number of milliseconds since the epoch. + * Note that [Instant] supports nanosecond precision, so converting to milliseconds is a lossy operation. + * + * [parse] and [toString] methods can be used to obtain a [Instant] from and convert it to a string in the + * ISO 8601 extended format (for example, `2023-01-02T22:35:01+01:00`). + * During parsing, the UTC offset is not returned separately. + * If the UTC offset is important, use [DateTimeComponents] with [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] to + * parse the string instead. + * + * Additionally, there are several `kotlinx-serialization` serializers for [Instant]: + * - [InstantIso8601Serializer] for the ISO 8601 extended format, + * - [InstantComponentSerializer] for an object with components. + * + * @see LocalDateTime for a user-visible representation of moments in time in an unspecified time zone. */ @Serializable(with = InstantIso8601Serializer::class) public expect class Instant : Comparable { @@ -43,7 +160,7 @@ public expect class Instant : Comparable { * * Note that this number doesn't include leap seconds added or removed since the epoch. * - * @see Instant.fromEpochSeconds + * @see fromEpochSeconds */ public val epochSeconds: Long @@ -52,7 +169,7 @@ public expect class Instant : Comparable { * * The value is always positive and lies in the range `0..999_999_999`. * - * @see Instant.fromEpochSeconds + * @see fromEpochSeconds */ public val nanosecondsOfSecond: Int @@ -63,7 +180,7 @@ public expect class Instant : Comparable { * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * - * @see Instant.fromEpochMilliseconds + * @see fromEpochMilliseconds */ public fun toEpochMilliseconds(): Long @@ -74,6 +191,11 @@ public expect class Instant : Comparable { * If the [duration] is negative, the returned instant is earlier than this instant. * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * **Pitfall**: do not use [Duration] values obtained via [Duration.Companion.days], as this is misleading: + * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers + * a day to be 24 hours. + * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. */ public operator fun plus(duration: Duration): Instant @@ -84,6 +206,12 @@ public expect class Instant : Comparable { * If the [duration] is negative, the returned instant is later than this instant. * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * **Pitfall**: do not use [Duration] values obtained via [Duration.Companion.days], as this is misleading: + * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers + * a day to be 24 hours. + * For an explanation of why this is error-prone, see the section about arithmetic operations in the [LocalDateTime] + * documentation. */ public operator fun minus(duration: Duration): Instant @@ -96,26 +224,31 @@ public expect class Instant : Comparable { * * The result is never clamped, but note that for instants that are far apart, * the value returned may represent the duration between them inexactly due to the loss of precision. + * + * Note that sources of [Instant] values (in particular, [Clock]) are not guaranteed to be in sync with each other + * or even monotonic, so the result of this operation may be negative even if the other instant was observed later + * than this one, or vice versa. + * For measuring time intervals, consider using [TimeSource.Monotonic]. */ public operator fun minus(other: Instant): Duration /** * Compares `this` instant with the [other] instant. - * Returns zero if this instant represents 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. */ public override operator fun compareTo(other: Instant): Int /** - * Converts this instant to the ISO-8601 string representation. + * Converts this instant to the ISO-8601 string representation; for example, `2023-01-02T23:40:57.120Z` * * 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 + * @see parse * @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET for a very similar format. The difference is that * [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] will not add trailing zeros for readability to the * fractional part of the second. @@ -131,6 +264,9 @@ public expect class Instant : Comparable { * Returns an [Instant] that is [epochMilliseconds] number of milliseconds from the epoch instant `1970-01-01T00:00:00Z`. * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented. + * + * Note that [Instant] also supports nanosecond precision via [fromEpochSeconds]. * * @see Instant.toEpochMilliseconds */ @@ -141,6 +277,12 @@ public expect class Instant : Comparable { * and the [nanosecondAdjustment] number of nanoseconds from the whole second. * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented. + * + * [fromEpochMilliseconds] is a similar function for when input data only has millisecond precision. + * + * @see Instant.epochSeconds + * @see Instant.nanosecondsOfSecond */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long = 0): Instant @@ -149,6 +291,12 @@ public expect class Instant : Comparable { * and the [nanosecondAdjustment] number of nanoseconds from the whole second. * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * In any case, it is guaranteed that instants between [DISTANT_PAST] and [DISTANT_FUTURE] can be represented. + * + * [fromEpochMilliseconds] is a similar function for when input data only has millisecond precision. + * + * @see Instant.epochSeconds + * @see Instant.nanosecondsOfSecond */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant @@ -180,7 +328,9 @@ 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][Instant.toLocalDateTime] to - * [LocalDateTime] without exceptions on all supported platforms. + * [LocalDateTime] without exceptions in every time zone on all supported platforms. + * + * [isDistantPast] returns true for this value and all earlier ones. */ public val DISTANT_PAST: Instant // -100001-12-31T23:59:59.999999999Z @@ -188,7 +338,9 @@ public expect class Instant : Comparable { * An instant value that is far in the future. * * All instants in the range `DISTANT_PAST..DISTANT_FUTURE` can be [converted][Instant.toLocalDateTime] to - * [LocalDateTime] without exceptions on all supported platforms. + * [LocalDateTime] without exceptions in every time zone on all supported platforms. + * + * [isDistantFuture] returns true for this value and all later ones. */ public val DISTANT_FUTURE: Instant // +100000-01-01T00:00:00Z diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0a411a8e1..98cefa603 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -6,7 +6,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable /** @@ -19,6 +19,31 @@ import kotlinx.serialization.Serializable * 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. + * + * ### Arithmetic operations + * + * Operations with [DateTimeUnit.DateBased] and [DatePeriod] are provided for [LocalDate]: + * - [LocalDate.plus] and [LocalDate.minus] allow expressing concepts like "two months later," + * - [LocalDate.until] and its shortcuts [LocalDate.daysUntil], [LocalDate.monthsUntil], and [LocalDate.yearsUntil] + * can be used to find the number of days, months, or years between two dates, + * - [LocalDate.periodUntil] (and [LocalDate.minus] that accepts a [LocalDate]) + * can be used to find the [DatePeriod] between two dates. + * + * ### Construction, serialization, and deserialization + * + * [LocalDate] can be constructed directly from its components, using the constructor. + * + * [fromEpochDays] can be used to obtain a [LocalDate] from the number of days since the epoch day `1970-01-01`; + * [toEpochDays] is the inverse operation. + * + * [parse] and [toString] methods can be used to obtain a [LocalDate] from and convert it to a string in the + * ISO 8601 extended format (for example, `2023-01-02`). + * + * [parse] and [LocalDate.format] both support custom formats created with [Format] or defined in [Formats]. + * + * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: + * - [LocalDateIso8601Serializer] for the ISO 8601 extended format, + * - [LocalDateComponentSerializer] for an object with components. */ @Serializable(with = LocalDateIso8601Serializer::class) public expect class LocalDate : Comparable { @@ -115,7 +140,7 @@ public expect class LocalDate : Comparable { * The components [monthNumber] and [dayOfMonth] are 1-based. * * The supported ranges of components: - * - [year] the range is platform dependent, but at least is enough to represent dates of all instants between + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [monthNumber] `1..12` * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -129,7 +154,7 @@ public expect class LocalDate : Comparable { * Constructs a [LocalDate] instance from the given date components. * * The supported ranges of components: - * - [year] the range is platform dependent, but at least is enough to represent dates of all instants between + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [month] all values of the [Month] enum * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -142,7 +167,7 @@ public expect class LocalDate : Comparable { /** Returns the year component of the date. */ public val year: Int - /** Returns the number-of-month (1..12) component of the date. */ + /** Returns the number-of-the-month (1..12) component of the date. */ public val monthNumber: Int /** Returns the month ([Month]) component of the date. */ @@ -168,7 +193,7 @@ public expect class LocalDate : Comparable { /** * Compares `this` date with the [other] date. - * Returns zero if this date represent the same day as the other (i.e. equal to other), + * Returns zero if this date represents the same day as the other (i.e., equal to other), * a negative number if this date is earlier than the other, * and a positive number if this date is later than the other. */ @@ -210,13 +235,18 @@ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: * * 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. + * + * **Pitfall**: since [LocalDateTime] is not tied to a particular time zone, the resulting [LocalDateTime] may not + * exist in the implicit time zone. + * For example, `LocalDate(2021, 3, 28).atTime(LocalTime(2, 16, 20))` will successfully create a [LocalDateTime], + * even though in Berlin, times between 2:00 and 3:00 do not exist on March 28, 2021 due to the transition to DST. */ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this, time) /** * Returns a date that is the result of adding components of [DatePeriod] to this date. The components are - * added in the order from the largest units to the smallest, i.e. from years to days. + * added in the order from the largest units to the smallest: first years and months, then days. * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in @@ -226,7 +256,7 @@ public expect operator fun LocalDate.plus(period: DatePeriod): LocalDate /** * Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are - * subtracted in the order from the largest units to the smallest, i.e. from years to days. + * subtracted in the order from the largest units to the smallest: first years and months, then days. * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in @@ -236,6 +266,7 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) { plus(with(period) { DatePeriod(-years, -months, -days) }) } else { + // TODO: calendar operations are non-associative, check if subtracting years and months separately is correct minus(period.years, DateTimeUnit.YEAR).minus(period.months, DateTimeUnit.MONTH) .minus(period.days, DateTimeUnit.DAY) } diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 6fa4e768f..3da318c3a 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -5,24 +5,85 @@ package kotlinx.datetime -import kotlinx.datetime.LocalDate.Companion.parse import kotlinx.datetime.format.* import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.datetime.serializers.LocalDateTimeComponentSerializer 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. + * Instead, its instances can be thought of as clock readings, something that someone could observe in their time zone. * 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. + * this on their clocks at different times, but it is a [LocalDateTime]. * * The main purpose of this class is to provide human-readable representations of [Instant] values, or to transfer them * as data. + * Instances of [LocalDateTime] should not be stored when a specific time zone is known: in this case, it is recommended + * to use [Instant] instead. * - * The arithmetic on [LocalDateTime] values is not provided, since without accounting for the time zone transitions it may give misleading results. + * ### Arithmetic operations + * + * The arithmetic on [LocalDateTime] values is not provided, since without accounting for the time zone transitions it + * may give misleading results. + * + * For example, in Berlin, naively adding one day to `2021-03-28T02:16:20` without accounting for the time zone would + * result in `2021-03-28T02:16:20`. + * However, this local date-time is invalid, because the clocks moved forward from `02:00` to `03:00` on that day. + * This is known as a "time gap", or a "spring forward" transition. + * + * Similarly, the local date-time `2021-10-31T02:16:20` is ambiguous, + * because the clocks moved back from `03:00` to `02:00`. + * This is known as a "time overlap", or a "fall back" transition. + * + * For these reasons, using [LocalDateTime] as an input to arithmetic operations is discouraged. + * + * When only arithmetic on the date component is needed, without touching the time, use [LocalDate] instead, + * as it provides well-defined date arithmetic. + * + * If the time component must be taken into account, [LocalDateTime] + * should be converted to [Instant] using a specific time zone, and the arithmetic on [Instant] should be used. + * + * ``` + * val timeZone = TimeZone.of("Europe/Berlin") + * val localDateTime = LocalDateTime(2021, 3, 27, 2, 16, 20) + * val instant = localDateTime.toInstant(timeZone) + * + * val instantOneDayLater = instant.plus(1, DateTimeUnit.DAY, timeZone) + * val localDateTimeOneDayLater = instantOneDayLater.toLocalDateTime(timeZone) + * // 2021-03-28T03:16:20, as 02:16:20 that day is in a time gap + * + * val instantTwoDaysLater = instant.plus(2, DateTimeUnit.DAY, timeZone) + * val localDateTimeTwoDaysLater = instantTwoDaysLater.toLocalDateTime(timeZone) + * // 2021-03-29T02:16:20 + * ``` + * + * ### Construction, serialization, and deserialization + * + * **Pitfall**: since [LocalDateTime] is always constructed without specifying the time zone, it cannot validate + * whether the given date and time components are valid in the implied time zone. + * For example, `2021-03-28T02:16:20` is invalid in Berlin, as it falls into a time gap, but nothing prevents one + * from constructing such a [LocalDateTime]. + * Before constructing a [LocalDateTime] using any API, please ensure that the result is valid in the implied time zone. + * The recommended pattern is to convert a [LocalDateTime] to [Instant] as soon as possible (see + * [LocalDateTime.toInstant]) and work with [Instant] values instead. + * + * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor. + * Some additional constructors that accept the date's and time's fields directly are provided for convenience. + * + * [parse] and [toString] methods can be used to obtain a [LocalDateTime] from and convert it to a string in the + * ISO 8601 extended format (for example, `2023-01-02T22:35:01`). + * + * [parse] and [LocalDateTime.format] both support custom formats created with [Format] or defined in [Formats]. + * + * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: + * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format, + * - [LocalDateTimeComponentSerializer] for an object with components. + * + * @see LocalDate for only the date part of the date/time value. + * @see LocalTime for only the time part of the date/time value. + * @see Instant for the representation of a specific moment in time independent of a time zone. */ @Serializable(with = LocalDateTimeIso8601Serializer::class) public expect class LocalDateTime : Comparable { @@ -104,7 +165,7 @@ public expect class LocalDateTime : Comparable { * The components [monthNumber] and [dayOfMonth] are 1-based. * * The supported ranges of components: - * - [year] the range is platform dependent, but at least is enough to represent dates of all instants between + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [monthNumber] `1..12` * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -113,8 +174,8 @@ public expect class LocalDateTime : Comparable { * - [second] `0..59` * - [nanosecond] `0..999_999_999` * - * @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, @@ -130,7 +191,7 @@ public expect class LocalDateTime : Comparable { * Constructs a [LocalDateTime] instance from the given date and time components. * * The supported ranges of components: - * - [year] the range is platform dependent, but at least is enough to represent dates of all instants between + * - [year] the range is platform-dependent, but at least is enough to represent dates of all instants between * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] * - [month] all values of the [Month] enum * - [dayOfMonth] `1..31`, the upper bound can be less, depending on the month @@ -139,8 +200,8 @@ public expect class LocalDateTime : Comparable { * - [second] `0..59` * - [nanosecond] `0..999_999_999` * - * @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, @@ -160,7 +221,7 @@ public expect class LocalDateTime : Comparable { /** Returns the year component of the date. */ public val year: Int - /** Returns the number-of-month (1..12) component of the date. */ + /** Returns the number-of-the-month (1..12) component of the date. */ public val monthNumber: Int /** Returns the month ([Month]) component of the date. */ @@ -172,7 +233,7 @@ public expect class LocalDateTime : Comparable { /** Returns the day-of-week component of the date. */ public val dayOfWeek: DayOfWeek - /** Returns the day-of-year component of the date. */ + /** Returns the 1-based day-of-year component of the date. */ public val dayOfYear: Int /** Returns the hour-of-day time component of this date/time value. */ @@ -198,8 +259,17 @@ public expect class LocalDateTime : Comparable { * Returns zero if this value is equal to the other, * 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. + * + * **Pitfall**: comparing [LocalDateTime] values is less robust than comparing [Instant] values. + * Consider the following situation, where a later moment in time corresponds to an earlier [LocalDateTime] value: + * ``` + * val zone = TimeZone.of("Europe/Berlin") + * val ldt1 = Clock.System.now().toLocalDateTime(zone) // 2021-10-31T02:16:20 + * // 45 minutes pass; clocks move back from 03:00 to 02:00 in the meantime + * val ldt2 = Clock.System.now().toLocalDateTime(zone) // 2021-10-31T02:01:20 + * ldt2 > ldt1 // returns `false` + * ``` */ - // 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/LocalTime.kt b/core/common/src/LocalTime.kt index 900bc61a1..e13f48159 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -8,22 +8,60 @@ package kotlinx.datetime import kotlinx.datetime.LocalDate.Companion.parse import kotlinx.datetime.format.* import kotlinx.datetime.serializers.LocalTimeIso8601Serializer +import kotlinx.datetime.serializers.LocalTimeComponentSerializer import kotlinx.serialization.Serializable /** * The time part of [LocalDateTime]. * - * This class represents time-of-day without a referencing a specific date. + * This class represents time-of-day without referencing a specific date. * To reconstruct a full [LocalDateTime], representing civil date and time, [LocalTime] needs to be * combined with [LocalDate] via [LocalDate.atTime] or [LocalTime.atDate]. * * Also, [LocalTime] does not reference a particular time zone. * Therefore, even on the same date, [LocalTime] denotes different moments of time. * For example, `18:43` happens at different moments in Berlin and in Tokyo. + * It may not even exist or be ambiguous on days when clocks are adjusted. * * The arithmetic on [LocalTime] values is not provided, since without accounting for the time zone * transitions it may give misleading results. + * + * ### Arithmetic operations + * + * Arithmetic operations on [LocalTime] are not provided, because they are not well-defined without a date and + * a time zone. + * See [LocalDateTime] for an explanation of why not accounting for time zone transitions may lead to incorrect results. + * To perform arithmetic operations on time values, first, obtain an [Instant]. + * + * ``` + * val time = LocalTime(13, 30) + * val date = Clock.System.todayAt(TimeZone.currentSystemDefault()) + * val instant = time.atDate(date).toInstant(TimeZone.currentSystemDefault()) + * val instantThreeHoursLater = instant.plus(3.hours) + * val timeThreeHoursLater = instantThreeHoursLater.toLocalDateTime(TimeZone.currentSystemDefault()).time + * ``` + * + * Because this pattern is extremely verbose and difficult to get right, it is recommended to work exclusively + * with [Instant] and only obtain a [LocalTime] when it is necessary to display the time to the user. + * + * ### Construction, serialization, and deserialization + * + * [LocalTime] can be constructed directly from its components, using the constructor. + * + * [fromSecondOfDay], [fromMillisecondOfDay], and [fromNanosecondOfDay] can be used to obtain a [LocalTime] from the + * number of seconds, milliseconds, or nanoseconds since the start of the day, assuming there the offset from the UTC + * does not change during the day. + * [toSecondOfDay], [toMillisecondOfDay], and [toNanosecondOfDay] are the inverse operations. + * + * [parse] and [toString] methods can be used to obtain a [LocalTime] from and convert it to a string in the + * ISO 8601 extended format (for example, `23:13:16.153200`). + * + * [parse] and [LocalTime.format] both support custom formats created with [Format] or defined in [Formats]. + * + * Additionally, there are several `kotlinx-serialization` serializers for [LocalTime]: + * - [LocalTimeIso8601Serializer] for the ISO 8601 extended format, + * - [LocalTimeComponentSerializer] for an object with components. */ @Serializable(LocalTimeIso8601Serializer::class) public expect class LocalTime : Comparable { @@ -49,6 +87,11 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [secondOfDay] is outside the `0 until 86400` range, * with 86400 being the number of seconds in a calendar day. * + * It is incorrect to pass the number of seconds since the start of the day to this function. + * The reason is that, due to the daylight-saving-time transitions, the number of seconds since the start + * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. + * Use [Instant] to perform reliable time arithmetic. + * * @see LocalTime.toSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.fromNanosecondOfDay @@ -63,6 +106,11 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [millisecondOfDay] is outside the `0 until 86400 * 1_000` range, * with 86400 being the number of seconds in a calendar day. * + * It is incorrect to pass the number of milliseconds since the start of the day to this function. + * The reason is that, due to the daylight-saving-time transitions, the number of milliseconds since the start + * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. + * Use [Instant] to perform reliable time arithmetic. + * * @see LocalTime.fromSecondOfDay * @see LocalTime.toMillisecondOfDay * @see LocalTime.fromNanosecondOfDay @@ -76,6 +124,11 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [nanosecondOfDay] is outside the `0 until 86400 * 1_000_000_000` range, * with 86400 being the number of seconds in a calendar day. * + * It is incorrect to pass the number of nanoseconds since the start of the day to this function. + * The reason is that, due to the daylight-saving-time transitions, the number of nanoseconds since the start + * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. + * Use [Instant] to perform reliable time arithmetic. + * * @see LocalTime.fromSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.toNanosecondOfDay @@ -142,25 +195,60 @@ public expect class LocalTime : Comparable { */ public constructor(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0) - /** Returns the hour-of-day time component of this time value. */ + /** + * Returns the hour-of-day (0..23) time component of this time value. + */ public val hour: Int - /** Returns the minute-of-hour time component of this time value. */ + /** Returns the minute-of-hour (0..59) time component of this time value. */ public val minute: Int - /** Returns the second-of-minute time component of this time value. */ + /** Returns the second-of-minute (0..59) time component of this time value. */ public val second: Int - /** Returns the nanosecond-of-second time component of this time value. */ + /** Returns the nanosecond-of-second (0..999_999_999) time component of this time value. */ public val nanosecond: Int - /** Returns the time as a second of a day, in `0 until 24 * 60 * 60`. */ + /** + * Returns the time as a second of a day, in `0 until 24 * 60 * 60`. + * + * Note that this is *not* the number of seconds since the start of the day! + * For example, `LocalTime(4, 0).toMillisecondOfDay()` will return `4 * 60 * 60`, the four hours' + * worth of seconds, but because of DST transitions, when clocks show 4:00, in fact, three, four, five, or + * some other number of hours could have passed since the day started. + * Use [Instant] to perform reliable time arithmetic. + * + * @see toMillisecondOfDay + * @see toNanosecondOfDay + */ public fun toSecondOfDay(): Int - /** Returns the time as a millisecond of a day, in `0 until 24 * 60 * 60 * 1_000`. */ + /** + * Returns the time as a millisecond of a day, in `0 until 24 * 60 * 60 * 1_000`. + * + * Note that this is *not* the number of milliseconds since the start of the day! + * For example, `LocalTime(4, 0).toMillisecondOfDay()` will return `4 * 60 * 60 * 1_000`, the four hours' + * worth of milliseconds, but because of DST transitions, when clocks show 4:00, in fact, three, four, five, or + * some other number of hours could have passed since the day started. + * Use [Instant] to perform reliable time arithmetic. + * + * @see toSecondOfDay + * @see toNanosecondOfDay + */ public fun toMillisecondOfDay(): Int - /** Returns the time as a nanosecond of a day, in `0 until 24 * 60 * 60 * 1_000_000_000`. */ + /** + * Returns the time as a nanosecond of a day, in `0 until 24 * 60 * 60 * 1_000_000_000`. + * + * Note that this is *not* the number of nanoseconds since the start of the day! + * For example, `LocalTime(4, 0).toMillisecondOfDay()` will return `4 * 60 * 60 * 1_000_000_000`, the four hours' + * worth of nanoseconds, but because of DST transitions, when clocks show 4:00, in fact, three, four, five, or + * some other number of hours could have passed since the day started. + * Use [Instant] to perform reliable time arithmetic. + * + * @see toMillisecondOfDay + * @see toNanosecondOfDay + */ public fun toNanosecondOfDay(): Long /** @@ -210,18 +298,27 @@ public fun String.toLocalTime(): LocalTime = LocalTime.parse(this) /** * Combines this time's components with the specified date components into a [LocalDateTime] value. + * + * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which + * this method does not accept. */ public fun LocalTime.atDate(year: Int, monthNumber: Int, dayOfMonth: Int = 0): LocalDateTime = LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) /** * Combines this time's components with the specified date components into a [LocalDateTime] value. + * + * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which + * this method does not accept. */ public fun LocalTime.atDate(year: Int, month: Month, dayOfMonth: Int = 0): LocalDateTime = LocalDateTime(year, month, dayOfMonth, hour, minute, second, nanosecond) /** * Combines this time's components with the specified [LocalDate] components into a [LocalDateTime] value. + * + * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which + * this method does not accept. */ public fun LocalTime.atDate(date: LocalDate): LocalDateTime = LocalDateTime(date, this) diff --git a/core/common/src/Month.kt b/core/common/src/Month.kt index 5603dc285..2ad818e9d 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -7,6 +7,10 @@ package kotlinx.datetime /** * The enumeration class representing the 12 months of the year. + * + * Can be acquired from [LocalDate.month] or constructed using the `Month` factory function that accepts + * the month number. + * This number can be obtained from the [number] property. */ public expect enum class Month { /** January, month #01, with 31 days. */ @@ -44,8 +48,6 @@ public expect enum class Month { /** December, month #12, with 31 days. */ DECEMBER; - -// val value: Int // member missing in java.time.Month has to be an extension } /** diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index c0af7d863..46f744ef3 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -14,6 +14,16 @@ 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]. + * + * A time zone needs can be used in [Instant.toLocalDateTime] and [LocalDateTime.toInstant], and also in + * those arithmetic operations on [Instant] that require knowing the calendar. + * + * A [TimeZone] can be constructed using the [TimeZone.of] function, which accepts the string identifier, like + * `"Europe/Berlin"`, `"America/Los_Angeles"`, etc. For a list of such identifiers, see [TimeZone.availableZoneIds]. + * Also, the constant [TimeZone.UTC] is provided for the UTC time zone. + * + * For interaction with `kotlinx-serialization`, [TimeZoneSerializer] is provided that serializes the time zone as its + * identifier. */ @Serializable(with = TimeZoneSerializer::class) public expect open class TimeZone { @@ -24,7 +34,15 @@ public expect open class TimeZone { */ public val id: String - // TODO: Declare and document toString/equals/hashCode + /** + * Equivalent to [id]. + */ + public override fun toString(): String + + /** + * Compares this time zone to the other one. Time zones are equal if their identifier is the same. + */ + public override fun equals(other: Any?): Boolean public companion object { /** @@ -50,6 +68,9 @@ public expect open class TimeZone { * In the IANA Time Zone Database (TZDB) which is used as the default source of time zones, * these ids are usually in the form `area/city`, for example, `Europe/Berlin` or `America/Los_Angeles`. * + * It is guaranteed that passing any value from [availableZoneIds] to this function will return + * a valid time zone. + * * @throws IllegalTimeZoneException if [zoneId] has an invalid format or a time-zone with the name [zoneId] * is not found. */ @@ -64,6 +85,13 @@ public expect open class TimeZone { /** * Return the civil date/time value that this instant has in the time zone provided as an implicit receiver. * + * The function can be used like this: + * ``` + * with(TimeZone.currentSystemDefault()) { + * Clock.System.now().toLocalDateTime() + * } + * ``` + * * Note that while this conversion is unambiguous, the inverse ([LocalDateTime.toInstant]) * is not necessary so. * @@ -76,14 +104,21 @@ public expect open class TimeZone { /** * Returns an instant that corresponds to this civil date/time value in the time zone provided as an implicit receiver. * - * Note that the conversion is not always unambiguous. There can be the following possible situations: - * - There's only one instant that has this date/time value in the time zone. In this case - * the conversion is unambiguous. - * - There's no instant that has this date/time value in the time zone. Such situation appears when - * the time zone experiences a transition from a lesser to a greater offset. In this case the conversion is performed with - * the lesser offset. - * - There are two possible instants that can have this date/time components in the time zone. In this case the earlier - * instant is returned. + * The function can be used like this: + * ``` + * with(TimeZone.currentSystemDefault()) { + * LocalDateTime(2021, 1, 1, 12, 0).toInstant() + * } + * ``` + * + * Note that the conversion is not always well-defined. There can be the following possible situations: + * - There's only one instant that has this date/time value in the [timeZone]. + * In this case, the conversion is unambiguous. + * - There's no instant that has this date/time value in the [timeZone]. + * Such a situation appears when the time zone experiences a transition from a lesser to a greater offset. + * In this case, the conversion is performed with the lesser (earlier) offset, as if the time gap didn't occur yet. + * - There are two possible instants that can have this date/time components in the [timeZone]. + * In this case, the earlier instant is returned. * * @see Instant.toLocalDateTime */ @@ -92,9 +127,26 @@ public expect open class TimeZone { /** * A time zone that is known to always have the same offset from UTC. + * + * [TimeZone.of] will return an instance of this class if the time zone rules are fixed. + * For example: + * ``` + * val zone = TimeZone.of("UTC+3") + * if (zone is FixedOffsetTimeZone) { + * // implement the more straightforward logic... + * } else { + * // ...or handle the general case + * } + * ``` + * + * Time zones that are [FixedOffsetTimeZone] at some point in time can become non-fixed in the future due to + * changes in legislation or other reasons. */ @Serializable(with = FixedOffsetTimeZoneSerializer::class) public expect class FixedOffsetTimeZone : TimeZone { + /** + * Constructs a time zone with the fixed [offset] from UTC. + */ public constructor(offset: UtcOffset) /** @@ -106,12 +158,16 @@ public expect class FixedOffsetTimeZone : TimeZone { public val totalSeconds: Int } -@Deprecated("Use FixedOffsetTimeZone of UtcOffset instead", ReplaceWith("FixedOffsetTimeZone")) +@Deprecated("Use FixedOffsetTimeZone or UtcOffset instead", ReplaceWith("FixedOffsetTimeZone")) public typealias ZoneOffset = FixedOffsetTimeZone /** * Finds the offset from UTC this time zone has at the specified [instant] of physical time. * + * **Pitfall**: the offset returned from this function should typically not be used for datetime arithmetics, + * because the offset can change over time due to daylight-saving-time transitions and other reasons. + * Use [TimeZone] directly with arithmetic operations instead. + * * @see Instant.toLocalDateTime * @see TimeZone.offsetAt */ @@ -132,6 +188,10 @@ public expect fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime /** * Returns a civil date/time value that this instant has in the specified [UTC offset][offset]. * + * **Pitfall**: it is typically more robust to use [TimeZone] directly, because the offset can change over time due to + * daylight-saving-time transitions and other reasons, so [this] instant may actually correspond to a different offset + * in the implied time zone. + * * @see LocalDateTime.toInstant * @see Instant.offsetIn */ @@ -140,6 +200,10 @@ internal expect fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime /** * Finds the offset from UTC the specified [timeZone] has at this instant of physical time. * + * **Pitfall**: the offset returned from this function should typically not be used for datetime arithmetics, + * because the offset can change over time due to daylight-saving-time transitions and other reasons. + * Use [TimeZone] directly with arithmetic operations instead. + * * @see Instant.toLocalDateTime * @see TimeZone.offsetAt */ @@ -149,14 +213,14 @@ public fun Instant.offsetIn(timeZone: TimeZone): UtcOffset = /** * Returns an instant that corresponds to this civil date/time value in the specified [timeZone]. * - * Note that the conversion is not always unambiguous. There can be the following possible situations: - * - There's only one instant that has this date/time value in the [timeZone]. In this case - * the conversion is unambiguous. - * - There's no instant that has this date/time value in the [timeZone]. Such situation appears when - * the time zone experiences a transition from a lesser to a greater offset. In this case the conversion is performed with - * the lesser offset. - * - There are two possible instants that can have this date/time components in the [timeZone]. In this case the earlier - * instant is returned. + * Note that the conversion is not always well-defined. There can be the following possible situations: + * - There's only one instant that has this date/time value in the [timeZone]. + * In this case, the conversion is unambiguous. + * - There's no instant that has this date/time value in the [timeZone]. + * Such a situation appears when the time zone experiences a transition from a lesser to a greater offset. + * In this case, the conversion is performed with the lesser (earlier) offset, as if the time gap didn't occur yet. + * - There are two possible instants that can have this date/time components in the [timeZone]. + * In this case, the earlier instant is returned. * * @see Instant.toLocalDateTime */ @@ -173,7 +237,7 @@ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant * Returns an instant that corresponds to the start of this date in the specified [timeZone]. * * Note that it's not equivalent to `atTime(0, 0).toInstant(timeZone)` - * because a day does not always start at the fixed time 0:00:00. + * because a day does not always start at the fixed time 00:00:00. * For example, if due do daylight saving time, clocks were shifted from 23:30 * of one day directly to 00:30 of the next day, skipping the midnight, then * `atStartOfDayIn` would return the `Instant` corresponding to 00:30, whereas diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 630ee1553..2d5e8d8c9 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -18,6 +18,27 @@ import kotlinx.serialization.Serializable * - `-02`, minus two hours; * - `+03:30`, plus three hours and thirty minutes; * - `+01:23:45`, plus one hour, 23 minutes, and 45 seconds. + * + * **Pitfall**: the offset is not a time zone. + * It does not contain any information about the time zone's rules, such as daylight-saving-time transitions. + * It is just a fixed offset from UTC, something that may be in effect in a given location today but change + * tomorrow. + * Even if the offset is fixed currently, it may change in the future due to political decisions. + * + * See [TimeZone] for a type that represents a time zone. + * + * ### Construction, serialization, and deserialization + * + * To construct a [UtcOffset] value, use the [UtcOffset] constructor function. + * [totalSeconds] returns the number of seconds from UTC. + * There is also a [ZERO] constant for the offset of zero. + * + * [parse] and [toString] methods can be used to obtain a [UtcOffset] from and convert it to a string in the + * ISO 8601 extended format (for example, `+01:30`). + * + * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. + * + * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the [UtcOffsetSerializer]. */ @Serializable(with = UtcOffsetSerializer::class) public expect class UtcOffset { @@ -28,7 +49,10 @@ public expect class UtcOffset { */ public val totalSeconds: Int - // TODO: Declare and document toString/equals/hashCode + /** + * Returns `true` if [other] is a [UtcOffset] with the same [totalSeconds]. + */ + public override fun equals(other: Any?): Boolean public companion object { /** @@ -46,7 +70,7 @@ public expect class UtcOffset { * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [UtcOffset] are * exceeded. */ - public fun parse(input: CharSequence, format: DateTimeFormat = getIsoUtcOffestFormat()): UtcOffset + public fun parse(input: CharSequence, format: DateTimeFormat = getIsoUtcOffsetFormat()): UtcOffset /** * Creates a new format for parsing and formatting [UtcOffset] values. @@ -134,7 +158,7 @@ public expect class UtcOffset { } /** - * Converts this UTC offset to the extended ISO-8601 string representation. + * Converts this UTC offset to the extended ISO-8601 string representation; for example, `+02:30` or `Z`. * * @see Formats.ISO for the format details. * @see parse for the dual operation: obtaining [UtcOffset] from a string. @@ -153,12 +177,14 @@ public fun UtcOffset.format(format: DateTimeFormat): String = format. * Constructs a [UtcOffset] from hours, minutes, and seconds components. * * All components must have the same sign. + * Otherwise, [IllegalArgumentException] will be thrown. * - * The bounds are checked: it is invalid to pass something other than `±[0; 59]` as the number of seconds or minutes. + * The bounds are checked: it is invalid to pass something other than `±[0; 59]` as the number of seconds or minutes; + * [IllegalArgumentException] will be thrown if this rule is violated. * 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. + * for example, `UtcOffset(minutes = 241)` and `UtcOffset(seconds = -3600)` are both valid. * * @throws IllegalArgumentException if a component exceeds its bounds when a higher order component is specified. * @throws IllegalArgumentException if components have different signs. @@ -171,8 +197,11 @@ public fun UtcOffset(): UtcOffset = UtcOffset.ZERO /** * Returns the fixed-offset time zone with the given UTC offset. + * + * **Pitfall**: if the offset is not fixed, the returned time zone will not reflect the changes in the offset. + * Use [TimeZone.of] with a IANA timezone name to obtain a time zone that can handle changes in the offset. */ public fun UtcOffset.asTimeZone(): FixedOffsetTimeZone = FixedOffsetTimeZone(this) // workaround for https://youtrack.jetbrains.com/issue/KT-65484 -internal fun getIsoUtcOffestFormat() = UtcOffset.Formats.ISO +internal fun getIsoUtcOffsetFormat() = UtcOffset.Formats.ISO diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt index ad41ba1b4..6561928f2 100644 --- a/core/common/src/serializers/TimeZoneSerializers.kt +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -54,7 +54,7 @@ public object FixedOffsetTimeZoneSerializer: KSerializer { } /** - * A serializer for [UtcOffset] that uses the ISO-8601 representation. + * A serializer for [UtcOffset] that uses the extended ISO-8601 representation. * * JSON example: `"+02:00"` * diff --git a/core/commonJs/src/TimeZone.kt b/core/commonJs/src/TimeZone.kt index a609c0141..d1fb6e906 100644 --- a/core/commonJs/src/TimeZone.kt +++ b/core/commonJs/src/TimeZone.kt @@ -20,12 +20,12 @@ public actual open class TimeZone internal constructor(internal val zoneId: jtZo public actual fun Instant.toLocalDateTime(): LocalDateTime = toLocalDateTime(this@TimeZone) public actual fun LocalDateTime.toInstant(): Instant = toInstant(this@TimeZone) - override fun equals(other: Any?): Boolean = + actual override fun equals(other: Any?): Boolean = (this === other) || (other is TimeZone && (this.zoneId === other.zoneId || this.zoneId.equals(other.zoneId))) override fun hashCode(): Int = zoneId.hashCode() - override fun toString(): String = zoneId.toString() + actual override fun toString(): String = zoneId.toString() public actual companion object { public actual fun currentSystemDefault(): TimeZone = ofZone(jtZoneId.systemDefault()) diff --git a/core/commonJs/src/UtcOffset.kt b/core/commonJs/src/UtcOffset.kt index 335626be7..65dcf8651 100644 --- a/core/commonJs/src/UtcOffset.kt +++ b/core/commonJs/src/UtcOffset.kt @@ -19,7 +19,8 @@ public actual class UtcOffset internal constructor(internal val zoneOffset: jtZo public actual val totalSeconds: Int get() = zoneOffset.totalSeconds() override fun hashCode(): Int = zoneOffset.hashCode() - override fun equals(other: Any?): Boolean = other is UtcOffset && (this.zoneOffset === other.zoneOffset || this.zoneOffset.equals(other.zoneOffset)) + actual override fun equals(other: Any?): Boolean = + other is UtcOffset && (this.zoneOffset === other.zoneOffset || this.zoneOffset.equals(other.zoneOffset)) actual override fun toString(): String = zoneOffset.toString() public actual companion object { diff --git a/core/jvm/src/TimeZoneJvm.kt b/core/jvm/src/TimeZoneJvm.kt index 6ec4b9c8a..cd90993ef 100644 --- a/core/jvm/src/TimeZoneJvm.kt +++ b/core/jvm/src/TimeZoneJvm.kt @@ -23,12 +23,12 @@ public actual open class TimeZone internal constructor(internal val zoneId: Zone public actual fun Instant.toLocalDateTime(): LocalDateTime = toLocalDateTime(this@TimeZone) public actual fun LocalDateTime.toInstant(): Instant = toInstant(this@TimeZone) - override fun equals(other: Any?): Boolean = + actual override fun equals(other: Any?): Boolean = (this === other) || (other is TimeZone && this.zoneId == other.zoneId) override fun hashCode(): Int = zoneId.hashCode() - override fun toString(): String = zoneId.toString() + actual override fun toString(): String = zoneId.toString() public actual companion object { public actual fun currentSystemDefault(): TimeZone = ofZone(ZoneId.systemDefault()) diff --git a/core/jvm/src/UtcOffsetJvm.kt b/core/jvm/src/UtcOffsetJvm.kt index 6b0b29387..129857d74 100644 --- a/core/jvm/src/UtcOffsetJvm.kt +++ b/core/jvm/src/UtcOffsetJvm.kt @@ -18,7 +18,7 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) { public actual val totalSeconds: Int get() = zoneOffset.totalSeconds override fun hashCode(): Int = zoneOffset.hashCode() - override fun equals(other: Any?): Boolean = other is UtcOffset && this.zoneOffset == other.zoneOffset + actual override fun equals(other: Any?): Boolean = other is UtcOffset && this.zoneOffset == other.zoneOffset actual override fun toString(): String = zoneOffset.toString() public actual companion object { diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index 2a8fc3e2c..70dc0e950 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -98,12 +98,12 @@ public actual open class TimeZone internal constructor() { internal open fun atZone(dateTime: LocalDateTime, preferred: UtcOffset? = null): ZonedDateTime = error("Should be overridden") - override fun equals(other: Any?): Boolean = + actual override fun equals(other: Any?): Boolean = this === other || other is TimeZone && this.id == other.id override fun hashCode(): Int = id.hashCode() - override fun toString(): String = id + actual override fun toString(): String = id } @Serializable(with = FixedOffsetTimeZoneSerializer::class) diff --git a/core/native/src/UtcOffset.kt b/core/native/src/UtcOffset.kt index 852ae62f4..abe8c64da 100644 --- a/core/native/src/UtcOffset.kt +++ b/core/native/src/UtcOffset.kt @@ -15,7 +15,7 @@ import kotlin.math.abs public actual class UtcOffset private constructor(public actual val totalSeconds: Int) { override fun hashCode(): Int = totalSeconds - override fun equals(other: Any?): Boolean = other is UtcOffset && this.totalSeconds == other.totalSeconds + actual override fun equals(other: Any?): Boolean = other is UtcOffset && this.totalSeconds == other.totalSeconds actual override fun toString(): String = format(Formats.ISO) public actual companion object { From 0a7298e461ab06ecb7d0800e1f5ea746d8fb5522 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 18 Mar 2024 13:44:53 +0100 Subject: [PATCH 02/35] Fix typos and small mistakes --- core/common/src/DateTimePeriod.kt | 4 ++-- core/common/src/Instant.kt | 19 +++++++++---------- core/common/src/LocalDateTime.kt | 2 +- core/common/src/TimeZone.kt | 2 +- core/common/src/format/DateTimeComponents.kt | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 654c4efe5..9826e5b98 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -39,7 +39,7 @@ import kotlinx.serialization.Serializable * [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the * ISO 8601 extended format (for example, `P1Y2M6DT13H`). * - * or returned as the result of instant arithmetic operations (see [Instant.periodUntil]). + * `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]). * * Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]: * - [DateTimePeriodIso8601Serializer] for the ISO 8601 format; @@ -54,7 +54,7 @@ public sealed class DateTimePeriod { * The number of calendar days. * * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details. - * Also, this field does not overflow into months, so values larger than 30 can be present. + * Also, this field does not overflow into months, so values larger than 31 can be present. */ public abstract val days: Int internal abstract val totalNanoseconds: Long diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 07b2c4e02..552166afa 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -38,16 +38,16 @@ import kotlin.time.* * The [Clock.System] implementation uses the platform-specific system clock to obtain the current moment. * Note that this clock is not guaranteed to be monotonic, and it may be adjusted by the user or the system at any time, * so it should not be used for measuring time intervals. - * For measuring time intervals, consider [TimeSource.Monotonic]. + * For that, consider [TimeSource.Monotonic]. * * ### Obtaining human-readable representations * * #### Date and time * - * [Instant] is essentially the number of seconds and nanoseconds since a deesignated moment in time, + * [Instant] is essentially the number of seconds and nanoseconds since a designated moment in time, * stored as something like `1709898983.123456789`. * [Instant] contains no information about what day or time it is, as this depends on the time zone. - * To obtain this information for a specific time zone, obtain a [LocalDateTime] using [Instant.toLocalDateTime]: + * To work with this information for a specific time zone, obtain a [LocalDateTime] using [Instant.toLocalDateTime]: * * ``` * val instant = Instant.fromEpochSeconds(1709898983, 123456789) @@ -57,11 +57,11 @@ import kotlin.time.* * * For values very far in the past or the future, this conversion may fail. * The specific range of values that can be converted to [LocalDateTime] is platform-specific, but at least - * [DISTANT_PAST], [DISTANT_FUTURE], and all values between them can be converted to [LocalDateTime] without exceptions. + * [DISTANT_PAST], [DISTANT_FUTURE], and all values between them can be converted to [LocalDateTime]. * * #### Date or time separately * - * To obtain a [LocalDate] or [LocalTime], first, obtain a [LocalDateTime] and then use its [LocalDateTime.date] + * To obtain a [LocalDate] or [LocalTime], first, obtain a [LocalDateTime], and then use its [LocalDateTime.date] * and [LocalDateTime.time] properties: * * ``` @@ -73,7 +73,7 @@ import kotlin.time.* * * #### Elapsed-time-based * - * The [plus] and [minus] operators can be used to add and subtract [Duration]s from an [Instant]: + * The [plus] and [minus] operators can be used to add [Duration]s to and subtract them from an [Instant]: * * ``` * Clock.System.now() + Duration.seconds(5) // 5 seconds from now @@ -85,7 +85,7 @@ import kotlin.time.* * Clock.System.now().plus(4, DateTimeUnit.HOUR) // 4 hours from now * ``` * - * Also, there is a [minus] operator that returns a [Duration] representing the difference between two instants: + * Also, there is a [minus] operator that returns the [Duration] representing the difference between two instants: * * ``` * val start = Clock.System.now() @@ -137,7 +137,7 @@ import kotlin.time.* * [toEpochMilliseconds] can be used to obtain the number of milliseconds since the epoch. * Note that [Instant] supports nanosecond precision, so converting to milliseconds is a lossy operation. * - * [parse] and [toString] methods can be used to obtain a [Instant] from and convert it to a string in the + * [parse] and [toString] methods can be used to obtain an [Instant] from and convert it to a string in the * ISO 8601 extended format (for example, `2023-01-02T22:35:01+01:00`). * During parsing, the UTC offset is not returned separately. * If the UTC offset is important, use [DateTimeComponents] with [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] to @@ -210,8 +210,7 @@ public expect class Instant : Comparable { * **Pitfall**: do not use [Duration] values obtained via [Duration.Companion.days], as this is misleading: * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers * a day to be 24 hours. - * For an explanation of why this is error-prone, see the section about arithmetic operations in the [LocalDateTime] - * documentation. + * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. */ public operator fun minus(duration: Duration): Instant diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 3da318c3a..656823b58 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -28,7 +28,7 @@ import kotlinx.serialization.Serializable * The arithmetic on [LocalDateTime] values is not provided, since without accounting for the time zone transitions it * may give misleading results. * - * For example, in Berlin, naively adding one day to `2021-03-28T02:16:20` without accounting for the time zone would + * For example, in Berlin, naively adding one day to `2021-03-27T02:16:20` without accounting for the time zone would * result in `2021-03-28T02:16:20`. * However, this local date-time is invalid, because the clocks moved forward from `02:00` to `03:00` on that day. * This is known as a "time gap", or a "spring forward" transition. diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 46f744ef3..e104d704e 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -15,7 +15,7 @@ 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]. * - * A time zone needs can be used in [Instant.toLocalDateTime] and [LocalDateTime.toInstant], and also in + * A time zone can be used in [Instant.toLocalDateTime] and [LocalDateTime.toInstant], and also in * those arithmetic operations on [Instant] that require knowing the calendar. * * A [TimeZone] can be constructed using the [TimeZone.of] function, which accepts the string identifier, like diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 1266c031c..82fc66f16 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -92,7 +92,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * ISO 8601 extended format for dates and times with UTC offset. * * For specifying the time zone offset, the format uses the [UtcOffset.Formats.ISO] format, except that during - * parsing, specifying the minutes is optional. + * parsing, specifying the minutes of the offset is optional. * * This format differs from [LocalTime.Formats.ISO] in its time part in that * specifying the seconds is *not* optional. From e076439431d01a931f24a02373cb870be2841bfc Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 21 Mar 2024 11:17:47 +0100 Subject: [PATCH 03/35] Don't hyphenate ISO 8601 when used as a compound adjective --- README.md | 4 ++-- core/common/src/DateTimePeriod.kt | 8 ++++---- core/common/src/DayOfWeek.kt | 4 ++-- core/common/src/Instant.kt | 2 +- core/common/src/LocalDate.kt | 2 +- core/common/src/LocalTime.kt | 2 +- core/common/src/UtcOffset.kt | 2 +- core/common/src/serializers/DateTimePeriodSerializers.kt | 4 ++-- core/common/src/serializers/InstantSerializers.kt | 2 +- core/common/src/serializers/LocalDateSerializers.kt | 2 +- core/common/src/serializers/LocalDateTimeSerializers.kt | 2 +- core/common/src/serializers/LocalTimeSerializers.kt | 2 +- core/common/src/serializers/TimeZoneSerializers.kt | 2 +- .../common/test/DateTimePeriodSerializationTest.kt | 2 +- serialization/common/test/InstantSerializationTest.kt | 4 ++-- serialization/common/test/LocalDateSerializationTest.kt | 2 +- .../common/test/LocalDateTimeSerializationTest.kt | 2 +- serialization/common/test/LocalTimeSerializationTest.kt | 2 +- 18 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index acbcf65b8..a2f961b2f 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ To convert back, use the companion object function `Instant.fromEpochMillisecond ### Converting instant and local date/time to and from the ISO 8601 string `Instant`, `LocalDateTime`, `LocalDate` and `LocalTime` provide shortcuts for -parsing and formatting them using the extended ISO-8601 format. +parsing and formatting them using the extended ISO 8601 format. The `toString()` function is used to convert the value to a string in that format, and the `parse` function in companion object is used to parse a string representation back. @@ -201,7 +201,7 @@ LocalTime.parse("12:0:03.999") // fails with an IllegalArgumentException ### Working with other string formats -When some data needs to be formatted in some format other than ISO-8601, one +When some data needs to be formatted in some format other than ISO 8601, one can define their own format or use some of the predefined ones: ```kotlin diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 9826e5b98..27950934e 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -96,7 +96,7 @@ public sealed class DateTimePeriod { totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or days != 0 || totalNanoseconds != 0L) /** - * Converts this period to the ISO-8601 string representation for durations, for example, `P2M1DT3H`. + * Converts this period to the ISO 8601 string representation for durations, for example, `P2M1DT3H`. * * @see DateTimePeriod.parse */ @@ -143,12 +143,12 @@ public sealed class DateTimePeriod { public companion object { /** - * Parses a ISO-8601 duration string as a [DateTimePeriod]. + * 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: + * 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 @@ -372,7 +372,7 @@ public class DatePeriod internal constructor( public companion object { /** - * Parses the ISO-8601 duration representation as a [DatePeriod], for example, `P1Y2M30D`. + * Parses the ISO 8601 duration representation as a [DatePeriod], for example, `P1Y2M30D`. * * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not * zero. diff --git a/core/common/src/DayOfWeek.kt b/core/common/src/DayOfWeek.kt index 1492497b1..03a11a541 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -22,12 +22,12 @@ public expect enum class DayOfWeek { } /** - * The ISO-8601 number of the given day of the week. Monday is 1, Sunday is 7. + * 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 /** - * Returns the [DayOfWeek] instance for the given ISO-8601 week day number. Monday is 1, Sunday is 7. + * 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) { "Expected ISO day-of-week number in 1..7, got $isoDayNumber" } diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 552166afa..abcfc7cda 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -240,7 +240,7 @@ public expect class Instant : Comparable { public override operator fun compareTo(other: Instant): Int /** - * Converts this instant to the ISO-8601 string representation; for example, `2023-01-02T23:40:57.120Z` + * Converts this instant to the ISO 8601 string representation; for example, `2023-01-02T23:40:57.120Z` * * 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. diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 98cefa603..f75d64e91 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -200,7 +200,7 @@ public expect class LocalDate : Comparable { public override fun compareTo(other: LocalDate): Int /** - * Converts this date to the extended ISO-8601 string representation. + * Converts this date to the extended ISO 8601 string representation. * * @see Formats.ISO for the format details. * @see parse for the dual operation: obtaining [LocalDate] from a string. diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index e13f48159..21da6bc3d 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -264,7 +264,7 @@ public expect class LocalTime : Comparable { public override operator fun compareTo(other: LocalTime): Int /** - * Converts this time value to the extended ISO-8601 string representation. + * Converts this time value to the extended ISO 8601 string representation. * * For readability, if the time represents a round minute (without seconds or fractional seconds), * the string representation will not include seconds. Also, fractions of seconds will add trailing zeros to diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 2d5e8d8c9..57c5b112b 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -158,7 +158,7 @@ public expect class UtcOffset { } /** - * Converts this UTC offset to the extended ISO-8601 string representation; for example, `+02:30` or `Z`. + * Converts this UTC offset to the extended ISO 8601 string representation; for example, `+02:30` or `Z`. * * @see Formats.ISO for the format details. * @see parse for the dual operation: obtaining [UtcOffset] from a string. diff --git a/core/common/src/serializers/DateTimePeriodSerializers.kt b/core/common/src/serializers/DateTimePeriodSerializers.kt index 3fd8e50fe..e5d904fc3 100644 --- a/core/common/src/serializers/DateTimePeriodSerializers.kt +++ b/core/common/src/serializers/DateTimePeriodSerializers.kt @@ -71,7 +71,7 @@ public object DateTimePeriodComponentSerializer: KSerializer { } /** - * A serializer for [DateTimePeriod] that represents it as an ISO-8601 duration string. + * A serializer for [DateTimePeriod] that represents it as an ISO 8601 duration string. * * JSON example: `"P1DT-1H"` * @@ -154,7 +154,7 @@ public object DatePeriodComponentSerializer: KSerializer { } /** - * A serializer for [DatePeriod] that represents it as an ISO-8601 duration string. + * 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. * diff --git a/core/common/src/serializers/InstantSerializers.kt b/core/common/src/serializers/InstantSerializers.kt index 6c0c07678..c64bdf475 100644 --- a/core/common/src/serializers/InstantSerializers.kt +++ b/core/common/src/serializers/InstantSerializers.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* /** - * A serializer for [Instant] that uses the ISO-8601 representation. + * A serializer for [Instant] that uses the ISO 8601 representation. * * JSON example: `"2020-12-09T09:16:56.000124Z"` * diff --git a/core/common/src/serializers/LocalDateSerializers.kt b/core/common/src/serializers/LocalDateSerializers.kt index 6d9f328ff..e1c1c5e96 100644 --- a/core/common/src/serializers/LocalDateSerializers.kt +++ b/core/common/src/serializers/LocalDateSerializers.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* /** - * A serializer for [LocalDate] that uses the ISO-8601 representation. + * A serializer for [LocalDate] that uses the ISO 8601 representation. * * JSON example: `"2020-01-01"` * diff --git a/core/common/src/serializers/LocalDateTimeSerializers.kt b/core/common/src/serializers/LocalDateTimeSerializers.kt index d22d2243d..73f291155 100644 --- a/core/common/src/serializers/LocalDateTimeSerializers.kt +++ b/core/common/src/serializers/LocalDateTimeSerializers.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* /** - * A serializer for [LocalDateTime] that uses the ISO-8601 representation. + * A serializer for [LocalDateTime] that uses the ISO 8601 representation. * * JSON example: `"2007-12-31T23:59:01"` * diff --git a/core/common/src/serializers/LocalTimeSerializers.kt b/core/common/src/serializers/LocalTimeSerializers.kt index 0becdc93a..b8c1c0ebd 100644 --- a/core/common/src/serializers/LocalTimeSerializers.kt +++ b/core/common/src/serializers/LocalTimeSerializers.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* /** - * A serializer for [LocalTime] that uses the ISO-8601 representation. + * A serializer for [LocalTime] that uses the ISO 8601 representation. * * JSON example: `"12:01:03.999"` * diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt index 6561928f2..2d6d1c38a 100644 --- a/core/common/src/serializers/TimeZoneSerializers.kt +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -54,7 +54,7 @@ public object FixedOffsetTimeZoneSerializer: KSerializer { } /** - * A serializer for [UtcOffset] that uses the extended ISO-8601 representation. + * A serializer for [UtcOffset] that uses the extended ISO 8601 representation. * * JSON example: `"+02:00"` * diff --git a/serialization/common/test/DateTimePeriodSerializationTest.kt b/serialization/common/test/DateTimePeriodSerializationTest.kt index b70407e71..a8cb1a6c5 100644 --- a/serialization/common/test/DateTimePeriodSerializationTest.kt +++ b/serialization/common/test/DateTimePeriodSerializationTest.kt @@ -117,7 +117,7 @@ class DateTimePeriodSerializationTest { @Test fun testDefaultSerializers() { - // Check that they behave the same as the ISO-8601 serializers + // Check that they behave the same as the ISO 8601 serializers dateTimePeriodIso8601Serialization(Json.serializersModule.serializer()) datePeriodIso8601Serialization(Json.serializersModule.serializer(), Json.serializersModule.serializer()) } diff --git a/serialization/common/test/InstantSerializationTest.kt b/serialization/common/test/InstantSerializationTest.kt index ffb92cd8b..dea5c2dbd 100644 --- a/serialization/common/test/InstantSerializationTest.kt +++ b/serialization/common/test/InstantSerializationTest.kt @@ -63,7 +63,7 @@ class InstantSerializationTest { @Test fun testDefaultSerializers() { - // should be the same as the ISO-8601 + // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } -} \ No newline at end of file +} diff --git a/serialization/common/test/LocalDateSerializationTest.kt b/serialization/common/test/LocalDateSerializationTest.kt index 1dd953352..91ed38277 100644 --- a/serialization/common/test/LocalDateSerializationTest.kt +++ b/serialization/common/test/LocalDateSerializationTest.kt @@ -66,7 +66,7 @@ class LocalDateSerializationTest { @Test fun testDefaultSerializers() { - // should be the same as the ISO-8601 + // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } diff --git a/serialization/common/test/LocalDateTimeSerializationTest.kt b/serialization/common/test/LocalDateTimeSerializationTest.kt index f01254d8a..c01e647c0 100644 --- a/serialization/common/test/LocalDateTimeSerializationTest.kt +++ b/serialization/common/test/LocalDateTimeSerializationTest.kt @@ -79,7 +79,7 @@ class LocalDateTimeSerializationTest { @Test fun testDefaultSerializers() { - // should be the same as the ISO-8601 + // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } } diff --git a/serialization/common/test/LocalTimeSerializationTest.kt b/serialization/common/test/LocalTimeSerializationTest.kt index 75d5f4305..5df81f542 100644 --- a/serialization/common/test/LocalTimeSerializationTest.kt +++ b/serialization/common/test/LocalTimeSerializationTest.kt @@ -69,7 +69,7 @@ class LocalTimeSerializationTest { @Test fun testDefaultSerializers() { - // should be the same as the ISO-8601 + // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } } From ca9c73971230f4e53d753058015825a4fc118b66 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 21 Mar 2024 15:19:42 +0100 Subject: [PATCH 04/35] Implement the first pack of fixes after the review --- core/common/src/Clock.kt | 19 +++++- core/common/src/DateTimePeriod.kt | 77 ++++++++++++++++++++++--- core/common/src/Instant.kt | 22 ++++++- core/common/test/ClockTimeSourceTest.kt | 2 +- 4 files changed, 108 insertions(+), 12 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 9e1fd14d0..40940bb7b 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -11,6 +11,11 @@ import kotlin.time.* * A source of [Instant] values. * * See [Clock.System][Clock.System] for the clock instance that queries the operating system. + * + * It is recommended not to use [Clock.System] directly in the implementation; instead, one could pass a + * [Clock] explicitly to the functions or classes that need it. + * This way, tests can be written deterministically by providing custom [Clock] implementations + * to the system under test. */ public interface Clock { /** @@ -23,7 +28,7 @@ public interface Clock { public fun now(): Instant /** - * The [Clock] instance that queries the operating system as its source of time knowledge. + * The [Clock] instance that queries the platform-specific system clock as its source of time knowledge. * * Successive calls to [now] will not necessarily return increasing [Instant] values, and when they do, * these increases will not necessarily correspond to the elapsed time. @@ -35,6 +40,9 @@ public interface Clock { * * When predictable intervals between successive measurements are needed, consider using * [TimeSource.Monotonic]. + * + * For improved testability, one could avoid using [Clock.System] directly in the implementation, + * instead passing a [Clock] explicitly. */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() @@ -47,6 +55,15 @@ public interface Clock { /** * Returns the current date at the given [time zone][timeZone], according to [this Clock][this]. + * + * The time zone is important because the current date is not the same in all time zones at the same time. + * ``` + * val clock = object : Clock { + * override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z") + * } + * val dateInUTC = clock.todayIn(TimeZone.UTC) // 2020-01-01 + * val dateInNewYork = clock.todayIn(TimeZone.of("America/New_York")) // 2019-12-31 + * ``` */ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = now().toLocalDateTime(timeZone).date diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 27950934e..8c2984073 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -17,9 +17,22 @@ import kotlinx.serialization.Serializable /** * A difference between two [instants][Instant], decomposed into date and time components. * - * The date components are: [years], [months], [days]. + * The date components are: [years] ([DateTimeUnit.YEAR]), [months] ([DateTimeUnit.MONTH]), [days] ([DateTimeUnit.DAY]). * - * The time components are: [hours], [minutes], [seconds], [nanoseconds]. + * The time components are: [hours] ([DateTimeUnit.HOUR]), [minutes] ([DateTimeUnit.MINUTE]), + * [seconds] ([DateTimeUnit.SECOND]), [nanoseconds] ([DateTimeUnit.NANOSECOND]). + * + * The time components are not independent and always overflow into one another. + * Likewise, months overflow into years. + * For example, there is no difference between `DateTimePeriod(months = 24, hours = 2, minutes = 63)` and + * `DateTimePeriod(years = 2, hours = 3, minutes = 3)`. + * + * All components can also be negative: for example, `DateTimePeriod(months = -5, days = 6, hours = -3)`. + * Whereas `months = 5` means "5 months after," `months = -5` means "5 months earlier." + * + * Since, semantically, a [DateTimePeriod] is a combination of [DateTimeUnit] values, in cases when the period is a + * fixed time interval (like "yearly" or "quarterly"), please consider using [DateTimeUnit] directly instead: + * for example, instead of `DateTimePeriod(months = 6)`, one could use `DateTimeUnit.MONTH * 6`. * * ### Interaction with other entities * @@ -29,6 +42,10 @@ import kotlinx.serialization.Serializable * [DatePeriod] is a subtype of [DateTimePeriod] that only stores the date components and has all time components equal * to zero. * + * [DateTimePeriod] can be thought of as a combination of a [Duration] and a [DatePeriod], as it contains both the + * time components of [Duration] and the date components of [DatePeriod]. + * [Duration.toDateTimePeriod] can be used to convert a [Duration] to the corresponding [DateTimePeriod]. + * * ### Construction, serialization, and deserialization * * When a [DateTimePeriod] is constructed in any way, a [DatePeriod] value, which is a subtype of [DateTimePeriod], @@ -98,7 +115,17 @@ public sealed class DateTimePeriod { /** * Converts this period to the ISO 8601 string representation for durations, for example, `P2M1DT3H`. * - * @see DateTimePeriod.parse + * Note that the ISO 8601 duration is not the same as [Duration], + * but instead includes the date components, like [DateTimePeriod] does. + * + * Examples of the output: + * - `P2Y4M-1D`: two years, four months, minus one day; + * - `-P2Y4M1D`: minus two years, minus four months, minus one day; + * - `P1DT3H2M4.123456789S`: one day, three hours, two minutes, four seconds, 123456789 nanoseconds; + * - `P1DT-3H-2M-4.123456789S`: one day, minus three hours, minus two minutes, + * minus four seconds, minus 123456789 nanoseconds; + * + * @see DateTimePeriod.parse for the detailed description of the format. */ override fun toString(): String = buildString { val sign = if (allNonpositive()) { append('-'); -1 } else 1 @@ -144,9 +171,11 @@ 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. + * Note that the ISO 8601 duration is not the same as [Duration], + * but instead includes the date components, like [DateTimePeriod] does. * * Examples of durations in the ISO 8601 format: * - `P1Y40D` is one year and 40 days @@ -154,6 +183,26 @@ public sealed class DateTimePeriod { * - `P1DT-1H` is one day minus one hour * - `-PT0.000000001S` is minus one nanosecond * + * The format is defined as follows: + * - First, optionally, a `-` or `+`. + * If `-` is present, the whole period after the `-` is negated: `-P-2M1D` is the same as `P2M-1D`. + * - Then, the letter `P`. + * - Optionally, the number of years, followed by `Y`. + * - Optionally, the number of months, followed by `M`. + * - Optionally, the number of weeks, followed by `W`. + * This is not a part of the ISO 8601 format but an extension. + * - Optionally, the number of days, followed by `D`. + * - The string can end here if there are no more time components. + * If there are time components, the letter `T` is required. + * - Optionally, the number of hours, followed by `H`. + * - Optionally, the number of minutes, followed by `M`. + * - Optionally, the number of seconds, followed by `S`. + * Seconds can optionally have a fractional part with up to nine digits. + * The fractional part is separated with a `.`. + * + * All numbers can be negative, in which case, `-` is prepended to them. + * Otherwise, a number can have `+` prepended to it, which does not have an effect. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are * exceeded. */ @@ -348,10 +397,14 @@ public class DatePeriod internal constructor( * Constructs a new [DatePeriod]. * * It is recommended to always explicitly name the arguments when constructing this manually, - * like `DatePeriod(years = 1, months = 12)`. + * like `DatePeriod(years = 1, months = 12, days = 16)`. * * 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)`. + * `DateTimePeriod(months = 24, days = 41)` becomes `DateTimePeriod(years = 2, days = 41)`. + * + * If only a single component is set and is always non-zero and is semantically a fixed time interval + * (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit.DateBased] instead. + * For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`. * * @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int]. */ @@ -435,10 +488,14 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos * 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)`. + * like `DateTimePeriod(years = 1, months = 12, days = 16)`. * * 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)`. + * `DateTimePeriod(months = 24, days = 41)` becomes `DateTimePeriod(years = 2, days = 41)`. + * + * If only a single component is set and is always non-zero and is semantically a fixed time interval + * (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit] instead. + * For example, instead of `DateTimePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`. * * @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] @@ -465,6 +522,10 @@ public fun DateTimePeriod( * The reason is that even a [Duration] obtained via [Duration.Companion.days] just means a multiple of 24 hours, * whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours. * See [DateTimeUnit.DayBased] for details. + * + * ``` + * 2.days.toDateTimePeriod() // 0 days, 48 hours + * ``` */ // TODO: maybe it's more consistent to throw here on overflow? public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds) diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index abcfc7cda..8a133b9e5 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -38,7 +38,7 @@ import kotlin.time.* * The [Clock.System] implementation uses the platform-specific system clock to obtain the current moment. * Note that this clock is not guaranteed to be monotonic, and it may be adjusted by the user or the system at any time, * so it should not be used for measuring time intervals. - * For that, consider [TimeSource.Monotonic]. + * For that, consider using [TimeSource.Monotonic] and [TimeMark] instead of [Clock.System] and [Instant]. * * ### Obtaining human-readable representations * @@ -104,7 +104,11 @@ import kotlin.time.* * requiring a [TimeZone]: * * ``` - * Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // one day from now in Berlin + * // one day from now in Berlin + * Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) + * + * // a day and two hours short from two months later in Berlin + * Clock.System.now().plus(DateTimePeriod(months = 2, days = -1, hours = -2), TimeZone.of("Europe/Berlin")) * ``` * * The difference between [Instant] values in terms of calendar-based units can be obtained using the [periodUntil] @@ -366,6 +370,13 @@ public fun String.toInstant(): Instant = Instant.parse(this) * Returns an instant that is the result of adding components of [DateTimePeriod] to this instant. The components are * added in the order from the largest units to the smallest, i.e. from years to nanoseconds. * + * - If the [DateTimePeriod] only contains time-based components, please consider adding a [Duration] instead, + * as in `Clock.System.now() + 5.hours`. + * Then, it will not be necessary to pass the [timeZone]. + * - If the [DateTimePeriod] only has a single non-zero component (only the months or only the days), + * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], like in + * `Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. + * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. */ @@ -375,6 +386,13 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst * Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components * are subtracted in the order from the largest units to the smallest, i.e. from years to nanoseconds. * + * - If the [DateTimePeriod] only contains time-based components, please consider subtracting a [Duration] instead, + * as in `Clock.System.now() - 5.hours`. + * Then, it is not necessary to pass the [timeZone]. + * - If the [DateTimePeriod] only has a single non-zero component (only the months or only the days), + * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], as in + * `Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. + * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. */ diff --git a/core/common/test/ClockTimeSourceTest.kt b/core/common/test/ClockTimeSourceTest.kt index ae2261727..561cd2221 100644 --- a/core/common/test/ClockTimeSourceTest.kt +++ b/core/common/test/ClockTimeSourceTest.kt @@ -83,4 +83,4 @@ class ClockTimeSourceTest { assertFailsWith { markFuture - Duration.INFINITE } assertFailsWith { markPast + Duration.INFINITE } } -} \ No newline at end of file +} From c6dd47fcd838d7265b1c3c581e439de293340ea1 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 22 Mar 2024 14:17:38 +0100 Subject: [PATCH 05/35] The second pack of changes --- core/common/src/Clock.kt | 3 + core/common/src/DateTimePeriod.kt | 33 ++++- core/common/src/DateTimeUnit.kt | 31 +++-- core/common/src/DayOfWeek.kt | 2 + core/common/src/Instant.kt | 124 ++++++++++++++++++- core/common/src/LocalDate.kt | 78 +++++++++++- core/common/src/LocalDateTime.kt | 35 +++++- core/common/src/LocalTime.kt | 30 ++++- core/common/src/Month.kt | 2 + core/common/src/TimeZone.kt | 4 + core/common/src/UtcOffset.kt | 26 ++++ core/common/src/format/DateTimeComponents.kt | 11 ++ core/common/src/format/DateTimeFormat.kt | 6 +- 13 files changed, 357 insertions(+), 28 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 40940bb7b..882ac0f88 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -24,6 +24,9 @@ public interface Clock { * It is not guaranteed that calling [now] later will return a larger [Instant]. * In particular, for [System], violations of this are completely expected and must be taken into account. * See the documentation of [System] for details. + * + * Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling + * leap seconds, [now] is not guaranteed to handle leap seconds in any specific way. */ public fun now(): Instant diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 8c2984073..17f9c46fc 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -53,8 +53,18 @@ import kotlinx.serialization.Serializable * * A `DateTimePeriod` can be constructed using the constructor function with the same name. * + * ``` + * val dateTimePeriod = DateTimePeriod(months = 24, days = -3) + * val datePeriod = dateTimePeriod as DatePeriod // the same as DatePeriod(years = 2, days = -3) + * ``` + * * [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the - * ISO 8601 extended format (for example, `P1Y2M6DT13H`). + * ISO 8601 extended format. + * + * ``` + * val dateTimePeriod = DateTimePeriod.parse("P1Y2M6DT13H1S") // 1 year, 2 months, 6 days, 13 hours, 1 second + * val string = dateTimePeriod.toString() // "P1Y2M6DT13H1S" + * ``` * * `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]). * @@ -77,7 +87,7 @@ public sealed class DateTimePeriod { internal abstract val totalNanoseconds: Long /** - * The number of whole years. + * The number of whole years. Can be negative. */ public val years: Int get() = totalMonths / 12 @@ -87,9 +97,9 @@ public sealed class DateTimePeriod { public val months: Int get() = totalMonths % 12 /** - * The number of whole hours in this period. + * The number of whole hours in this period. Can be negative. * - * This field does not overflow into days, so values larger than 23 can be present. + * This field does not overflow into days, so values larger than 23 or smaller than -23 can be present. */ public open val hours: Int get() = (totalNanoseconds / 3_600_000_000_000).toInt() @@ -381,12 +391,23 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * * A `DatePeriod` is automatically returned from all constructor functions for [DateTimePeriod] if it turns out that * the time components are zero. + * + * ``` + * DateTimePeriod.parse("P1Y3D") as DatePeriod // 1 year and 3 days + * ``` + * * Additionally, [DatePeriod] has its own constructor, the [parse] function that will fail if any of the time components * are not zero, and [DatePeriodIso8601Serializer] and [DatePeriodComponentSerializer], mirroring those of * [DateTimePeriod]. * - * `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. + * ``` + * val datePeriod1 = DatePeriod(years = 1, days = 3) + * val string = datePeriod1.toString() // "P1Y3D" + * val datePeriod2 = DatePeriod.parse(string) // 1 year and 3 days + * ``` + * + * `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( diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 0cfe76f9f..576b61971 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -12,10 +12,11 @@ import kotlin.time.* import kotlin.time.Duration.Companion.nanoseconds /** - * A unit for measuring time. + * A unit for measuring time; for example, a second, 20 seconds, a day, a month, or a quarter. * * This class is used to express arithmetic operations like addition and subtraction on date-time values: - * for example, adding 10 days to a date-time value, or subtracting 5 hours from a date-time value. + * for example, adding 10 days to a date-time value, subtracting 5 hours from a date-time value, or finding the + * number of 30-second intervals between two date-time values. * * ### Interaction with other entities * @@ -26,7 +27,7 @@ import kotlin.time.Duration.Companion.nanoseconds * [DateTimeUnit.TimeBased] can be used in the [Instant] operations without specifying the time zone, because * [DateTimeUnit.TimeBased] is defined in terms of passage of real time, and is independent of the time zone. * Note that a calendar day is not considered identical to 24 hours, so using it does require specifying the time zone. - * See [DateTimeUnit.DayBased] for a discussion. + * See [DateTimeUnit.DayBased] for an explanation. * * [DateTimeUnit.DateBased] units can be used in the [LocalDate] operations: [LocalDate.plus], [LocalDate.minus], and * [LocalDate.until]. @@ -34,6 +35,13 @@ import kotlin.time.Duration.Companion.nanoseconds * Arithmetic operations on [LocalDateTime] are not provided. * Please see the [LocalDateTime] documentation for a discussion. * + * [DateTimePeriod] is a combination of all [DateTimeUnit] values, used to express things like + * "two days and three hours." + * [DatePeriod] is specifically a combination of [DateTimeUnit.DateBased] values. + * [DateTimePeriod] is more flexible than [DateTimeUnit] because it can express a combination of different units or + * have the length of zero, but in exchange, the duration of time between two [Instant] or [LocalDate] values can be + * measured in terms of some [DateTimeUnit], but not [DateTimePeriod] or [DatePeriod]. + * * ### Construction, serialization, and deserialization * * See the predefined constants for time units, like [DateTimeUnit.NANOSECOND], [DateTimeUnit.DAY], @@ -42,7 +50,7 @@ import kotlin.time.Duration.Companion.nanoseconds * 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)`. + * `DateTimeUnit.TimeBased(nanoseconds = 10)`. * * Also, [DateTimeUnit] can be serialized and deserialized using `kotlinx.serialization`: * [DateTimeUnitSerializer], [DateBasedDateTimeUnitSerializer], [DayBasedDateTimeUnitSerializer], @@ -55,6 +63,10 @@ public sealed class DateTimeUnit { /** * Produces a date-time unit that is a multiple of this unit times the specified integer [scalar] value. * + * ``` + * val quarter = DateTimeUnit.MONTH * 3 + * ``` + * * @throws ArithmeticException if the result overflows. */ public abstract operator fun times(scalar: Int): DateTimeUnit @@ -65,7 +77,7 @@ public sealed class DateTimeUnit { * Such units are independent of the time zone. * Any such unit can be represented as some fixed number of nanoseconds. * - * @see DateTimeUnit for a discussion of date-time units in general. + * @see DateTimeUnit for a description of date-time units in general. */ @Serializable(with = TimeBasedDateTimeUnitSerializer::class) public class TimeBased( @@ -131,7 +143,7 @@ public sealed class DateTimeUnit { * require a [TimeZone] to find the corresponding [LocalDateTimes][LocalDateTime] first to perform * the operation with the date component of these `LocalDateTime` values. * - * @see DateTimeUnit for a discussion of date-time units in general. + * @see DateTimeUnit for a description of date-time units in general. */ @Serializable(with = DateBasedDateTimeUnitSerializer::class) public sealed class DateBased : DateTimeUnit() { @@ -146,14 +158,15 @@ public sealed class DateTimeUnit { /** * A [date-time unit][DateTimeUnit] 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. + * 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. * - * @see DateTimeUnit for a discussion of date-time units in general. + * @see DateTimeUnit for a description of date-time units in general. */ @Serializable(with = DayBasedDateTimeUnitSerializer::class) public class DayBased( @@ -184,7 +197,7 @@ public sealed class DateTimeUnit { * * Since different months have different number of days, a `MonthBased`-unit cannot be expressed a multiple of some [DayBased]-unit. * - * @see DateTimeUnit for a discussion of date-time units in general. + * @see DateTimeUnit for a description of date-time units in general. */ @Serializable(with = MonthBasedDateTimeUnitSerializer::class) public class MonthBased( diff --git a/core/common/src/DayOfWeek.kt b/core/common/src/DayOfWeek.kt index 03a11a541..d664174fb 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -28,6 +28,8 @@ public val DayOfWeek.isoDayNumber: Int get() = ordinal + 1 /** * Returns the [DayOfWeek] instance for the given ISO 8601 week day number. Monday is 1, Sunday is 7. + * + * @throws IllegalArgumentException if the day number is not in the range 1..7 */ public fun DayOfWeek(isoDayNumber: Int): DayOfWeek { require(isoDayNumber in 1..7) { "Expected ISO day-of-week number in 1..7, got $isoDayNumber" } diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 8a133b9e5..5f35ee402 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -137,16 +137,50 @@ import kotlin.time.* * `1970-01-01T00:00:00Z` (the Unix epoch). * [epochSeconds] and [nanosecondsOfSecond] can be used to obtain the number of seconds and nanoseconds since the epoch. * + * ``` + * val instant = Instant.fromEpochSeconds(1709898983, 123456789) + * instant.epochSeconds // 1709898983 + * instant.nanosecondsOfSecond // 123456789 + * ``` + * * [fromEpochMilliseconds] allows constructing an instant from the number of milliseconds since the epoch. * [toEpochMilliseconds] can be used to obtain the number of milliseconds since the epoch. * Note that [Instant] supports nanosecond precision, so converting to milliseconds is a lossy operation. * + * ``` + * val instant1 = Instant.fromEpochSeconds(1709898983, 123456789) + * instant1.nanosecondsOfSecond // 123456789 + * val milliseconds = instant1.toEpochMilliseconds() // 1709898983123 + * val instant2 = Instant.fromEpochMilliseconds(milliseconds) + * instant2.nanosecondsOfSecond // 123000000 + * ``` + * * [parse] and [toString] methods can be used to obtain an [Instant] from and convert it to a string in the - * ISO 8601 extended format (for example, `2023-01-02T22:35:01+01:00`). + * ISO 8601 extended format. + * + * ``` + * val instant = Instant.parse("2023-01-02T22:35:01+01:00") + * instant.toString() // 2023-01-02T21:35:01Z + * ``` + * * During parsing, the UTC offset is not returned separately. * If the UTC offset is important, use [DateTimeComponents] with [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] to * parse the string instead. * + * [Instant.parse] and [Instant.format] also accept custom formats: + * + * ``` + * val customFormat = DateTimeComponents.Format { + * date(LocalDate.Formats.ISO) + * char(' ') + * time(LocalTime.Formats.ISO) + * char(' ') + * offset(UtcOffset.Formats.ISO) + * } + * val instant = Instant.parse("2023-01-02 22:35:01.14 +01:00", customFormat) + * instant.format(customFormat, offset = UtcOffset(hours = 2)) // 2023-01-02 23:35:01.14 +02:00 + * ``` + * * Additionally, there are several `kotlinx-serialization` serializers for [Instant]: * - [InstantIso8601Serializer] for the ISO 8601 extended format, * - [InstantComponentSerializer] for an object with components. @@ -180,7 +214,7 @@ public expect class Instant : Comparable { /** * Returns the number of milliseconds from the epoch instant `1970-01-01T00:00:00Z`. * - * Any fractional part of millisecond is rounded down to the whole number of milliseconds. + * Any fractional part of millisecond is rounded toward zero to the whole number of milliseconds. * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * @@ -316,6 +350,7 @@ public expect class Instant : Comparable { * 23:59:60 is invalid on UTC-SLS, so parsing it will fail. * * If the format is not specified, [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is used. + * `2023-01-02T23:40:57.120Z` is an example of a string in this format. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded. * @@ -368,7 +403,7 @@ public fun String.toInstant(): Instant = Instant.parse(this) /** * Returns an instant that is the result of adding components of [DateTimePeriod] to this instant. The components are - * added in the order from the largest units to the smallest, i.e. from years to nanoseconds. + * added in the order from the largest units to the smallest, i.e., from years to nanoseconds. * * - If the [DateTimePeriod] only contains time-based components, please consider adding a [Duration] instead, * as in `Clock.System.now() + 5.hours`. @@ -377,6 +412,10 @@ public fun String.toInstant(): Instant = Instant.parse(this) * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], like in * `Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. * + * ``` + * Clock.System.now().plus(DateTimePeriod(months = 1, days = -1), TimeZone.UTC) // one day short from a month later + * ``` + * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. */ @@ -393,6 +432,10 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], as in * `Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. * + * ``` + * Clock.System.now().minus(DateTimePeriod(months = 1, days = -1), TimeZone.UTC) // one day short from a month earlier + * ``` + * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. */ @@ -434,6 +477,12 @@ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * + * ``` + * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) + * val currentMoment = Clock.System.now() + * val daysLived = momentOfBirth.until(currentMoment, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) + * ``` + * * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. */ public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long @@ -447,6 +496,12 @@ public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti * - zero if this instant is equal to the other. * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. + * + * ``` + * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) + * val currentMoment = Clock.System.now() + * val minutesLived = momentOfBirth.until(currentMoment, DateTimeUnit.MINUTE) + * ``` */ public fun Instant.until(other: Instant, unit: DateTimeUnit.TimeBased): Long = try { @@ -561,6 +616,13 @@ public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant = * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * + * Note that the time zone does not need to be passed when the [unit] is a time-based unit. + * It is also not needed when adding date-based units to a [LocalDate]. + * + * ``` + * Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days from now in Berlin + * ``` + * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -572,6 +634,16 @@ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * + * Note that the time zone does not need to be passed when the [unit] is a time-based unit. + * It is also not needed when subtracting date-based units from a [LocalDate]. + * + * ``` + * Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days earlier than now in Berlin + * ``` + * + * If the [value] is positive, the returned instant is earlier than this instant. + * If the [value] is negative, the returned instant is later than this instant. + * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -582,6 +654,10 @@ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZo * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * + * ``` + * Clock.System.now().plus(5, DateTimeUnit.HOUR) // 5 hours from now + * ``` + * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. */ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = @@ -593,6 +669,10 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * + * ``` + * Clock.System.now().minus(5, DateTimeUnit.HOUR) // 5 hours earlier than now + * ``` + * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. */ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = @@ -605,6 +685,13 @@ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * + * Note that the time zone does not need to be passed when the [unit] is a time-based unit. + * It is also not needed when adding date-based units to a [LocalDate]. + * + * ``` + * Clock.System.now().plus(5L, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days from now in Berlin + * ``` + * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -616,6 +703,13 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * + * Note that the time zone does not need to be passed when the [unit] is a time-based unit. + * It is also not needed when subtracting date-based units from a [LocalDate]. + * + * ``` + * Clock.System.now().minus(5L, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days earlier than now in Berlin + * ``` + * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = @@ -631,6 +725,10 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * + * ``` + * Clock.System.now().plus(5L, DateTimeUnit.HOUR) // 5 hours from now + * ``` + * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant @@ -641,6 +739,10 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * + * ``` + * Clock.System.now().minus(5L, DateTimeUnit.HOUR) // 5 hours earlier than now + * ``` + * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. */ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = @@ -657,10 +759,16 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = * The value returned is negative or zero if this instant is earlier than the other, * and positive or zero if this instant is later than the other. * + * ``` + * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) + * val currentMoment = Clock.System.now() + * val daysLived = currentMoment.minus(momentOfBirth, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) + * ``` + * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. - * @see Instant.until + * @see Instant.until for the same operation but with swapped arguments. */ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = other.until(this, unit, timeZone) @@ -671,9 +779,15 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone) * The value returned is negative or zero if this instant is earlier than the other, * and positive or zero if this instant is later than the other. * + * ``` + * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) + * val currentMoment = Clock.System.now() + * val minutesLived = currentMoment.minus(momentOfBirth, DateTimeUnit.MINUTE) + * ``` + * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * - * @see Instant.until + * @see Instant.until for the same operation but with swapped arguments. */ public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long = other.until(this, unit) diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index f75d64e91..87c9eaac6 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -33,14 +33,40 @@ import kotlinx.serialization.Serializable * * [LocalDate] can be constructed directly from its components, using the constructor. * + * ``` + * LocalDate(year = 2023, monthNumber = 1, dayOfMonth = 2) == LocalDate(2023, Month.JANUARY, 2) + * ``` + * * [fromEpochDays] can be used to obtain a [LocalDate] from the number of days since the epoch day `1970-01-01`; * [toEpochDays] is the inverse operation. * + * ``` + * LocalDate.fromEpochDays(0) == LocalDate(1970, Month.JANUARY, 1) + * LocalDate(1970, Month.JANUARY, 31).toEpochDays() == 30 + * ``` + * * [parse] and [toString] methods can be used to obtain a [LocalDate] from and convert it to a string in the - * ISO 8601 extended format (for example, `2023-01-02`). + * ISO 8601 extended format. + * + * ``` + * LocalDate.parse("2023-01-02") == LocalDate(2023, Month.JANUARY, 2) + * LocalDate(2023, Month.JANUARY, 2).toString() == "2023-01-02" + * ``` * * [parse] and [LocalDate.format] both support custom formats created with [Format] or defined in [Formats]. * + * ``` + * val customFormat = LocalDate.Format { + * monthName(MonthNames.ENGLISH_ABBREVIATED) + * char(' ') + * dayOfMonth() + * char(' ') + * year() + * } + * LocalDate.parse("Jan 05 2020", customFormat) == LocalDate(2020, Month.JANUARY, 5) + * LocalDate(2020, Month.JANUARY, 5).format(customFormat) == "Jan 05 2020" + * ``` + * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: * - [LocalDateIso8601Serializer] for the ISO 8601 extended format, * - [LocalDateComponentSerializer] for an object with components. @@ -54,6 +80,7 @@ public expect class LocalDate : Comparable { * Parses a string that represents a date and returns the parsed [LocalDate] value. * * If [format] is not specified, [Formats.ISO] is used. + * `2023-01-02` is an example of a string in this format. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded. * @@ -248,6 +275,13 @@ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this * Returns a date that is the result of adding components of [DatePeriod] to this date. The components are * added in the order from the largest units to the smallest: first years and months, then days. * + * ``` + * LocalDate(2023, Month.JANUARY, 30) + DatePeriod(years = 1, months = 2, days = 2) == LocalDate(2024, Month.APRIL, 1) + * // 2023-01-30 + 1 year = 2024-01-30 + * // 2024-01-30 + 2 months = 2024-03-30 + * // 2024-03-30 + 2 days = 2024-04-01 + * ``` + * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDate]. @@ -258,6 +292,13 @@ public expect operator fun LocalDate.plus(period: DatePeriod): LocalDate * Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are * subtracted in the order from the largest units to the smallest: first years and months, then days. * + * ``` + * LocalDate(2023, Month.JANUARY, 2) - DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2021, Month.OCTOBER, 30) + * // 2023-01-02 - 1 year = 2022-01-02 + * // 2022-01-02 - 2 months = 2021-11-02 + * // 2021-11-02 - 3 days = 2021-10-30 + * ``` + * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDate]. @@ -281,9 +322,13 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = * - negative or zero if this date is later than the other, * - exactly zero if this date is equal to the other. * + * ``` + * LocalDate(2023, Month.JANUARY, 2).periodUntil(LocalDate(2024, Month.APRIL, 1)) == DatePeriod(years = 1, months = 2, days = 30) + * ``` + * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * - * @see LocalDate.minus + * @see LocalDate.minus for the same operation with the order of arguments reversed. */ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod @@ -297,9 +342,13 @@ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod * - positive or zero if this date is later than the other, * - exactly zero if this date is equal to the other. * + * ``` + * LocalDate(2024, Month.APRIL, 1) - LocalDate(2023, Month.JANUARY, 2) == DatePeriod(years = 1, months = 2, days = 30) + * ``` + * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * - * @see LocalDate.periodUntil + * @see LocalDate.periodUntil for the same operation with the order of arguments reversed. */ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.periodUntil(this) @@ -310,6 +359,13 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * - positive or zero if this date is earlier than the other, * - negative or zero if this date is later than the other, * - zero if this date is equal to the other. + * + * The value is rounded toward zero. + * + * ``` + * LocalDate(2023, Month.JANUARY, 2).until(LocalDate(2024, Month.APRIL, 1), DateTimeUnit.MONTH) == 14 + * // one year, two months, and 30 days, rounded toward zero. + * ``` * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result or [Int.MIN_VALUE] for a negative result. * @@ -323,6 +379,8 @@ public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased /** * Returns the number of whole days between two dates. * + * The value is rounded toward zero. + * * 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.until @@ -332,6 +390,8 @@ public expect fun LocalDate.daysUntil(other: LocalDate): Int /** * Returns the number of whole months between two dates. * + * The value is rounded toward zero. + * * 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.until @@ -350,6 +410,8 @@ public expect fun LocalDate.yearsUntil(other: LocalDate): Int /** * Returns a [LocalDate] that is the result of adding one [unit] to this date. * + * The value is rounded toward zero. + * * The returned date is later than this date. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. @@ -360,6 +422,8 @@ public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate /** * Returns a [LocalDate] that is the result of subtracting one [unit] from this date. * + * The value is rounded toward zero. + * * The returned date is earlier than this date. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. @@ -373,6 +437,8 @@ public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, u * If the [value] is positive, the returned date is later than this date. * If the [value] is negative, the returned date is earlier than this date. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -383,6 +449,8 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca * If the [value] is positive, the returned date is earlier than this date. * If the [value] is negative, the returned date is later than this date. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -393,6 +461,8 @@ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): Loc * If the [value] is positive, the returned date is later than this date. * If the [value] is negative, the returned date is earlier than this date. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate @@ -403,6 +473,8 @@ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): Loc * If the [value] is positive, the returned date is earlier than this date. * If the [value] is negative, the returned date is later than this date. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 656823b58..2d937d514 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -69,14 +69,42 @@ import kotlinx.serialization.Serializable * The recommended pattern is to convert a [LocalDateTime] to [Instant] as soon as possible (see * [LocalDateTime.toInstant]) and work with [Instant] values instead. * - * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor. + * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor: + * + * ``` + * val date = LocalDate(2021, 3, 27) + * val time = LocalTime(hour = 2, minute = 16, second = 20) + * LocalDateTime(date, time) + * ``` + * * Some additional constructors that accept the date's and time's fields directly are provided for convenience. * + * ``` + * LocalDateTime(year = 2021, monthNumber = 3, dayOfMonth = 27, hour = 2, minute = 16, second = 20) + * LocalDateTime( + * year = 2021, month = Month.MARCH, dayOfMonth = 27, + * hour = 2, minute = 16, second = 20, nanosecond = 999_999_999 + * ) + * ``` + * * [parse] and [toString] methods can be used to obtain a [LocalDateTime] from and convert it to a string in the * ISO 8601 extended format (for example, `2023-01-02T22:35:01`). * + * ``` + * LocalDateTime.parse("2023-01-02T22:35:01").toString() // 2023-01-02T22:35:01 + * ``` + * * [parse] and [LocalDateTime.format] both support custom formats created with [Format] or defined in [Formats]. * + * ``` + * val customFormat = LocalDateTime.Format { + * date(LocalDate.Formats.ISO) + * char(' ') + * time(LocalTime.Formats.ISO) + * } + * LocalDateTime.parse("2023-01-02 22:35:01", customFormat).format(customFormat) // 2023-01-02 22:35:01 + * ``` + * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format, * - [LocalDateTimeComponentSerializer] for an object with components. @@ -96,6 +124,9 @@ public expect class LocalDateTime : Comparable { * but without any time zone component and returns the parsed [LocalDateTime] value. * * If [format] is not specified, [Formats.ISO] is used. + * `2023-01-02T23:40:57.120` is an example of a string in this format. + * + * See [Formats] and [Format] for predefined and custom formats. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDateTime] are * exceeded. @@ -297,6 +328,8 @@ public expect class LocalDateTime : Comparable { /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. + * + * See [LocalDateTime.Formats] and [LocalDateTime.Format] for predefined and custom formats. */ public fun LocalDateTime.format(format: DateTimeFormat): String = format.format(this) diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 21da6bc3d..030146e3b 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -49,16 +49,41 @@ import kotlinx.serialization.Serializable * * [LocalTime] can be constructed directly from its components, using the constructor. * + * ``` + * val night = LocalTime(hour = 23, minute = 13, second = 16, nanosecond = 153_200_001) + * val evening = LocalTime(hour = 18, minute = 31, second = 54) + * val noon = LocalTime(12, 0) + * ``` + * * [fromSecondOfDay], [fromMillisecondOfDay], and [fromNanosecondOfDay] can be used to obtain a [LocalTime] from the * number of seconds, milliseconds, or nanoseconds since the start of the day, assuming there the offset from the UTC * does not change during the day. * [toSecondOfDay], [toMillisecondOfDay], and [toNanosecondOfDay] are the inverse operations. * + * ``` + * val time = LocalTime.fromSecondOfDay(13 * 60 * 60) // 13:00, even if the clock was adjusted during the day + * time.toSecondOfDay() // 13 * 60 * 60 + * ``` + * * [parse] and [toString] methods can be used to obtain a [LocalTime] from and convert it to a string in the - * ISO 8601 extended format (for example, `23:13:16.153200`). + * ISO 8601 extended format. + * + * ``` + * val time = LocalTime.parse("23:13:16.1532") + * time.toString() // "23:13:16.153200" + * ``` * * [parse] and [LocalTime.format] both support custom formats created with [Format] or defined in [Formats]. * + * ``` + * val customFormat = LocalTime.Format { + * hour(); char(':'); minute(); char(':'); second() + * optional { char(','); secondFraction() } + * } + * val time = LocalTime.parse("23:13:16,1532", customFormat) + * time.format(customFormat) // "23:13:16,1532" + * ``` + * * Additionally, there are several `kotlinx-serialization` serializers for [LocalTime]: * - [LocalTimeIso8601Serializer] for the ISO 8601 extended format, * - [LocalTimeComponentSerializer] for an object with components. @@ -72,6 +97,9 @@ public expect class LocalTime : Comparable { * * Parses a string that represents time-of-day and returns the parsed [LocalTime] value. * + * If [format] is not specified, [Formats.ISO] is used. + * `23:40:57.120` is an example of a string in this format. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalTime] are * exceeded. * diff --git a/core/common/src/Month.kt b/core/common/src/Month.kt index 2ad818e9d..8461f433f 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -57,6 +57,8 @@ public val Month.number: Int get() = ordinal + 1 /** * Returns the [Month] instance for the given month number. January is 1, December is 12. + * + * @throws IllegalArgumentException if the month number is not in the range 1..12 */ public fun Month(number: Int): Month { require(number in 1..12) diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index e104d704e..8cd15df73 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -22,6 +22,10 @@ import kotlinx.serialization.Serializable * `"Europe/Berlin"`, `"America/Los_Angeles"`, etc. For a list of such identifiers, see [TimeZone.availableZoneIds]. * Also, the constant [TimeZone.UTC] is provided for the UTC time zone. * + * ``` + * TimeZone.of("Europe/Berlin") + * ``` + * * For interaction with `kotlinx-serialization`, [TimeZoneSerializer] is provided that serializes the time zone as its * identifier. */ diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 57c5b112b..eb7432657 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -31,13 +31,38 @@ import kotlinx.serialization.Serializable * * To construct a [UtcOffset] value, use the [UtcOffset] constructor function. * [totalSeconds] returns the number of seconds from UTC. + * * There is also a [ZERO] constant for the offset of zero. * + * ``` + * val offset = UtcOffset(hours = 3, minutes = 30) + * println(offset.totalSeconds) // 12600 + * UtcOffset(seconds = 0) == UtcOffset.ZERO + * ``` + * * [parse] and [toString] methods can be used to obtain a [UtcOffset] from and convert it to a string in the * ISO 8601 extended format (for example, `+01:30`). * + * ``` + * val offset = UtcOffset.parse("+01:30") + * offset.toString() // +01:30 + * ``` + * * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. * + * ``` + * val customFormat = UtcOffset.Format { + * optional("GMT") { + * offsetHours(Padding.NONE); char(':'); offsetMinutesOfHour() + * optional { char(':'); offsetSecondsOfMinute() } + * } + * } + * + * val offset = UtcOffset.parse("+01:30:15", customFormat) + * offset.format(customFormat) // +1:30:15 + * offset.format(UtcOffset.Formats.FOUR_DIGITS) // +0130 + * ``` + * * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the [UtcOffsetSerializer]. */ @Serializable(with = UtcOffsetSerializer::class) @@ -66,6 +91,7 @@ public expect class UtcOffset { * Parses a string that represents a UTC offset and returns the parsed [UtcOffset] value. * * If [format] is not specified, [Formats.ISO] is used. + * `Z` or `+01:30` are examples of valid input. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [UtcOffset] are * exceeded. diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 82fc66f16..8a80fa5b6 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -111,6 +111,17 @@ public class DateTimeComponents internal constructor(internal val contents: Date * See ISO-8601-1:2019, 5.4.2.1b), excluding the format without the offset. * * Guaranteed to parse all strings that [Instant.toString] produces. + * + * Typically, to use this format, one can simply call [Instant.toString] and [Instant.parse], + * but accessing it directly allows one to obtain the UTC offset, which is not returned from [Instant.parse], + * or specifying the UTC offset to be formatted. + * + * ``` + * val components = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse("2020-08-30T18:43:00.123456789+03:00") + * val instant = components.toInstantUsingOffset() // 2020-08-30T15:43:00.123456789Z + * val localDateTime = components.toLocalDateTime() // 2020-08-30T18:43:00.123456789 + * val offset = components.toUtcOffset() // UtcOffset(hours = 3) + * ``` */ public val ISO_DATE_TIME_OFFSET: DateTimeFormat = Format { date(ISO_DATE) diff --git a/core/common/src/format/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index f7117cccb..adf1e6c26 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -56,17 +56,17 @@ public sealed interface DateTimeFormat { */ public enum class Padding { /** - * No padding. + * No padding during formatting. Parsing does not require padding, but it is allowed. */ NONE, /** - * Pad with zeros. + * Pad with zeros during formatting. During parsing, the padding is required, or parsing fails. */ ZERO, /** - * Pad with spaces. + * Pad with spaces during formatting. During parsing, the padding is required, or parsing fails. */ SPACE } From 1a4b0f9e0e93929829bac2e910fa865e9113606d Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 3 Apr 2024 16:18:47 +0200 Subject: [PATCH 06/35] Add samples for TimeZone, UtcOffset, and Month --- core/common/src/Month.kt | 5 + core/common/src/TimeZone.kt | 58 ++--- core/common/src/UtcOffset.kt | 57 ++--- core/common/test/samples/MonthSamples.kt | 54 +++++ core/common/test/samples/TimeZoneSamples.kt | 228 +++++++++++++++++++ core/common/test/samples/UtcOffsetSamples.kt | 126 ++++++++++ 6 files changed, 466 insertions(+), 62 deletions(-) create mode 100644 core/common/test/samples/MonthSamples.kt create mode 100644 core/common/test/samples/TimeZoneSamples.kt create mode 100644 core/common/test/samples/UtcOffsetSamples.kt diff --git a/core/common/src/Month.kt b/core/common/src/Month.kt index 8461f433f..ae3b8177a 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -11,6 +11,8 @@ package kotlinx.datetime * Can be acquired from [LocalDate.month] or constructed using the `Month` factory function that accepts * the month number. * This number can be obtained from the [number] property. + * + * @sample kotlinx.datetime.test.samples.MonthSamples.usage */ public expect enum class Month { /** January, month #01, with 31 days. */ @@ -52,6 +54,8 @@ public expect enum class Month { /** * The number of the [Month]. January is 1, December is 12. + * + * @sample kotlinx.datetime.test.samples.MonthSamples.number */ public val Month.number: Int get() = ordinal + 1 @@ -59,6 +63,7 @@ public val Month.number: Int get() = ordinal + 1 * Returns the [Month] instance for the given month number. January is 1, December is 12. * * @throws IllegalArgumentException if the month number is not in the range 1..12 + * @sample kotlinx.datetime.test.samples.MonthSamples.constructorFunction */ public fun Month(number: Int): Month { require(number in 1..12) diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 8cd15df73..1d9ea91ae 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -22,12 +22,10 @@ import kotlinx.serialization.Serializable * `"Europe/Berlin"`, `"America/Los_Angeles"`, etc. For a list of such identifiers, see [TimeZone.availableZoneIds]. * Also, the constant [TimeZone.UTC] is provided for the UTC time zone. * - * ``` - * TimeZone.of("Europe/Berlin") - * ``` - * * For interaction with `kotlinx-serialization`, [TimeZoneSerializer] is provided that serializes the time zone as its * identifier. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.usage */ @Serializable(with = TimeZoneSerializer::class) public expect open class TimeZone { @@ -35,16 +33,22 @@ public expect open class TimeZone { * Returns the identifier string of the time zone. * * This identifier can be used later for finding this time zone with [TimeZone.of] function. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.id */ public val id: String /** * Equivalent to [id]. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.equalsSample */ public override fun toString(): String /** * Compares this time zone to the other one. Time zones are equal if their identifier is the same. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.equalsSample */ public override fun equals(other: Any?): Boolean @@ -53,11 +57,15 @@ public expect open class TimeZone { * Queries the current system time zone. * * If the current system time zone changes, this function can reflect this change on the next invocation. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.currentSystemDefault */ public fun currentSystemDefault(): TimeZone /** * Returns the time zone with the fixed UTC+0 offset. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.utc */ public val UTC: FixedOffsetTimeZone @@ -77,11 +85,14 @@ public expect open class TimeZone { * * @throws IllegalTimeZoneException if [zoneId] has an invalid format or a time-zone with the name [zoneId] * is not found. + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.constructorFunction */ public fun of(zoneId: String): TimeZone /** * Queries the set of identifiers of time zones available in the system. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.availableZoneIds */ public val availableZoneIds: Set } @@ -89,32 +100,19 @@ public expect open class TimeZone { /** * Return the civil date/time value that this instant has in the time zone provided as an implicit receiver. * - * The function can be used like this: - * ``` - * with(TimeZone.currentSystemDefault()) { - * Clock.System.now().toLocalDateTime() - * } - * ``` - * * Note that while this conversion is unambiguous, the inverse ([LocalDateTime.toInstant]) * is not necessary so. * * @see LocalDateTime.toInstant * @see Instant.offsetIn * @throws DateTimeArithmeticException if this value is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.toLocalDateTimeWithTwoReceivers */ public fun Instant.toLocalDateTime(): LocalDateTime /** * Returns an instant that corresponds to this civil date/time value in the time zone provided as an implicit receiver. * - * The function can be used like this: - * ``` - * with(TimeZone.currentSystemDefault()) { - * LocalDateTime(2021, 1, 1, 12, 0).toInstant() - * } - * ``` - * * Note that the conversion is not always well-defined. There can be the following possible situations: * - There's only one instant that has this date/time value in the [timeZone]. * In this case, the conversion is unambiguous. @@ -125,6 +123,7 @@ public expect open class TimeZone { * In this case, the earlier instant is returned. * * @see Instant.toLocalDateTime + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.toInstantWithTwoReceivers */ public fun LocalDateTime.toInstant(): Instant } @@ -133,28 +132,25 @@ public expect open class TimeZone { * A time zone that is known to always have the same offset from UTC. * * [TimeZone.of] will return an instance of this class if the time zone rules are fixed. - * For example: - * ``` - * val zone = TimeZone.of("UTC+3") - * if (zone is FixedOffsetTimeZone) { - * // implement the more straightforward logic... - * } else { - * // ...or handle the general case - * } - * ``` * * Time zones that are [FixedOffsetTimeZone] at some point in time can become non-fixed in the future due to * changes in legislation or other reasons. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.FixedOffsetTimeZoneSamples.casting */ @Serializable(with = FixedOffsetTimeZoneSerializer::class) public expect class FixedOffsetTimeZone : TimeZone { /** * Constructs a time zone with the fixed [offset] from UTC. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.FixedOffsetTimeZoneSamples.constructorFunction */ public constructor(offset: UtcOffset) /** * The constant offset from UTC that this time zone has. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.FixedOffsetTimeZoneSamples.offset */ public val offset: UtcOffset @@ -174,6 +170,7 @@ public typealias ZoneOffset = FixedOffsetTimeZone * * @see Instant.toLocalDateTime * @see TimeZone.offsetAt + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.offsetAt */ public expect fun TimeZone.offsetAt(instant: Instant): UtcOffset @@ -186,6 +183,7 @@ public expect fun TimeZone.offsetAt(instant: Instant): UtcOffset * @see LocalDateTime.toInstant * @see Instant.offsetIn * @throws DateTimeArithmeticException if this value is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.instantToLocalDateTime */ public expect fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime @@ -198,6 +196,7 @@ public expect fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime * * @see LocalDateTime.toInstant * @see Instant.offsetIn + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.instantToLocalDateTimeInOffset */ internal expect fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime @@ -210,6 +209,7 @@ internal expect fun Instant.toLocalDateTime(offset: UtcOffset): LocalDateTime * * @see Instant.toLocalDateTime * @see TimeZone.offsetAt + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.offsetIn */ public fun Instant.offsetIn(timeZone: TimeZone): UtcOffset = timeZone.offsetAt(this) @@ -227,6 +227,7 @@ public fun Instant.offsetIn(timeZone: TimeZone): UtcOffset = * In this case, the earlier instant is returned. * * @see Instant.toLocalDateTime + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.localDateTimeToInstantInZone */ public expect fun LocalDateTime.toInstant(timeZone: TimeZone): Instant @@ -234,6 +235,7 @@ 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.localDateTimeToInstantInOffset */ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant @@ -247,5 +249,7 @@ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant * `atStartOfDayIn` would return the `Instant` corresponding to 00:30, whereas * `atTime(0, 0).toInstant(timeZone)` would return the `Instant` corresponding * to 01:00. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.atStartOfDayIn */ public expect fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index eb7432657..5043e7d4f 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -31,39 +31,22 @@ import kotlinx.serialization.Serializable * * To construct a [UtcOffset] value, use the [UtcOffset] constructor function. * [totalSeconds] returns the number of seconds from UTC. + * See sample 1. * * There is also a [ZERO] constant for the offset of zero. * - * ``` - * val offset = UtcOffset(hours = 3, minutes = 30) - * println(offset.totalSeconds) // 12600 - * UtcOffset(seconds = 0) == UtcOffset.ZERO - * ``` - * * [parse] and [toString] methods can be used to obtain a [UtcOffset] from and convert it to a string in the * ISO 8601 extended format (for example, `+01:30`). - * - * ``` - * val offset = UtcOffset.parse("+01:30") - * offset.toString() // +01:30 - * ``` + * See sample 2. * * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. - * - * ``` - * val customFormat = UtcOffset.Format { - * optional("GMT") { - * offsetHours(Padding.NONE); char(':'); offsetMinutesOfHour() - * optional { char(':'); offsetSecondsOfMinute() } - * } - * } - * - * val offset = UtcOffset.parse("+01:30:15", customFormat) - * offset.format(customFormat) // +1:30:15 - * offset.format(UtcOffset.Formats.FOUR_DIGITS) // +0130 - * ``` + * See sample 3. * * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the [UtcOffsetSerializer]. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.construction + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.customFormat */ @Serializable(with = UtcOffsetSerializer::class) public expect class UtcOffset { @@ -76,6 +59,8 @@ public expect class UtcOffset { /** * Returns `true` if [other] is a [UtcOffset] with the same [totalSeconds]. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.equalsSample */ public override fun equals(other: Any?): Boolean @@ -95,23 +80,13 @@ public expect class UtcOffset { * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [UtcOffset] are * exceeded. + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoUtcOffsetFormat()): UtcOffset /** * Creates a new format for parsing and formatting [UtcOffset] values. * - * Example: - * ``` - * // `GMT` on zero, `+4:30:15`, using a custom format: - * UtcOffset.Format { - * optional("GMT") { - * offsetHours(Padding.NONE); char(':'); offsetMinutesOfHour() - * optional { char(':'); offsetSecondsOfMinute() } - * } - * } - * ``` - * * Since [UtcOffset] values are rarely formatted and parsed on their own, * instances of [DateTimeFormat] obtained here will likely need to be passed to * [DateTimeFormatBuilder.WithUtcOffset.offset] in a format builder for a larger data structure. @@ -119,6 +94,7 @@ public expect class UtcOffset { * There is a collection of predefined formats in [UtcOffset.Formats]. * * @throws IllegalArgumentException if parsing using this format is ambiguous. + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.customFormat */ @Suppress("FunctionName") public fun Format(block: DateTimeFormatBuilder.WithUtcOffset.() -> Unit): DateTimeFormat @@ -147,6 +123,8 @@ public expect class UtcOffset { * - `-02:00`, minus two hours; * - `-17:16` * - `+10:36:30` + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.iso */ public val ISO: DateTimeFormat @@ -164,6 +142,7 @@ public expect class UtcOffset { * - `+103630` * * @see UtcOffset.Formats.FOUR_DIGITS + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.isoBasic */ public val ISO_BASIC: DateTimeFormat @@ -179,6 +158,7 @@ public expect class UtcOffset { * - `+1036` * * @see UtcOffset.Formats.ISO_BASIC + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.fourDigits */ public val FOUR_DIGITS: DateTimeFormat } @@ -189,6 +169,8 @@ public expect class UtcOffset { * @see Formats.ISO for the format details. * @see parse for the dual operation: obtaining [UtcOffset] from a string. * @see UtcOffset.format for formatting using a custom format. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.toStringSample */ public override fun toString(): String } @@ -196,6 +178,8 @@ public expect class UtcOffset { /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.formatting */ public fun UtcOffset.format(format: DateTimeFormat): String = format.format(this) @@ -215,6 +199,7 @@ public fun UtcOffset.format(format: DateTimeFormat): String = format. * @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`. + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.constructorFunction */ public expect fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset @@ -226,6 +211,8 @@ public fun UtcOffset(): UtcOffset = UtcOffset.ZERO * * **Pitfall**: if the offset is not fixed, the returned time zone will not reflect the changes in the offset. * Use [TimeZone.of] with a IANA timezone name to obtain a time zone that can handle changes in the offset. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.asFixedOffsetTimeZone */ public fun UtcOffset.asTimeZone(): FixedOffsetTimeZone = FixedOffsetTimeZone(this) diff --git a/core/common/test/samples/MonthSamples.kt b/core/common/test/samples/MonthSamples.kt new file mode 100644 index 000000000..60708e1f5 --- /dev/null +++ b/core/common/test/samples/MonthSamples.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.test.* + +class MonthSamples { + + @Test + fun usage() { + val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) + when (today.month) { + Month.JANUARY -> check(today.month.number == 1) + Month.FEBRUARY -> check(today.month.number == 2) + Month.MARCH -> check(today.month.number == 3) + Month.APRIL -> check(today.month.number == 4) + Month.MAY -> check(today.month.number == 5) + Month.JUNE -> check(today.month.number == 6) + Month.JULY -> check(today.month.number == 7) + Month.AUGUST -> check(today.month.number == 8) + Month.SEPTEMBER -> check(today.month.number == 9) + Month.OCTOBER -> check(today.month.number == 10) + Month.NOVEMBER -> check(today.month.number == 11) + Month.DECEMBER -> check(today.month.number == 12) + else -> TODO("A new month was added to the calendar?") + } + } + + @Test + fun number() { + check(Month.JANUARY.number == 1) + check(Month.FEBRUARY.number == 2) + // ... + check(Month.DECEMBER.number == 12) + } + + @Test + fun constructorFunction() { + check(Month(1) == Month.JANUARY) + check(Month(2) == Month.FEBRUARY) + // ... + check(Month(12) == Month.DECEMBER) + try { + Month(0) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Expected + } + } +} diff --git a/core/common/test/samples/TimeZoneSamples.kt b/core/common/test/samples/TimeZoneSamples.kt new file mode 100644 index 000000000..4e05757be --- /dev/null +++ b/core/common/test/samples/TimeZoneSamples.kt @@ -0,0 +1,228 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class TimeZoneSamples { + + @Test + fun usage() { + val zone = TimeZone.of("Europe/Berlin") + val localDateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) + val instant = localDateTime.toInstant(zone) + check(instant == Instant.parse("2021-03-28T01:16:20Z")) + val newLocalDateTime = instant.toLocalDateTime(zone) + check(newLocalDateTime == LocalDate(2021, 3, 28).atTime(3, 16, 20)) + } + + @Test + fun id() { + check(TimeZone.of("America/New_York").id == "America/New_York") + } + + @Test + fun toStringSample() { + val zone = TimeZone.of("America/New_York") + check(zone.toString() == "America/New_York") + check(zone.toString() == zone.id) + } + + @Test + fun equalsSample() { + val zone1 = TimeZone.of("America/New_York") + val zone2 = TimeZone.of("America/New_York") + check(zone1 == zone2) // different instances, but the same ID + val zone3 = TimeZone.of("UTC+01:00") + val zone4 = TimeZone.of("GMT+01:00") + check(zone3 != zone4) // the same time zone rules, but different IDs + } + + @Test + fun currentSystemDefault() { + // a fixed-width format for log entries + val logTimeFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + offset(UtcOffset.Formats.FOUR_DIGITS) + } + fun logEntry(message: String, now: Instant = Clock.System.now()): String { + val formattedTime = logTimeFormat.format { + with(TimeZone.currentSystemDefault()) { + setDateTime(now.toLocalDateTime()) + setOffset(offsetAt(now)) + } + } + return "[$formattedTime] $message" + } + logEntry("Starting the application") + } + + @Test + fun utc() { + val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) + val instant = localDateTime.toInstant(TimeZone.UTC) + check(instant == Instant.parse("2023-06-02T12:30:00Z")) + val newInstant = instant.plus(5, DateTimeUnit.DAY, TimeZone.UTC) + check(newInstant == Instant.parse("2023-06-07T12:30:00Z")) + val newLocalDateTime = newInstant.toLocalDateTime(TimeZone.UTC) + check(newLocalDateTime == LocalDate(2023, 6, 7).atTime(12, 30)) + } + + @Test + fun constructorFunction() { + val zone = TimeZone.of("America/New_York") + check(zone.id == "America/New_York") + } + + @Test + fun availableZoneIds() { + for (zoneId in TimeZone.availableZoneIds) { + val zone = TimeZone.of(zoneId) + check(zone.id == zoneId) + } + } + + /** + * @see instantToLocalDateTime + */ + @Test + fun toLocalDateTimeWithTwoReceivers() { + val zone = TimeZone.of("America/New_York") + val instant = Instant.parse("2023-06-02T12:30:00Z") + val localDateTime = with(zone) { + instant.toLocalDateTime() + } + check(localDateTime == LocalDate(2023, 6, 2).atTime(8, 30)) + } + + /** + * @see localDateTimeToInstantInZone + */ + @Test + fun toInstantWithTwoReceivers() { + val zone = TimeZone.of("America/New_York") + val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) + val instant = with(zone) { + localDateTime.toInstant() + } + check(instant == Instant.parse("2023-06-02T16:30:00Z")) + } + + /** + * @see offsetIn + */ + @Test + fun offsetAt() { + val zone = TimeZone.of("America/New_York") + val instant = Instant.parse("2023-06-02T12:30:00Z") + val offset = zone.offsetAt(instant) + check(offset == UtcOffset(hours = -4)) + } + + @Test + fun instantToLocalDateTime() { + val zone = TimeZone.of("America/New_York") + val instant = Instant.parse("2023-06-02T12:30:00Z") + val localDateTime = instant.toLocalDateTime(zone) + check(localDateTime == LocalDate(2023, 6, 2).atTime(8, 30)) + } + + @Test + fun instantToLocalDateTimeInOffset() { + val offset = UtcOffset.parse("+01:30") + val instant = Instant.fromEpochMilliseconds(1685709000000) // "2023-06-02T12:30:00Z" + val localDateTime = instant.toLocalDateTime(offset) + check(localDateTime == LocalDate(2023, 6, 2).atTime(14, 0)) + } + + /** + * @see offsetAt + */ + @Test + fun offsetIn() { + val zone = TimeZone.of("America/New_York") + val instant = Instant.parse("2023-06-02T12:30:00Z") + val offset = instant.offsetIn(zone) + check(offset == UtcOffset(hours = -4)) + } + + /** + * @see toInstantWithTwoReceivers + */ + @Test + fun localDateTimeToInstantInZone() { + val zone = TimeZone.of("America/New_York") + val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) + val instant = localDateTime.toInstant(zone) + check(instant == Instant.parse("2023-06-02T16:30:00Z")) + } + + @Test + fun localDateTimeToInstantInOffset() { + val offset = UtcOffset.parse("+01:30") + val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) + val instant = localDateTime.toInstant(offset) + check(instant == Instant.parse("2023-06-02T11:00:00Z")) + } + + @Test + fun atStartOfDayIn() { + val zone = TimeZone.of("America/Cuiaba") + // The normal case where `atStartOfDayIn` returns the instant of 00:00:00 in the given time zone. + val normalDate = LocalDate(2023, 6, 2) + val startOfDay = normalDate.atStartOfDayIn(zone) + check(startOfDay.toLocalDateTime(zone) == normalDate.atTime(hour = 0, minute = 0)) + // The edge case where 00:00:00 does not exist in this time zone on this date due to clocks moving forward. + val dateWithoutMidnight = LocalDate(1985, 11, 2) + val startOfDayWithoutMidnight = dateWithoutMidnight.atStartOfDayIn(zone) + check(startOfDayWithoutMidnight.toLocalDateTime(zone) == dateWithoutMidnight.atTime(hour = 1, minute = 0)) + } + + class FixedOffsetTimeZoneSamples { + @Test + fun casting() { + val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) + for ((zoneId, expectedString) in listOf( + "UTC+01:30" to "2023-06-02T12:30+01:30", + "Europe/Berlin" to "2023-06-02T12:30+02:00[Europe/Berlin]", + )) { + val zone = TimeZone.of(zoneId) + // format the local date-time with either just the offset or the offset and the full time zone + val formatted = buildString { + append(localDateTime) + if (zone is FixedOffsetTimeZone) { + append(zone.offset) + } else { + append(localDateTime.toInstant(zone).offsetIn(zone)) + append('[') + append(zone.id) + append(']') + } + } + check(formatted == expectedString) + } + } + + @Test + fun constructorFunction() { + val offset = UtcOffset(hours = 1, minutes = 30) + val zone = FixedOffsetTimeZone(offset) + check(zone.id == "+01:30") + check(zone.offset == offset) + } + + @Test + fun offset() { + val zone = TimeZone.of("UTC+01:30") as FixedOffsetTimeZone + check(zone.id == "UTC+01:30") + check(zone.offset == UtcOffset(hours = 1, minutes = 30)) + } + } +} diff --git a/core/common/test/samples/UtcOffsetSamples.kt b/core/common/test/samples/UtcOffsetSamples.kt new file mode 100644 index 000000000..e4b2410ca --- /dev/null +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class UtcOffsetSamples { + + @Test + fun construction() { + val offset = UtcOffset(hours = 3, minutes = 30) + check(offset.totalSeconds == 12600) + check(UtcOffset(seconds = 0) == UtcOffset.ZERO) + } + + @Test + fun simpleParsingAndFormatting() { + val offset = UtcOffset.parse("+01:30") + check(offset.totalSeconds == 90 * 60) + val formatted = offset.toString() + check(formatted == "+01:30") + } + + @Test + fun customFormat() { + val customFormat = UtcOffset.Format { + optional("GMT") { + offsetHours(Padding.NONE); char(':'); offsetMinutesOfHour() + optional { char(':'); offsetSecondsOfMinute() } + } + } + val offset = customFormat.parse("+01:30:15") + check(offset.format(customFormat) == "+1:30:15") + check(offset.format(UtcOffset.Formats.FOUR_DIGITS) == "+0130") + } + + @Test + fun equalsSample() { + val offset1 = UtcOffset.parse("+01:30") + val offset2 = UtcOffset(minutes = 90) + check(offset1 == offset2) + val offset3 = UtcOffset(hours = 1) + check(offset1 != offset3) + } + + @Test + fun parsing() { + check(UtcOffset.parse("+01:30").totalSeconds == 5400) + check(UtcOffset.parse("+0130", UtcOffset.Formats.FOUR_DIGITS).totalSeconds == 5400) + val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } + check(UtcOffset.parse("+130", customFormat).totalSeconds == 5400) + } + + @Test + fun toStringSample() { + check(UtcOffset.parse("+01:30:00").toString() == "+01:30") + check(UtcOffset(hours = 1, minutes = 30).toString() == "+01:30") + check(UtcOffset(seconds = 5400).toString() == "+01:30") + } + + @Test + fun formatting() { + check(UtcOffset(hours = 1, minutes = 30).format(UtcOffset.Formats.FOUR_DIGITS) == "+0130") + val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } + assertEquals("+130", UtcOffset(hours = 1, minutes = 30).format(customFormat)) + check(UtcOffset(hours = 1, minutes = 30).format(customFormat) == "+130") + } + + @Test + fun constructorFunction() { + check(UtcOffset(hours = 3, minutes = 30).totalSeconds == 12600) + check(UtcOffset(seconds = -3600) == UtcOffset(hours = -1)) + try { + UtcOffset(hours = 1, minutes = 60) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Since `hours` is non-zero, `minutes` must be in the range of 0..59 + } + try { + UtcOffset(hours = -1, minutes = 30) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Since `hours` is negative, `minutes` must also be negative + } + } + + @Test + fun asFixedOffsetTimeZone() { + UtcOffset(hours = 3, minutes = 30).asTimeZone().let { timeZone -> + check(timeZone.id == "+03:30") + check(timeZone.offset == UtcOffset(hours = 3, minutes = 30)) + } + } + + class Formats { + @Test + fun isoBasic() { + val offset = UtcOffset.Formats.ISO_BASIC.parse("+103622") + check(offset == UtcOffset(hours = 10, minutes = 36, seconds = 22)) + val formatted = UtcOffset.Formats.ISO_BASIC.format(offset) + check(formatted == "+103622") + } + + @Test + fun iso() { + val offset = UtcOffset.Formats.ISO.parse("+10:36:22") + check(offset == UtcOffset(hours = 10, minutes = 36, seconds = 22)) + val formatted = UtcOffset.Formats.ISO.format(offset) + check(formatted == "+10:36:22") + } + + @Test + fun fourDigits() { + val offset = UtcOffset.Formats.FOUR_DIGITS.parse("+1036") + check(offset == UtcOffset(hours = 10, minutes = 36)) + val offsetWithSeconds = UtcOffset(hours = 10, minutes = 36, seconds = 59) + val formattedOffsetWithSeconds = UtcOffset.Formats.FOUR_DIGITS.format(offsetWithSeconds) + check(formattedOffsetWithSeconds == "+1036") + } + } +} From 6883ffbcbf1e67ec627be30051a829335a0b448d Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 4 Apr 2024 15:44:33 +0200 Subject: [PATCH 07/35] WIP: add samples for LocalTime --- core/common/src/LocalTime.kt | 81 ++++-- core/common/test/samples/LocalTimeSamples.kt | 273 +++++++++++++++++++ core/common/test/samples/UtcOffsetSamples.kt | 1 - 3 files changed, 324 insertions(+), 31 deletions(-) create mode 100644 core/common/test/samples/LocalTimeSamples.kt diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 030146e3b..0f0e7543f 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -47,46 +47,28 @@ import kotlinx.serialization.Serializable * * ### Construction, serialization, and deserialization * - * [LocalTime] can be constructed directly from its components, using the constructor. - * - * ``` - * val night = LocalTime(hour = 23, minute = 13, second = 16, nanosecond = 153_200_001) - * val evening = LocalTime(hour = 18, minute = 31, second = 54) - * val noon = LocalTime(12, 0) - * ``` + * [LocalTime] can be constructed directly from its components, using the constructor. See sample 1. * * [fromSecondOfDay], [fromMillisecondOfDay], and [fromNanosecondOfDay] can be used to obtain a [LocalTime] from the * number of seconds, milliseconds, or nanoseconds since the start of the day, assuming there the offset from the UTC * does not change during the day. * [toSecondOfDay], [toMillisecondOfDay], and [toNanosecondOfDay] are the inverse operations. - * - * ``` - * val time = LocalTime.fromSecondOfDay(13 * 60 * 60) // 13:00, even if the clock was adjusted during the day - * time.toSecondOfDay() // 13 * 60 * 60 - * ``` + * See sample 2. * * [parse] and [toString] methods can be used to obtain a [LocalTime] from and convert it to a string in the - * ISO 8601 extended format. - * - * ``` - * val time = LocalTime.parse("23:13:16.1532") - * time.toString() // "23:13:16.153200" - * ``` + * ISO 8601 extended format. See sample 3. * * [parse] and [LocalTime.format] both support custom formats created with [Format] or defined in [Formats]. - * - * ``` - * val customFormat = LocalTime.Format { - * hour(); char(':'); minute(); char(':'); second() - * optional { char(','); secondFraction() } - * } - * val time = LocalTime.parse("23:13:16,1532", customFormat) - * time.format(customFormat) // "23:13:16,1532" - * ``` + * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalTime]: * - [LocalTimeIso8601Serializer] for the ISO 8601 extended format, * - [LocalTimeComponentSerializer] for an object with components. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.construction + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.representingAsNumbers + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.customFormat */ @Serializable(LocalTimeIso8601Serializer::class) public expect class LocalTime : Comparable { @@ -105,6 +87,7 @@ public expect class LocalTime : Comparable { * * @see LocalTime.toString for formatting using the default format. * @see LocalTime.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoTimeFormat()): LocalTime @@ -123,6 +106,7 @@ public expect class LocalTime : Comparable { * @see LocalTime.toSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.fromNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToSecondOfDay */ public fun fromSecondOfDay(secondOfDay: Int): LocalTime @@ -142,6 +126,7 @@ public expect class LocalTime : Comparable { * @see LocalTime.fromSecondOfDay * @see LocalTime.toMillisecondOfDay * @see LocalTime.fromNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToMillisecondOfDay */ public fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime @@ -160,6 +145,7 @@ public expect class LocalTime : Comparable { * @see LocalTime.fromSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToNanosecondOfDay */ public fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime @@ -180,6 +166,7 @@ public expect class LocalTime : Comparable { * There is a collection of predefined formats in [LocalTime.Formats]. * * @throws IllegalArgumentException if parsing using this format is ambiguous. + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.customFormat */ @Suppress("FunctionName") public fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat @@ -206,6 +193,8 @@ public expect class LocalTime : Comparable { * Fractional parts of the second are included if non-zero. * * Guaranteed to parse all strings that [LocalTime.toString] produces. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.Formats.iso */ public val ISO: DateTimeFormat } @@ -220,21 +209,36 @@ public expect class LocalTime : Comparable { * - [nanosecond] `0..999_999_999` * * @throws IllegalArgumentException if any parameter is out of range. + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.constructorFunction */ public constructor(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0) /** * Returns the hour-of-day (0..23) time component of this time value. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.hour */ public val hour: Int - /** Returns the minute-of-hour (0..59) time component of this time value. */ + /** + * Returns the minute-of-hour (0..59) time component of this time value. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.minute + */ public val minute: Int - /** Returns the second-of-minute (0..59) time component of this time value. */ + /** + * Returns the second-of-minute (0..59) time component of this time value. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.second + */ public val second: Int - /** Returns the nanosecond-of-second (0..999_999_999) time component of this time value. */ + /** + * Returns the nanosecond-of-second (0..999_999_999) time component of this time value. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.nanosecond + */ public val nanosecond: Int /** @@ -248,6 +252,8 @@ public expect class LocalTime : Comparable { * * @see toMillisecondOfDay * @see toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toSecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToSecondOfDay */ public fun toSecondOfDay(): Int @@ -262,6 +268,8 @@ public expect class LocalTime : Comparable { * * @see toSecondOfDay * @see toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toMillisecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToMillisecondOfDay */ public fun toMillisecondOfDay(): Int @@ -276,6 +284,8 @@ public expect class LocalTime : Comparable { * * @see toMillisecondOfDay * @see toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToNanosecondOfDay */ public fun toNanosecondOfDay(): Long @@ -288,6 +298,8 @@ public expect class LocalTime : Comparable { * Note that, on days when there is a time overlap (for example, due to the daylight saving time * transitions in autumn), a "lesser" wall-clock reading can, in fact, happen later than the * "greater" one. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.compareTo */ public override operator fun compareTo(other: LocalTime): Int @@ -308,6 +320,7 @@ public expect class LocalTime : Comparable { * even if they are zero, and will not add trailing zeros to the fractional part of the second for readability. * @see parse for the dual operation: obtaining [LocalTime] from a string. * @see LocalTime.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toStringSample */ public override fun toString(): String } @@ -315,6 +328,8 @@ public expect class LocalTime : Comparable { /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.formatting */ public fun LocalTime.format(format: DateTimeFormat): String = format.format(this) @@ -329,6 +344,8 @@ public fun String.toLocalTime(): LocalTime = LocalTime.parse(this) * * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which * this method does not accept. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.atDateComponentWiseMonthNumber */ public fun LocalTime.atDate(year: Int, monthNumber: Int, dayOfMonth: Int = 0): LocalDateTime = LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) @@ -338,6 +355,8 @@ public fun LocalTime.atDate(year: Int, monthNumber: Int, dayOfMonth: Int = 0): L * * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which * this method does not accept. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.atDateComponentWise */ public fun LocalTime.atDate(year: Int, month: Month, dayOfMonth: Int = 0): LocalDateTime = LocalDateTime(year, month, dayOfMonth, hour, minute, second, nanosecond) @@ -347,6 +366,8 @@ public fun LocalTime.atDate(year: Int, month: Month, dayOfMonth: Int = 0): Local * * There is no check of whether the time is valid on the specified date, because that depends on a time zone, which * this method does not accept. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.atDate */ public fun LocalTime.atDate(date: LocalDate): LocalDateTime = LocalDateTime(date, this) diff --git a/core/common/test/samples/LocalTimeSamples.kt b/core/common/test/samples/LocalTimeSamples.kt new file mode 100644 index 000000000..5f6ae3581 --- /dev/null +++ b/core/common/test/samples/LocalTimeSamples.kt @@ -0,0 +1,273 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.random.* +import kotlin.test.* + +class LocalTimeSamples { + + @Test + fun construction() { + val night = LocalTime(hour = 23, minute = 13, second = 16, nanosecond = 153_200_001) + check(night.hour == 23) + check(night.minute == 13) + check(night.second == 16) + check(night.nanosecond == 153_200_001) + + val noon = LocalTime(12, 0) + check(noon.hour == 12) + check(noon.minute == 0) + check(noon.second == 0) + check(noon.nanosecond == 0) + + val evening = LocalTime(hour = 18, minute = 31, second = 54) + check(evening.nanosecond == 0) + } + + @Test + fun representingAsNumbers() { + val time = LocalTime(hour = 8, minute = 30, second = 15, nanosecond = 123_456_789) + // The number of whole seconds since the start of the day: + val timeAsSecondOfDay = time.toSecondOfDay() + check(timeAsSecondOfDay == 8 * 60 * 60 + 30 * 60 + 15) + // The number of whole milliseconds since the start of the day: + val timeAsMillisecondOfDay = time.toMillisecondOfDay() + check(timeAsMillisecondOfDay == timeAsSecondOfDay * 1_000 + 123) + // The number of nanoseconds since the start of the day: + val timeAsNanosecondOfDay = time.toNanosecondOfDay() + check(timeAsNanosecondOfDay == timeAsMillisecondOfDay * 1_000_000L + 456_789) + // The local time is completely defined by the number of nanoseconds since the start of the day: + val reconstructedTime = LocalTime.fromNanosecondOfDay(timeAsNanosecondOfDay) + check(reconstructedTime == time) + } + + @Test + fun simpleParsingAndFormatting() { + val time = LocalTime.parse("08:30:15.1234567") + check(time == LocalTime(8, 30, 15, 123_456_700)) + val formatted = time.toString() + check(formatted == "08:30:15.123456700") + } + + @Test + fun parsing() { + check(LocalTime.parse("08:30:15.123456789") == LocalTime(8, 30, 15, 123_456_789)) + check(LocalTime.parse("08:30:15") == LocalTime(8, 30, 15)) + check(LocalTime.parse("08:30") == LocalTime(8, 30)) + val customFormat = LocalTime.Format { + hour(); char(':'); minute(); char(':'); second() + alternativeParsing({ char(',') }) { char('.') } // parse either a dot or a comma + secondFraction(fixedLength = 3) + } + check(LocalTime.parse("08:30:15,123", customFormat) == LocalTime(8, 30, 15, 123_000_000)) + } + + @Test + fun fromAndToSecondOfDay() { + val secondsInDay = 24 * 60 * 60 + val randomNumberOfSeconds = Random.nextInt(secondsInDay) + val time = LocalTime.fromSecondOfDay(randomNumberOfSeconds) + check(time.toSecondOfDay() == randomNumberOfSeconds) + check(time.nanosecond == 0) // sub-second part is zero + } + + @Test + fun fromAndToMillisecondOfDay() { + val millisecondsInDay = 24 * 60 * 60 * 1_000 + val randomNumberOfMilliseconds = Random.nextInt(millisecondsInDay) + val time = LocalTime.fromMillisecondOfDay(randomNumberOfMilliseconds) + check(time.toMillisecondOfDay() == randomNumberOfMilliseconds) + check(time.nanosecond % 1_000_000 == 0) // sub-millisecond part is zero + } + + @Test + fun fromAndToNanosecondOfDay() { + val originalTime = LocalTime( + hour = Random.nextInt(24), + minute = Random.nextInt(60), + second = Random.nextInt(60), + nanosecond = Random.nextInt(1_000_000_000) + ) + val nanosecondOfDay = originalTime.toNanosecondOfDay() + val reconstructedTime = LocalTime.fromNanosecondOfDay(nanosecondOfDay) + check(originalTime == reconstructedTime) + } + + @Test + fun customFormat() { + val customFormat = LocalTime.Format { + hour(); char(':'); minute(); char(':'); second() + char(','); secondFraction(fixedLength = 3) + } + val time = LocalTime(8, 30, 15, 123_456_789) + check(time.format(customFormat) == "08:30:15,123") + check(time.format(LocalTime.Formats.ISO) == "08:30:15.123456789") + } + + @Test + fun constructorFunction() { + val time = LocalTime(8, 30, 15, 123_456_789) + check(time.hour == 8) + check(time.minute == 30) + check(time.second == 15) + check(time.nanosecond == 123_456_789) + val timeWithoutSeconds = LocalTime(23, 30) + check(timeWithoutSeconds.hour == 23) + check(timeWithoutSeconds.minute == 30) + check(timeWithoutSeconds.second == 0) + check(timeWithoutSeconds.nanosecond == 0) + } + + @Test + fun hour() { + check(LocalTime(8, 30, 15, 123_456_789).hour == 8) + } + + @Test + fun minute() { + check(LocalTime(8, 30, 15, 123_456_789).minute == 30) + } + + @Test + fun second() { + check(LocalTime(8, 30).second == 0) + check(LocalTime(8, 30, 15, 123_456_789).second == 15) + } + + @Test + fun nanosecond() { + check(LocalTime(8, 30).nanosecond == 0) + check(LocalTime(8, 30, 15).nanosecond == 0) + check(LocalTime(8, 30, 15, 123_456_789).nanosecond == 123_456_789) + } + + @Test + fun toSecondOfDay() { + check(LocalTime(0, 0, 0, 0).toSecondOfDay() == 0) + check(LocalTime(0, 0, 0, 1).toSecondOfDay() == 0) + check(LocalTime(0, 0, 1, 0).toSecondOfDay() == 1) + check(LocalTime(0, 1, 0, 0).toSecondOfDay() == 60) + check(LocalTime(1, 0, 0, 0).toSecondOfDay() == 3_600) + check(LocalTime(1, 1, 1, 0).toSecondOfDay() == 3_600 + 60 + 1) + check(LocalTime(1, 1, 1, 999_999_999).toSecondOfDay() == 3_600 + 60 + 1) + } + + @Test + fun toMillisecondOfDay() { + check(LocalTime(0, 0, 0, 0).toMillisecondOfDay() == 0) + check(LocalTime(0, 0, 0, 1).toMillisecondOfDay() == 0) + check(LocalTime(0, 0, 1, 0).toMillisecondOfDay() == 1000) + check(LocalTime(0, 1, 0, 0).toMillisecondOfDay() == 60_000) + check(LocalTime(1, 0, 0, 0).toMillisecondOfDay() == 3_600_000) + check(LocalTime(1, 1, 1, 1).toMillisecondOfDay() == 3_600_000 + 60_000 + 1_000) + check(LocalTime(1, 1, 1, 1_000_000).toMillisecondOfDay() == 3_600_000 + 60_000 + 1_000 + 1) + } + + @Test + fun toNanosecondOfDay() { + check(LocalTime(0, 0, 0, 0).toNanosecondOfDay() == 0L) + check(LocalTime(0, 0, 0, 1).toNanosecondOfDay() == 1L) + check(LocalTime(0, 0, 1, 0).toNanosecondOfDay() == 1_000_000_000L) + check(LocalTime(0, 1, 0, 0).toNanosecondOfDay() == 60_000_000_000L) + check(LocalTime(1, 0, 0, 0).toNanosecondOfDay() == 3_600_000_000_000L) + check(LocalTime(1, 1, 1, 1).toNanosecondOfDay() == 3_600_000_000_000L + 60_000_000_000 + 1_000_000_000 + 1) + } + + @Test + fun compareTo() { + check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 17, minute = 10)) + check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 31)) + check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 30, second = 1)) + check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 30, second = 0, nanosecond = 1)) + } + + @Test + fun toStringSample() { + check(LocalTime(hour = 8, minute = 30).toString() == "08:30") + check(LocalTime(hour = 8, minute = 30, second = 15).toString() == "08:30:15") + check(LocalTime(hour = 8, minute = 30, second = 0, nanosecond = 120000000).toString() == "08:30:00.120") + } + + @Test + fun formatting() { + check(LocalTime(hour = 8, minute = 30).format(LocalTime.Formats.ISO) == "08:30:00") + val customFormat = LocalTime.Format { + hour(); char(':'); minute() + optional { + char(':'); second() + optional { + char('.'); secondFraction(minLength = 3) + } + } + } + val timeWithZeroSeconds = LocalTime(hour = 8, minute = 30) + check(timeWithZeroSeconds.format(customFormat) == "08:30") + val timeWithNonZeroSecondFraction = LocalTime(hour = 8, minute = 30, second = 0, nanosecond = 120000000) + check(timeWithNonZeroSecondFraction.format(customFormat) == "08:30:00.120") + } + + /** + * @see atDateComponentWise + */ + @Test + fun atDateComponentWiseMonthNumber() { + val morning = LocalTime(8, 30) + val firstMorningOfEveryMonth = (1..12).map { month -> + morning.atDate(2021, month, 1) + } + check(firstMorningOfEveryMonth.all { it.time == morning && it.dayOfMonth == 1 }) + } + + /** + * @see atDateComponentWiseMonthNumber + */ + @Test + fun atDateComponentWise() { + val morning = LocalTime(8, 30) + val firstMorningOfEveryMonth = Month.entries.map { month -> + morning.atDate(2021, month, 1) + } + check(firstMorningOfEveryMonth.all { it.time == morning && it.dayOfMonth == 1 }) + } + + @Test + fun atDate() { + val workdayStart = LocalTime(8, 30) + val startDate = LocalDate(2021, Month.JANUARY, 1) + val endDate = LocalDate(2021, Month.DECEMBER, 31) + val allWorkdays = buildList { + var currentDate = startDate + while (currentDate <= endDate) { + if (currentDate.dayOfWeek != DayOfWeek.SATURDAY && currentDate.dayOfWeek != DayOfWeek.SUNDAY) { + add(currentDate) + } + currentDate = currentDate.plus(1, DateTimeUnit.DAY) + } + } + val allStartsOfWorkdays = allWorkdays.map { + workdayStart.atDate(it) + } + check(allStartsOfWorkdays.all { it.time == workdayStart }) + } + + class Formats { + @Test + fun iso() { + val timeWithNanoseconds = LocalTime(hour = 8, minute = 30, second = 15, nanosecond = 160_000_000) + val timeWithSeconds = LocalTime(hour = 8, minute = 30, second = 15) + val timeWithoutSeconds = LocalTime(hour = 8, minute = 30) + check(LocalTime.Formats.ISO.parse("08:30:15.16") == timeWithNanoseconds) + check(LocalTime.Formats.ISO.parse("08:30:15") == timeWithSeconds) + check(LocalTime.Formats.ISO.parse("08:30") == timeWithoutSeconds) + check(LocalTime.Formats.ISO.format(timeWithNanoseconds) == "08:30:15.16") + check(LocalTime.Formats.ISO.format(timeWithSeconds) == "08:30:15") + check(LocalTime.Formats.ISO.format(timeWithoutSeconds) == "08:30:00") + } + } +} diff --git a/core/common/test/samples/UtcOffsetSamples.kt b/core/common/test/samples/UtcOffsetSamples.kt index e4b2410ca..7ea006799 100644 --- a/core/common/test/samples/UtcOffsetSamples.kt +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -67,7 +67,6 @@ class UtcOffsetSamples { fun formatting() { check(UtcOffset(hours = 1, minutes = 30).format(UtcOffset.Formats.FOUR_DIGITS) == "+0130") val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } - assertEquals("+130", UtcOffset(hours = 1, minutes = 30).format(customFormat)) check(UtcOffset(hours = 1, minutes = 30).format(customFormat) == "+130") } From 671ee5951918e1317334fe673df14362f96d4122 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 5 Apr 2024 14:40:41 +0200 Subject: [PATCH 08/35] WIP: add samples for some formatting APIs --- core/common/src/format/DateTimeFormat.kt | 18 +++ .../src/format/DateTimeFormatBuilder.kt | 8 + core/common/src/format/LocalDateFormat.kt | 61 +++++++- core/common/src/format/Unicode.kt | 1 + .../format/DateTimeFormatBuilderSamples.kt | 99 ++++++++++++ .../samples/format/DateTimeFormatSamples.kt | 141 ++++++++++++++++++ .../samples/format/LocalDateFormatSamples.kt | 137 +++++++++++++++++ .../test/samples/format/UnicodeSamples.kt | 30 ++++ 8 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 core/common/test/samples/format/DateTimeFormatBuilderSamples.kt create mode 100644 core/common/test/samples/format/DateTimeFormatSamples.kt create mode 100644 core/common/test/samples/format/LocalDateFormatSamples.kt create mode 100644 core/common/test/samples/format/UnicodeSamples.kt diff --git a/core/common/src/format/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index adf1e6c26..c04473d5b 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -15,11 +15,17 @@ import kotlinx.datetime.internal.format.parser.* public sealed interface DateTimeFormat { /** * Formats the given [value] into a string, using this format. + * + * @throws IllegalArgumentException if the value does not contain all the information required by the format. + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.format */ public fun format(value: T): String /** * Formats the given [value] into the given [appendable] using this format. + * + * @throws IllegalArgumentException if the value does not contain all the information required by the format. + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.formatTo */ public fun formatTo(appendable: A, value: T): A @@ -27,6 +33,7 @@ public sealed interface DateTimeFormat { * Parses the given [input] string as [T] using this format. * * @throws IllegalArgumentException if the input string is not in the expected format or the value is invalid. + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.parse */ public fun parse(input: CharSequence): T @@ -34,6 +41,7 @@ public sealed interface DateTimeFormat { * Parses the given [input] string as [T] using this format. * * @return the parsed value, or `null` if the input string is not in the expected format or the value is invalid. + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.parseOrNull */ public fun parseOrNull(input: CharSequence): T? @@ -44,6 +52,8 @@ public sealed interface DateTimeFormat { * * The typical use case for this is to create a [DateTimeFormat] instance using a non-idiomatic approach and * then convert it to a builder DSL. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.formatAsKotlinBuilderDsl */ public fun formatAsKotlinBuilderDsl(format: DateTimeFormat<*>): String = when (format) { is AbstractDateTimeFormat<*, *> -> format.actualFormat.builderString(allFormatConstants) @@ -53,20 +63,28 @@ public sealed interface DateTimeFormat { /** * The style of padding to use when formatting a value. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.usage */ public enum class Padding { /** * No padding during formatting. Parsing does not require padding, but it is allowed. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.none */ NONE, /** * Pad with zeros during formatting. During parsing, the padding is required, or parsing fails. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.zero */ ZERO, /** * Pad with spaces during formatting. During parsing, the padding is required, or parsing fails. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.spaces */ SPACE } diff --git a/core/common/src/format/DateTimeFormatBuilder.kt b/core/common/src/format/DateTimeFormatBuilder.kt index 2230051dd..ae1266edf 100644 --- a/core/common/src/format/DateTimeFormatBuilder.kt +++ b/core/common/src/format/DateTimeFormatBuilder.kt @@ -18,6 +18,8 @@ public sealed interface DateTimeFormatBuilder { * * When formatting, the string is appended to the result as is, * and when parsing, the string is expected to be present in the input verbatim. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatBuilderSamples.chars */ public fun chars(value: String) @@ -332,6 +334,8 @@ internal fun DateTimeFormatBuilder.WithTime.secondFractionInternal( * ``` * * This will always format a date as `MM/DD`, but will also accept `DD-MM` and `MM DD`. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatBuilderSamples.alternativeParsing */ @Suppress("UNCHECKED_CAST") public fun T.alternativeParsing( @@ -352,6 +356,7 @@ public fun T.alternativeParsing( * * When formatting, the section is formatted if the value of any field in the block is not equal to the default value. * Only [optional] calls where all the fields have default values are permitted. + * See [alternativeParsing] for a way to parse some fields optionally without introducing special formatting behavior. * * Example: * ``` @@ -368,6 +373,7 @@ public fun T.alternativeParsing( * [ifZero] defines the string that is used if values are the default ones. * * @throws IllegalArgumentException if not all fields used in [format] have a default value. + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatBuilderSamples.optional */ @Suppress("UNCHECKED_CAST") public fun T.optional(ifZero: String = "", format: T.() -> Unit): Unit = when (this) { @@ -383,6 +389,8 @@ public fun T.optional(ifZero: String = "", format: T * A literal character. * * This is a shorthand for `chars(value.toString())`. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeFormatBuilderSamples.char */ public fun DateTimeFormatBuilder.char(value: Char): Unit = chars(value.toString()) diff --git a/core/common/src/format/LocalDateFormat.kt b/core/common/src/format/LocalDateFormat.kt index 21484c70b..4d63ab967 100644 --- a/core/common/src/format/LocalDateFormat.kt +++ b/core/common/src/format/LocalDateFormat.kt @@ -6,6 +6,8 @@ package kotlinx.datetime.format import kotlinx.datetime.* +import kotlinx.datetime.format.MonthNames.Companion.ENGLISH_ABBREVIATED +import kotlinx.datetime.format.MonthNames.Companion.ENGLISH_FULL import kotlinx.datetime.internal.* import kotlinx.datetime.internal.format.* import kotlinx.datetime.internal.format.parser.Copyable @@ -13,11 +15,21 @@ import kotlinx.datetime.internal.format.parser.Copyable /** * A description of how month names are formatted. * + * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithDate.monthName]. + * + * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED], and custom instances can be created + * using the constructor. + * * An [IllegalArgumentException] will be thrown if some month name is empty or there are duplicate names. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.usage + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.constructionFromList */ public class MonthNames( /** * A list of month names, in order from January to December. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.names */ public val names: List ) { @@ -35,6 +47,8 @@ public class MonthNames( /** * Create a [MonthNames] using the month names in order from January to December. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.constructionFromStrings */ public constructor( january: String, february: String, march: String, april: String, may: String, june: String, @@ -45,6 +59,8 @@ public class MonthNames( public companion object { /** * English month names, 'January' to 'December'. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.englishFull */ public val ENGLISH_FULL: MonthNames = MonthNames( listOf( @@ -55,6 +71,8 @@ public class MonthNames( /** * Shortened English month names, 'Jan' to 'Dec'. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.englishAbbreviated */ public val ENGLISH_ABBREVIATED: MonthNames = MonthNames( listOf( @@ -63,22 +81,43 @@ public class MonthNames( ) ) } + + /** @suppress */ + override fun toString(): String = + names.joinToString(", ", "MonthNames(", ")", transform = String::toString) + + /** @suppress */ + override fun equals(other: Any?): Boolean = other is MonthNames && names == other.names + + /** @suppress */ + override fun hashCode(): Int = names.hashCode() } -internal fun MonthNames.toKotlinCode(): String = when (this.names) { +private fun MonthNames.toKotlinCode(): String = when (this.names) { MonthNames.ENGLISH_FULL.names -> "MonthNames.${DayOfWeekNames.Companion::ENGLISH_FULL.name}" MonthNames.ENGLISH_ABBREVIATED.names -> "MonthNames.${DayOfWeekNames.Companion::ENGLISH_ABBREVIATED.name}" else -> names.joinToString(", ", "MonthNames(", ")", transform = String::toKotlinCode) } /** - * A description of how day of week names are formatted. + * A description of how day-of-week names are formatted. + * + * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithDate.dayOfWeek]. + * + * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED], and custom instances can be created + * using the constructor. * * An [IllegalArgumentException] will be thrown if some day-of-week name is empty or there are duplicate names. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.usage + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.constructionFromList +>>>>>>> 585441e (WIP: add samples for some formatting APIs) */ public class DayOfWeekNames( /** * A list of day of week names, in order from Monday to Sunday. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.names */ public val names: List ) { @@ -96,6 +135,8 @@ public class DayOfWeekNames( /** * A constructor that takes the day of week names, in order from Monday to Sunday. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.constructionFromStrings */ public constructor( monday: String, @@ -111,6 +152,8 @@ public class DayOfWeekNames( public companion object { /** * English day of week names, 'Monday' to 'Sunday'. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.englishFull */ public val ENGLISH_FULL: DayOfWeekNames = DayOfWeekNames( listOf( @@ -120,6 +163,8 @@ public class DayOfWeekNames( /** * Shortened English day of week names, 'Mon' to 'Sun'. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.englishAbbreviated */ public val ENGLISH_ABBREVIATED: DayOfWeekNames = DayOfWeekNames( listOf( @@ -127,9 +172,19 @@ public class DayOfWeekNames( ) ) } + + /** @suppress */ + override fun toString(): String = + names.joinToString(", ", "DayOfWeekNames(", ")", transform = String::toString) + + /** @suppress */ + override fun equals(other: Any?): Boolean = other is DayOfWeekNames && names == other.names + + /** @suppress */ + override fun hashCode(): Int = names.hashCode() } -internal fun DayOfWeekNames.toKotlinCode(): String = when (this.names) { +private fun DayOfWeekNames.toKotlinCode(): String = when (this.names) { DayOfWeekNames.ENGLISH_FULL.names -> "DayOfWeekNames.${DayOfWeekNames.Companion::ENGLISH_FULL.name}" DayOfWeekNames.ENGLISH_ABBREVIATED.names -> "DayOfWeekNames.${DayOfWeekNames.Companion::ENGLISH_ABBREVIATED.name}" else -> names.joinToString(", ", "DayOfWeekNames(", ")", transform = String::toKotlinCode) diff --git a/core/common/src/format/Unicode.kt b/core/common/src/format/Unicode.kt index 5b87f0c72..79d49c9e2 100644 --- a/core/common/src/format/Unicode.kt +++ b/core/common/src/format/Unicode.kt @@ -100,6 +100,7 @@ public annotation class FormatStringsInDatetimeFormats * @throws IllegalArgumentException if the pattern is invalid or contains unsupported directives. * @throws IllegalArgumentException if the builder is incompatible with the specified directives. * @throws UnsupportedOperationException if the kotlinx-datetime library does not support the specified directives. + * @sample kotlinx.datetime.test.samples.format.UnicodeSample.byUnicodePattern */ @FormatStringsInDatetimeFormats public fun DateTimeFormatBuilder.byUnicodePattern(pattern: String) { diff --git a/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt new file mode 100644 index 000000000..ba21418a4 --- /dev/null +++ b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class DateTimeFormatBuilderSamples { + + @Test + fun chars() { + val format = LocalDate.Format { + monthNumber() + char('/') + dayOfMonth() + chars(", ") + year() + } + check(LocalDate(2020, 1, 13).format(format) == "01/13, 2020") + } + + @Test + fun alternativeParsing() { + val format = DateTimeComponents.Format { + // optionally, date: + alternativeParsing({ + }) { + date(LocalDate.Formats.ISO) + } + // optionally, time: + alternativeParsing({ + }) { + // either the `T` or the `t` character: + alternativeParsing({ char('t') }) { char('T') } + time(LocalTime.Formats.ISO) + } + } + val date = LocalDate(2020, 1, 13) + val time = LocalTime(12, 30, 16) + check(format.parse("2020-01-13t12:30:16").toLocalDateTime() == date.atTime(time)) + check(format.parse("2020-01-13").toLocalDate() == date) + check(format.parse("T12:30:16").toLocalTime() == time) + check(format.format { setDate(date); setTime(time) } == "2020-01-13T12:30:16") + } + + @Test + fun optional() { + val format = UtcOffset.Format { + optional(ifZero = "Z") { + offsetHours() + optional { + char(':') + offsetMinutesOfHour() + optional { + char(':') + offsetSecondsOfMinute() + } + } + } + } + // During parsing, the optional parts can be omitted: + check(format.parse("Z") == UtcOffset.ZERO) + check(format.parse("-05") == UtcOffset(hours = -5)) + check(format.parse("-05:30") == UtcOffset(hours = -5, minutes = -30)) + check(format.parse("-05:15:05") == UtcOffset(hours = -5, minutes = -15, seconds = -5)) + // ... but they can also be present: + check(format.parse("-05:00") == UtcOffset(hours = -5)) + check(format.parse("-05:00:00") == UtcOffset(hours = -5)) + // During formatting, the optional parts are only included if they are non-zero: + check(UtcOffset.ZERO.format(format) == "Z") + check(UtcOffset(hours = -5).format(format) == "-05") + check(UtcOffset(hours = -5, minutes = -30).format(format) == "-05:30") + check(UtcOffset(hours = -5, minutes = -15, seconds = -5).format(format) == "-05:15:05") + + try { + LocalDate.Format { optional { year() }} + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Since `year` has no optional component, it is an error to put it inside `optional`. + // Use `alternativeParsing` for parsing-only optional components. + } + } + + @Test + fun char() { + val format = LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + dayOfMonth() + } + check(LocalDate(2020, 1, 1).format(format) == "2020-01-01") + } +} diff --git a/core/common/test/samples/format/DateTimeFormatSamples.kt b/core/common/test/samples/format/DateTimeFormatSamples.kt new file mode 100644 index 000000000..5d6fa78f8 --- /dev/null +++ b/core/common/test/samples/format/DateTimeFormatSamples.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class DateTimeFormatSamples { + + @Test + fun format() { + check(LocalDate.Formats.ISO.format(LocalDate(2021, 2, 7)) == "2021-02-07") + } + + @Test + fun formatTo() { + val sb = StringBuilder() + sb.append("Today is ") + LocalDate.Formats.ISO.formatTo(sb, LocalDate(2024, 4, 5)) + check(sb.toString() == "Today is 2024-04-05") + } + + @Test + fun parse() { + check(LocalDate.Formats.ISO.parse("2021-02-07") == LocalDate(2021, 2, 7)) + try { + LocalDate.Formats.ISO.parse("2021-02-07T") + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // the input string is not in the expected format + } + try { + LocalDate.Formats.ISO.parse("2021-02-40") + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // the input string is in the expected format, but the value is invalid + } + // to parse strings that have valid formats but invalid values, use `DateTimeComponents`: + check(DateTimeComponents.Format { date(LocalDate.Formats.ISO) }.parse("2021-02-40").dayOfMonth == 40) + } + + @Test + fun parseOrNull() { + check(LocalDate.Formats.ISO.parseOrNull("2021-02-07") == LocalDate(2021, 2, 7)) + check(LocalDate.Formats.ISO.parseOrNull("2021-02-07T") == null) + check(LocalDate.Formats.ISO.parseOrNull("2021-02-40") == null) + check(LocalDate.Formats.ISO.parseOrNull("2021-02-40") == null) + // to parse strings that have valid formats but invalid values, use `DateTimeComponents`: + val dateTimeComponentsFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO) } + check(dateTimeComponentsFormat.parseOrNull("2021-02-40")?.dayOfMonth == 40) + } + + @Test + fun formatAsKotlinBuilderDsl() { + val customFormat = LocalDate.Format { + @OptIn(FormatStringsInDatetimeFormats::class) + byUnicodePattern("MM/dd uuuu") + } + val customFormatAsKotlinCode = DateTimeFormat.formatAsKotlinBuilderDsl(customFormat) + check( + customFormatAsKotlinCode == """ + monthNumber() + char('/') + dayOfMonth() + char(' ') + year() + """.trimIndent() + ) + } + + class PaddingSamples { + @Test + fun usage() { + val format = LocalDate.Format { + year(Padding.SPACE) + chars(", ") + monthNumber(Padding.NONE) + char('/') + dayOfMonth(Padding.ZERO) + } + val leoFirstReignStart = LocalDate(457, 2, 7) + check(leoFirstReignStart.format(format) == " 457, 2/07") + } + + @Test + fun zero() { + val format = LocalDate.Format { + monthNumber(Padding.ZERO) // padding with zeros is the default, but can be explicitly specified + char('/') + dayOfMonth() + char(' ') + year() + } + val leoFirstReignStart = LocalDate(457, 2, 7) + check(leoFirstReignStart.format(format) == "02/07 0457") + check(LocalDate.parse("02/07 0457", format) == leoFirstReignStart) + try { + LocalDate.parse("02/7 0457", format) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // parsing without padding is not allowed, and the day-of-month was not padded + } + } + + @Test + fun none() { + val format = LocalDate.Format { + monthNumber(Padding.NONE) + char('/') + dayOfMonth() + char(' ') + year() + } + val leoFirstReignStart = LocalDate(457, 2, 7) + check(leoFirstReignStart.format(format) == "2/07 0457") + // providing leading zeros on parsing is not required, but allowed: + check(LocalDate.parse("2/07 0457", format) == leoFirstReignStart) + check(LocalDate.parse("02/07 0457", format) == leoFirstReignStart) + } + + @Test + fun spaces() { + val format = LocalDate.Format { + monthNumber(Padding.SPACE) + char('/') + dayOfMonth() + char(' ') + year() + } + val leoFirstReignStart = LocalDate(457, 2, 7) + check(leoFirstReignStart.format(format) == " 2/07 0457") + // providing leading zeros on parsing instead of spaces is allowed: + check(LocalDate.parse(" 2/07 0457", format) == leoFirstReignStart) + check(LocalDate.parse("02/07 0457", format) == leoFirstReignStart) + } + } +} diff --git a/core/common/test/samples/format/LocalDateFormatSamples.kt b/core/common/test/samples/format/LocalDateFormatSamples.kt new file mode 100644 index 000000000..0475c2316 --- /dev/null +++ b/core/common/test/samples/format/LocalDateFormatSamples.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class LocalDateFormatSamples { + class MonthNamesSamples { + @Test + fun usage() { + val format = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED) // "Jan", "Feb", ... + char(' ') + dayOfMonth() + chars(", ") + year() + } + check(format.format(LocalDate(2021, 1, 13)) == "Jan 13, 2021") + } + + @Test + fun constructionFromStrings() { + // constructing by passing 12 strings + val myMonthNames = MonthNames( + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ) + check(myMonthNames == MonthNames.ENGLISH_ABBREVIATED) // could just use the built-in one... + } + + @Test + fun constructionFromList() { + val germanMonthNames = listOf( + "Januar", "Februar", "März", "April", "Mai", "Juni", + "Juli", "August", "September", "Oktober", "November", "Dezember" + ) + // constructing by passing a list of 12 strings + val myMonthNamesFromList = MonthNames(germanMonthNames) + check(myMonthNamesFromList.names == germanMonthNames) + } + + @Test + fun names() { + check(MonthNames.ENGLISH_ABBREVIATED.names == listOf( + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + )) + } + + @Test + fun englishFull() { + val format = LocalDate.Format { + monthName(MonthNames.ENGLISH_FULL) + char(' ') + dayOfMonth() + chars(", ") + year() + } + check(format.format(LocalDate(2021, 1, 13)) == "January 13, 2021") + } + + @Test + fun englishAbbreviated() { + val format = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + dayOfMonth() + chars(", ") + year() + } + check(format.format(LocalDate(2021, 1, 13)) == "Jan 13, 2021") + } + } + + class DayOfWeekNamesSamples { + @Test + fun usage() { + val format = LocalDate.Format { + date(LocalDate.Formats.ISO) + chars(", ") + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) // "Mon", "Tue", ... + } + check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wed") + } + + @Test + fun constructionFromStrings() { + // constructing by passing 7 strings + val myMonthNames = DayOfWeekNames( + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" + ) + check(myMonthNames == DayOfWeekNames.ENGLISH_ABBREVIATED) // could just use the built-in one... + } + + @Test + fun constructionFromList() { + val germanDayOfWeekNames = listOf( + "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag" + ) + // constructing by passing a list of 7 strings + val myDayOfWeekNames = DayOfWeekNames(germanDayOfWeekNames) + check(myDayOfWeekNames.names == germanDayOfWeekNames) + } + + @Test + fun names() { + check(DayOfWeekNames.ENGLISH_ABBREVIATED.names == listOf( + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" + )) + } + + @Test + fun englishFull() { + val format = LocalDate.Format { + date(LocalDate.Formats.ISO) + chars(", ") + dayOfWeek(DayOfWeekNames.ENGLISH_FULL) + } + check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wednesday") + } + + @Test + fun englishAbbreviated() { + val format = LocalDate.Format { + date(LocalDate.Formats.ISO) + chars(", ") + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + } + check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wed") + } + } +} diff --git a/core/common/test/samples/format/UnicodeSamples.kt b/core/common/test/samples/format/UnicodeSamples.kt new file mode 100644 index 000000000..466ded0a5 --- /dev/null +++ b/core/common/test/samples/format/UnicodeSamples.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class UnicodeSamples { + @Test + fun byUnicodePattern() { + val customFormat = LocalDate.Format { + @OptIn(FormatStringsInDatetimeFormats::class) + byUnicodePattern("MM/dd uuuu") + } + check(customFormat.format(LocalDate(2021, 1, 13)) == "01/13 2021") + check( + DateTimeFormat.formatAsKotlinBuilderDsl(customFormat) == """ + monthNumber() + char('/') + dayOfMonth() + char(' ') + year() + """.trimIndent() + ) + } +} From bca9eab95cc102a92b436e72c97e178c7c15b106 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 15 Apr 2024 14:51:40 +0200 Subject: [PATCH 09/35] More samples --- core/common/src/format/DateTimeComponents.kt | 101 +++-- .../src/format/DateTimeFormatBuilder.kt | 23 +- core/common/test/samples/TimeZoneSamples.kt | 3 +- .../format/DateTimeComponentsSamples.kt | 385 ++++++++++++++++++ .../samples/format/LocalDateFormatSamples.kt | 81 ++++ 5 files changed, 541 insertions(+), 52 deletions(-) create mode 100644 core/common/test/samples/format/DateTimeComponentsSamples.kt diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 8a80fa5b6..80382ca5d 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -17,51 +17,25 @@ import kotlin.reflect.* * A collection of date-time fields, used specifically for parsing and formatting. * * Its main purpose is to provide support for complex date-time formats that don't correspond to any of the standard - * entities in the library. For example, a format that includes only the month and the day of the month, but not the - * year, can not be represented and parsed as a [LocalDate], but it is valid for a [DateTimeComponents]. - * - * Example: - * ``` - * val input = "2020-03-16T23:59:59.999999999+03:00" - * val components = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse(input) - * val localDateTime = components.toLocalDateTime() // LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999) - * val instant = components.toInstantUsingOffset() // Instant.parse("2020-03-16T20:59:59.999999999Z") - * val offset = components.toUtcOffset() // UtcOffset(hours = 3) - * ``` + * entities in the library. For example, a format that includes only the month and the day of the month but not the + * year cannot be represented and parsed as a [LocalDate], but it is valid for a [DateTimeComponents]. + * See sample 1. * * Another purpose is to support parsing and formatting data with out-of-bounds values. For example, parsing * `23:59:60` as a [LocalTime] is not possible, but it is possible to parse it as a [DateTimeComponents], adjust the value by * setting [second] to `59`, and then convert it to a [LocalTime] via [toLocalTime]. - * - * Example: - * ``` - * val input = "23:59:60" - * val extraDay: Boolean - * val time = DateTimeComponents.Format { - * time(LocalTime.Formats.ISO) - * }.parse(input).apply { - * if (hour == 23 && minute == 59 && second == 60) { - * hour = 0; minute = 0; second = 0; extraDay = true - * } else { - * extraDay = false - * } - * }.toLocalTime() - * ``` + * See sample 2. * * Because this class has limited applications, constructing it directly is not possible. * For formatting, use the [format] overload that accepts a lambda with a [DateTimeComponents] receiver. - * - * Example: - * ``` - * // Mon, 16 Mar 2020 23:59:59 +0300 - * DateTimeComponents.Formats.RFC_1123.format { - * setDateTimeOffset(LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999)) - * setDateTimeOffset(UtcOffset(hours = 3)) - * } - * ``` + * See sample 3. * * Accessing the fields of this class is not thread-safe. * Make sure to apply proper synchronization if you are using a single instance from multiple threads. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.parsingComplexInput + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.parsingInvalidInput + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.simpleFormatting */ public class DateTimeComponents internal constructor(internal val contents: DateTimeComponentsContents = DateTimeComponentsContents()) { public companion object { @@ -71,6 +45,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * There is a collection of predefined formats in [DateTimeComponents.Formats]. * * @throws IllegalArgumentException if parsing using this format is ambiguous. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.customFormat */ @Suppress("FunctionName") public fun Format(block: DateTimeFormatBuilder.WithDateTimeComponents.() -> Unit): DateTimeFormat { @@ -122,6 +97,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * val localDateTime = components.toLocalDateTime() // 2020-08-30T18:43:00.123456789 * val offset = components.toUtcOffset() // UtcOffset(hours = 3) * ``` + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.Formats.iso */ public val ISO_DATE_TIME_OFFSET: DateTimeFormat = Format { date(ISO_DATE) @@ -155,6 +132,9 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * `30 Jun 2008 11:05:30 UT` * * North American and military time zone abbreviations are not supported. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.Formats.rfc1123parsing + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.Formats.rfc1123formatting */ public val RFC_1123: DateTimeFormat = Format { alternativeParsing({ @@ -194,6 +174,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * The [localTime] is written to the [hour], [hourOfAmPm], [amPm], [minute], [second] and [nanosecond] fields. * * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.time */ public fun setTime(localTime: LocalTime) { contents.time.populateFrom(localTime) @@ -204,6 +186,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * The [localDate] is written to the [year], [monthNumber], [dayOfMonth], and [dayOfWeek] fields. * * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date */ public fun setDate(localDate: LocalDate) { contents.date.populateFrom(localDate) @@ -216,6 +200,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * [hour], [hourOfAmPm], [amPm], [minute], [second] and [nanosecond] fields. * * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.setDateTime */ public fun setDateTime(localDateTime: LocalDateTime) { contents.date.populateFrom(localDateTime.date) @@ -228,6 +214,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * [offsetIsNegative] fields. * * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset */ public fun setOffset(utcOffset: UtcOffset) { contents.offset.populateFrom(utcOffset) @@ -244,6 +232,8 @@ public class DateTimeComponents internal constructor(internal val contents: Date * However, this also works for instants that are too large to be represented as a [LocalDateTime]. * * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.setDateTimeOffsetInstant */ public fun setDateTimeOffset(instant: Instant, utcOffset: UtcOffset) { val smallerInstant = Instant.fromEpochSeconds( @@ -261,24 +251,31 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * If [localDateTime] is obtained from an [Instant] using [LocalDateTime.toInstant], it is recommended to use * [setDateTimeOffset] that accepts an [Instant] directly. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.setDateTimeOffset */ public fun setDateTimeOffset(localDateTime: LocalDateTime, utcOffset: UtcOffset) { setDateTime(localDateTime) setOffset(utcOffset) } - /** The year component of the date. */ + /** + * The year component of the date. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date + */ public var year: Int? by contents.date::year /** * The number-of-month (1..12) component of the date. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date */ public var monthNumber: Int? by TwoDigitNumber(contents.date::monthNumber) /** * The month ([Month]) component of the date. * @throws IllegalArgumentException during getting if [monthNumber] is outside the `1..12` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date */ public var month: Month? get() = monthNumber?.let { Month(it) } @@ -289,10 +286,14 @@ public class DateTimeComponents internal constructor(internal val contents: Date /** * The day-of-month component of the date. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date */ public var dayOfMonth: Int? by TwoDigitNumber(contents.date::dayOfMonth) - /** The day-of-week component of the date. */ + /** + * The day-of-week component of the date. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.dayOfWeek + */ public var dayOfWeek: DayOfWeek? get() = contents.date.isoDayOfWeek?.let { DayOfWeek(it) } set(value) { @@ -304,6 +305,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date /** * The hour-of-day (0..23) time component. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.time */ public var hour: Int? by TwoDigitNumber(contents.time::hour) @@ -311,30 +313,35 @@ public class DateTimeComponents internal constructor(internal val contents: Date * The 12-hour (1..12) time component. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. * @see amPm + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.timeAmPm */ public var hourOfAmPm: Int? by TwoDigitNumber(contents.time::hourOfAmPm) /** * The AM/PM state of the time component. * @see hourOfAmPm + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.timeAmPm */ public var amPm: AmPmMarker? by contents.time::amPm /** * The minute-of-hour component. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.time */ public var minute: Int? by TwoDigitNumber(contents.time::minute) /** * The second-of-minute component. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.time */ public var second: Int? by TwoDigitNumber(contents.time::second) /** * The nanosecond-of-second component. * @throws IllegalArgumentException during assignment if the value is outside the `0..999_999_999` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.time */ public var nanosecond: Int? get() = contents.time.nanosecond @@ -345,28 +352,37 @@ public class DateTimeComponents internal constructor(internal val contents: Date contents.time.nanosecond = value } - /** True if the offset is negative. */ + /** + * True if the offset is negative. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset + */ public var offsetIsNegative: Boolean? by contents.offset::isNegative /** * The total amount of full hours in the UTC offset, in the range [0; 18]. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset */ public var offsetHours: Int? by TwoDigitNumber(contents.offset::totalHoursAbs) /** * The amount of minutes that don't add to a whole hour in the UTC offset, in the range [0; 59]. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset */ public var offsetMinutesOfHour: Int? by TwoDigitNumber(contents.offset::minutesOfHour) /** * The amount of seconds that don't add to a whole minute in the UTC offset, in the range [0; 59]. * @throws IllegalArgumentException during assignment if the value is outside the `0..99` range. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.offset */ public var offsetSecondsOfMinute: Int? by TwoDigitNumber(contents.offset::secondsOfMinute) - /** The timezone identifier, for example, "Europe/Berlin". */ + /** + * The timezone identifier, for example, "Europe/Berlin". + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.timeZoneId + */ public var timeZoneId: String? by contents::timeZoneId /** @@ -379,6 +395,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * [offsetSecondsOfMinute] (default value is 0) * * @throws IllegalArgumentException if any of the fields has an out-of-range value. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toUtcOffset */ public fun toUtcOffset(): UtcOffset = contents.offset.toUtcOffset() @@ -393,6 +410,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * Also, [dayOfWeek] is checked for consistency with the other fields. * * @throws IllegalArgumentException if any of the fields is missing or invalid. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toLocalDate */ public fun toLocalDate(): LocalDate = contents.date.toLocalDate() @@ -407,6 +425,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * @throws IllegalArgumentException if hours or minutes are not present, if any of the fields are invalid, or * [hourOfAmPm] and [amPm] are inconsistent with [hour]. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toLocalTime */ public fun toLocalTime(): LocalTime = contents.time.toLocalTime() @@ -429,6 +448,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * @see toLocalDate * @see toLocalTime + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toLocalDateTime */ public fun toLocalDateTime(): LocalDateTime = toLocalDate().atTime(toLocalTime()) @@ -442,6 +462,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * @throws IllegalArgumentException if any of the required fields are not present, out-of-range, or inconsistent * with one another. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toInstantUsingOffset */ public fun toInstantUsingOffset(): Instant { val offset = toUtcOffset() @@ -484,6 +505,7 @@ public class DateTimeComponents internal constructor(internal val contents: Date * @throws IllegalStateException if some values needed for the format are not present or can not be formatted: * for example, trying to format [DateTimeFormatBuilder.WithDate.monthName] using a [DateTimeComponents.monthNumber] * value of 20. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.formatting */ public fun DateTimeFormat.format(block: DateTimeComponents.() -> Unit): String = format(DateTimeComponents().apply { block() }) @@ -496,6 +518,7 @@ public fun DateTimeFormat.format(block: DateTimeComponents.( * matches. * * @throws IllegalArgumentException if the text does not match the format. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.parsing */ public fun DateTimeComponents.Companion.parse( input: CharSequence, diff --git a/core/common/src/format/DateTimeFormatBuilder.kt b/core/common/src/format/DateTimeFormatBuilder.kt index ae1266edf..831f05ee1 100644 --- a/core/common/src/format/DateTimeFormatBuilder.kt +++ b/core/common/src/format/DateTimeFormatBuilder.kt @@ -34,6 +34,8 @@ public sealed interface DateTimeFormatBuilder { * this padding can be disabled or changed to space padding by passing [padding]. * For years outside this range, it's formatted as a decimal number with a leading sign, so the year 12345 * is formatted as "+12345". + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.year */ public fun year(padding: Padding = Padding.ZERO) @@ -51,6 +53,8 @@ public sealed interface DateTimeFormatBuilder { * When given a two-digit year, it returns a year in the valid range, so "93" becomes 1993, * and when given a full year number with a leading sign, it parses the full year number, * so "+1850" becomes 1850. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.yearTwoDigits */ public fun yearTwoDigits(baseYear: Int) @@ -58,16 +62,15 @@ public sealed interface DateTimeFormatBuilder { * A month-of-year number, from 1 to 12. * * By default, it's padded with zeros to two digits. This can be changed by passing [padding]. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.monthNumber */ public fun monthNumber(padding: Padding = Padding.ZERO) /** * A month name (for example, "January"). * - * Example: - * ``` - * monthName(MonthNames.ENGLISH_FULL) - * ``` + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.monthName */ public fun monthName(names: MonthNames) @@ -75,26 +78,22 @@ public sealed interface DateTimeFormatBuilder { * A day-of-month number, from 1 to 31. * * By default, it's padded with zeros to two digits. This can be changed by passing [padding]. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.dayOfMonth */ public fun dayOfMonth(padding: Padding = Padding.ZERO) /** * A day-of-week name (for example, "Thursday"). * - * Example: - * ``` - * dayOfWeek(DayOfWeekNames.ENGLISH_FULL) - * ``` + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.dayOfWeek */ public fun dayOfWeek(names: DayOfWeekNames) /** * An existing [DateTimeFormat] for the date part. * - * Example: - * ``` - * date(LocalDate.Formats.ISO) - * ``` + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.date */ public fun date(format: DateTimeFormat) } diff --git a/core/common/test/samples/TimeZoneSamples.kt b/core/common/test/samples/TimeZoneSamples.kt index 4e05757be..2436082e0 100644 --- a/core/common/test/samples/TimeZoneSamples.kt +++ b/core/common/test/samples/TimeZoneSamples.kt @@ -85,7 +85,8 @@ class TimeZoneSamples { fun availableZoneIds() { for (zoneId in TimeZone.availableZoneIds) { val zone = TimeZone.of(zoneId) - check(zone.id == zoneId) + // for fixed-offset time zones, normalization can happen, e.g. "UTC+01" -> "UTC+01:00" + check(zone.id == zoneId || zone is FixedOffsetTimeZone) } } diff --git a/core/common/test/samples/format/DateTimeComponentsSamples.kt b/core/common/test/samples/format/DateTimeComponentsSamples.kt new file mode 100644 index 000000000..e463b0777 --- /dev/null +++ b/core/common/test/samples/format/DateTimeComponentsSamples.kt @@ -0,0 +1,385 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class DateTimeComponentsSamples { + + @Test + fun parsingComplexInput() { + val input = "2020-03-16T23:59:59.999999999+03:00" + val components = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse(input) + check(components.toLocalDateTime() == LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999)) + check(components.toInstantUsingOffset() == Instant.parse("2020-03-16T20:59:59.999999999Z")) + check(components.toUtcOffset() == UtcOffset(3, 0)) + } + + @Test + fun parsingInvalidInput() { + val input = "23:59:60" + val extraDay: Boolean + val time = DateTimeComponents.Format { + time(LocalTime.Formats.ISO) + }.parse(input).apply { + if (hour == 23 && minute == 59 && second == 60) { + hour = 0; minute = 0; second = 0; extraDay = true + } else { + extraDay = false + } + }.toLocalTime() + check(time == LocalTime(0, 0)) + check(extraDay) + } + + @Test + fun simpleFormatting() { + val formatted = DateTimeComponents.Formats.RFC_1123.format { + setDateTimeOffset( + LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999), + UtcOffset(hours = 3) + ) + } + check(formatted == "Mon, 16 Mar 2020 23:59:59 +0300") + } + + @Test + fun customFormat() { + val customFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + char(' ') + offset(UtcOffset.Formats.FOUR_DIGITS) + } + val formatted = customFormat.format { + setDate(LocalDate(2023, 1, 2)) + setTime(LocalTime(3, 46, 58, 530_000_000)) + setOffset(UtcOffset(3, 30)) + } + check(formatted == "2023-01-02 03:46:58.530 +0330") + val parsed = customFormat.parse("2023-01-31 24:00:00.530 +0330").apply { + // components can be out of bounds + if (hour == 24 && minute == 0 && second == 0) { + setTime(LocalTime(0, 0)) + setDate(toLocalDate().plus(1, DateTimeUnit.DAY)) + } + } + check(parsed.toLocalDate() == LocalDate(2023, 2, 1)) + check(parsed.toLocalTime() == LocalTime(0, 0)) + check(parsed.toUtcOffset() == UtcOffset(3, 30)) + } + + @Test + fun setDateTime() { + val dateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) + val customFormat = DateTimeComponents.Format { + dateTime(LocalDateTime.Formats.ISO) + char('[') + timeZoneId() + char(']') + } + val formatted = customFormat.format { + setDateTime(dateTime) + timeZoneId = "America/New_York" + } + check(formatted == "2021-03-28T02:16:20[America/New_York]") + } + + @Test + fun setDateTimeOffsetInstant() { + val instant = Instant.parse("2021-03-28T02:16:20+03:00") + val offset = UtcOffset(3, 0) + val formatted = DateTimeComponents.Formats.RFC_1123.format { + setDateTimeOffset(instant, offset) + } + check(formatted == "Sun, 28 Mar 2021 02:16:20 +0300") + } + + @Test + fun setDateTimeOffset() { + val localDateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) + val offset = UtcOffset(3, 0) + val formatted = DateTimeComponents.Formats.RFC_1123.format { + setDateTimeOffset(localDateTime, offset) + } + check(formatted == "Sun, 28 Mar 2021 02:16:20 +0300") + } + + @Test + fun dayOfWeek() { + val formatWithDayOfWeek = DateTimeComponents.Format { + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + char(' ') + date(LocalDate.Formats.ISO) + } + val formattedWithDayOfWeek = formatWithDayOfWeek.format { + setDate(LocalDate(2021, 3, 28)) + check(dayOfWeek == DayOfWeek.SUNDAY) // `setDate` sets the day of the week automatically + } + check(formattedWithDayOfWeek == "Sun 2021-03-28") + val parsedWithDayOfWeek = formatWithDayOfWeek.parse("Sun 2021-03-28") + check(parsedWithDayOfWeek.toLocalDate() == LocalDate(2021, 3, 28)) + check(parsedWithDayOfWeek.dayOfWeek == DayOfWeek.SUNDAY) + // Note: the day of the week is only parsed when it's present in the format + val formatWithoutDayOfWeek = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + } + val parsedWithoutDayOfWeek = formatWithoutDayOfWeek.parse("2021-03-28") + check(parsedWithoutDayOfWeek.dayOfWeek == null) + } + + @Test + fun date() { + val format = DateTimeComponents.Format { + year(); char('-'); monthNumber(); char('-'); dayOfMonth() + } + val formattedDate = format.format { + setDate(LocalDate(2023, 1, 2)) + check(year == 2023) + check(month == Month.JANUARY) + check(dayOfMonth == 2) + check(dayOfWeek == DayOfWeek.MONDAY) + } + check(formattedDate == "2023-01-02") + val parsedDate = format.parse("2023-01-02") + check(parsedDate.toLocalDate() == LocalDate(2023, 1, 2)) + check(parsedDate.year == 2023) + check(parsedDate.month == Month.JANUARY) + check(parsedDate.dayOfMonth == 2) + check(parsedDate.dayOfWeek == null) + } + + @Test + fun timeAmPm() { + val format = DateTimeComponents.Format { + amPmHour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(1, 9) + char(' '); amPmMarker("AM", "PM") + } + val formattedTime = format.format { + setTime(LocalTime(3, 46, 58, 123_456_789)) + check(hour == 3) + check(minute == 46) + check(second == 58) + check(nanosecond == 123_456_789) + check(hourOfAmPm == 3) + check(amPm == AmPmMarker.AM) + } + check(formattedTime == "03:46:58.123456789 AM") + val parsedTime = format.parse("03:46:58.123456789 AM") + check(parsedTime.toLocalTime() == LocalTime(3, 46, 58, 123_456_789)) + check(parsedTime.hour == null) + check(parsedTime.minute == 46) + check(parsedTime.second == 58) + check(parsedTime.nanosecond == 123_456_789) + check(parsedTime.hourOfAmPm == 3) + check(parsedTime.amPm == AmPmMarker.AM) + } + + @Test + fun time() { + val format = DateTimeComponents.Format { + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(1, 9) + } + val formattedTime = format.format { + setTime(LocalTime(3, 46, 58, 123_456_789)) + check(hour == 3) + check(minute == 46) + check(second == 58) + check(nanosecond == 123_456_789) + check(hourOfAmPm == 3) + check(amPm == AmPmMarker.AM) + } + check(formattedTime == "03:46:58.123456789") + val parsedTime = format.parse("03:46:58.123456789") + check(parsedTime.toLocalTime() == LocalTime(3, 46, 58, 123_456_789)) + check(parsedTime.hour == 3) + check(parsedTime.minute == 46) + check(parsedTime.second == 58) + check(parsedTime.nanosecond == 123_456_789) + check(parsedTime.hourOfAmPm == null) + check(parsedTime.amPm == null) + } + + @Test + fun offset() { + val format = DateTimeComponents.Format { offset(UtcOffset.Formats.ISO) } + val formattedOffset = format.format { + setOffset(UtcOffset(-3, -30, -15)) + check(offsetHours == 3) + check(offsetMinutesOfHour == 30) + check(offsetSecondsOfMinute == 15) + check(offsetIsNegative == true) + } + check(formattedOffset == "-03:30:15") + val parsedOffset = format.parse("-03:30:15") + check(parsedOffset.toUtcOffset() == UtcOffset(-3, -30, -15)) + check(parsedOffset.offsetHours == 3) + check(parsedOffset.offsetMinutesOfHour == 30) + check(parsedOffset.offsetSecondsOfMinute == 15) + check(parsedOffset.offsetIsNegative == true) + } + + @Test + fun timeZoneId() { + val formatWithTimeZone = DateTimeComponents.Format { + dateTime(LocalDateTime.Formats.ISO) + char('[') + timeZoneId() + char(']') + } + val formattedWithTimeZone = formatWithTimeZone.format { + setDateTime(LocalDate(2021, 3, 28).atTime(2, 16, 20)) + timeZoneId = "America/New_York" + } + check(formattedWithTimeZone == "2021-03-28T02:16:20[America/New_York]") + val parsedWithTimeZone = DateTimeComponents.parse(formattedWithTimeZone, formatWithTimeZone) + check(parsedWithTimeZone.timeZoneId == "America/New_York") + try { + formatWithTimeZone.parse("2021-03-28T02:16:20[Mars/Phobos]") + fail("Expected an exception") + } catch (e: DateTimeFormatException) { + // expected: the time zone ID is invalid + } + } + + @Test + fun toUtcOffset() { + val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) + val offset = parsed.toUtcOffset() + check(offset == UtcOffset(3, 0)) + } + + @Test + fun toLocalDate() { + val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) + val localDate = parsed.toLocalDate() + check(localDate == LocalDate(1994, 11, 6)) + } + + @Test + fun toLocalTime() { + val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) + val localTime = parsed.toLocalTime() + check(localTime == LocalTime(8, 49, 37)) + } + + @Test + fun toLocalDateTime() { + val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) + val localDateTime = parsed.toLocalDateTime() + check(localDateTime == LocalDateTime(1994, 11, 6, 8, 49, 37)) + } + + @Test + fun toInstantUsingOffset() { + val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) + val instant = parsed.toInstantUsingOffset() + check(instant == Instant.parse("1994-11-06T08:49:37+03:00")) + val localDateTime = parsed.toLocalDateTime() + val offset = parsed.toUtcOffset() + check(localDateTime.toInstant(offset) == instant) + } + + @Test + fun formatting() { + // DateTimeComponents can be used to format complex data that consists of multiple components + val compoundFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + char(' ') + offsetHours(); char(':'); offsetMinutesOfHour(); char(':'); offsetSecondsOfMinute() + } + val formattedCompoundData = compoundFormat.format { + setDate(LocalDate(2023, 1, 2)) + setTime(LocalTime(3, 46, 58, 531_000_000)) + setOffset(UtcOffset(3, 30)) + } + check(formattedCompoundData == "2023-01-02 03:46:58.531 +03:30:00") + // It can also be used to format partial data that is missing some components + val partialFormat = DateTimeComponents.Format { + year(); char('-'); monthNumber() + } + val formattedPartialData = partialFormat.format { + year = 2023 + month = Month.JANUARY + } + check(formattedPartialData == "2023-01") + } + + @Test + fun parsing() { + // DateTimeComponents can be used to parse complex data that consists of multiple components + val compoundFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + char(' ') + offsetHours(); char(':'); offsetMinutesOfHour(); optional { char(':'); offsetSecondsOfMinute() } + } + val parsedCompoundData = DateTimeComponents.parse("2023-01-02 03:46:58.531 +03:30", compoundFormat) + check(parsedCompoundData.toLocalTime() == LocalTime(3, 46, 58, 531_000_000)) + check(parsedCompoundData.toLocalDate() == LocalDate(2023, 1, 2)) + check(parsedCompoundData.toUtcOffset() == UtcOffset(3, 30)) + check(parsedCompoundData.toInstantUsingOffset() == Instant.parse("2023-01-02T03:46:58.531+03:30")) + // It can also be used to parse partial data that is missing some components + val partialFormat = DateTimeComponents.Format { + year(); char('-'); monthNumber() + } + val parsedPartialData = DateTimeComponents.parse("2023-01", partialFormat) + check(parsedPartialData.year == 2023) + check(parsedPartialData.month == Month.JANUARY) + try { + parsedPartialData.toLocalDate() + fail("Expected an exception") + } catch (e: IllegalArgumentException) { + // expected: the day is missing, so LocalDate cannot be constructed + } + } + + class Formats { + @Test + fun rfc1123parsing() { + val rfc1123string = "Mon, 30 Jun 2008 11:05:30 -0300" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123string) + check(parsed.toLocalDate() == LocalDate(2008, 6, 30)) + check(parsed.toLocalTime() == LocalTime(11, 5, 30)) + check(parsed.toUtcOffset() == UtcOffset(-3, 0)) + } + + @Test + fun rfc1123formatting() { + val today = Instant.fromEpochSeconds(1713182461) + val offset = today.offsetIn(TimeZone.of("Europe/Berlin")) + val formatted = DateTimeComponents.Formats.RFC_1123.format { + setDateTimeOffset(today, offset) + } + check(formatted == "Mon, 15 Apr 2024 14:01:01 +0200") + } + + @Test + fun iso() { + val formatted = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.format { + setDate(LocalDate(2023, 1, 2)) + setTime(LocalTime(3, 46, 58, 530_000_000)) + setOffset(UtcOffset(3, 30)) + } + check(formatted == "2023-01-02T03:46:58.53+03:30") + val parsed = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse("2023-01-02T03:46:58.53+03:30") + check(parsed.toLocalDate() == LocalDate(2023, 1, 2)) + check(parsed.toLocalTime() == LocalTime(3, 46, 58, 530_000_000)) + check(parsed.toUtcOffset() == UtcOffset(3, 30)) + } + } +} diff --git a/core/common/test/samples/format/LocalDateFormatSamples.kt b/core/common/test/samples/format/LocalDateFormatSamples.kt index 0475c2316..bb2bd5c94 100644 --- a/core/common/test/samples/format/LocalDateFormatSamples.kt +++ b/core/common/test/samples/format/LocalDateFormatSamples.kt @@ -10,6 +10,87 @@ import kotlinx.datetime.format.* import kotlin.test.* class LocalDateFormatSamples { + + @Test + fun year() { + val format = LocalDate.Format { + year(); char(' '); monthNumber(); char('/'); dayOfMonth() + } + check(format.format(LocalDate(2021, 1, 13)) == "2021 01/13") + check(format.format(LocalDate(13, 1, 13)) == "0013 01/13") + check(format.format(LocalDate(-2021, 1, 13)) == "-2021 01/13") + check(format.format(LocalDate(12021, 1, 13)) == "+12021 01/13") + } + + @Test + fun yearTwoDigits() { + val format = LocalDate.Format { + yearTwoDigits(baseYear = 1960); char(' '); monthNumber(); char('/'); dayOfMonth() + } + check(format.format(LocalDate(1960, 1, 13)) == "60 01/13") + check(format.format(LocalDate(2000, 1, 13)) == "00 01/13") + check(format.format(LocalDate(2021, 1, 13)) == "21 01/13") + check(format.format(LocalDate(2059, 1, 13)) == "59 01/13") + check(format.format(LocalDate(2060, 1, 13)) == "+2060 01/13") + check(format.format(LocalDate(-13, 1, 13)) == "-13 01/13") + } + + @Test + fun monthNumber() { + val zeroPaddedMonths = LocalDate.Format { + monthNumber(); char('/'); dayOfMonth(); char('/'); year() + } + check(zeroPaddedMonths.format(LocalDate(2021, 1, 13)) == "01/13/2021") + check(zeroPaddedMonths.format(LocalDate(2021, 12, 13)) == "12/13/2021") + val spacePaddedMonths = LocalDate.Format { + monthNumber(padding = Padding.SPACE); char('/'); dayOfMonth(); char('/'); year() + } + check(spacePaddedMonths.format(LocalDate(2021, 1, 13)) == " 1/13/2021") + check(spacePaddedMonths.format(LocalDate(2021, 12, 13)) == "12/13/2021") + } + + @Test + fun monthName() { + val format = LocalDate.Format { + monthName(MonthNames.ENGLISH_FULL); char(' '); dayOfMonth(); char('/'); year() + } + check(format.format(LocalDate(2021, 1, 13)) == "January 13/2021") + check(format.format(LocalDate(2021, 12, 13)) == "December 13/2021") + } + + @Test + fun dayOfMonth() { + val zeroPaddedDays = LocalDate.Format { + dayOfMonth(); char('/'); monthNumber(); char('/'); year() + } + check(zeroPaddedDays.format(LocalDate(2021, 1, 6)) == "06/01/2021") + check(zeroPaddedDays.format(LocalDate(2021, 1, 31)) == "31/01/2021") + val spacePaddedDays = LocalDate.Format { + dayOfMonth(padding = Padding.SPACE); char('/'); monthNumber(); char('/'); year() + } + check(spacePaddedDays.format(LocalDate(2021, 1, 6)) == " 6/01/2021") + check(spacePaddedDays.format(LocalDate(2021, 1, 31)) == "31/01/2021") + } + + @Test + fun dayOfWeek() { + val format = LocalDate.Format { + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); char('/'); monthNumber(); char('/'); year() + } + check(format.format(LocalDate(2021, 1, 13)) == "Wed 13/01/2021") + check(format.format(LocalDate(2021, 12, 13)) == "Mon 13/12/2021") + } + + @Test + fun date() { + val format = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + alternativeParsing({ char('t') }) { char('T') } + hour(); char(':'); minute() + } + check(format.format(LocalDateTime(2021, 1, 13, 14, 30)) == "2021-01-13T14:30") + } + class MonthNamesSamples { @Test fun usage() { From ba55ab6f9155cae5203ecbb2c6fccf4c6ffe1786 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 15 Apr 2024 15:58:15 +0200 Subject: [PATCH 10/35] Add samples for format builders --- .../src/format/DateTimeFormatBuilder.kt | 39 ++++++++------ .../format/DateTimeComponentsFormatSamples.kt | 46 ++++++++++++++++ .../format/LocalDateTimeFormatSamples.kt | 27 ++++++++++ .../samples/format/LocalTimeFormatSamples.kt | 54 +++++++++++++++++++ .../samples/format/UtcOffsetFormatSamples.kt | 43 +++++++++++++++ 5 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 core/common/test/samples/format/DateTimeComponentsFormatSamples.kt create mode 100644 core/common/test/samples/format/LocalDateTimeFormatSamples.kt create mode 100644 core/common/test/samples/format/LocalTimeFormatSamples.kt create mode 100644 core/common/test/samples/format/UtcOffsetFormatSamples.kt diff --git a/core/common/src/format/DateTimeFormatBuilder.kt b/core/common/src/format/DateTimeFormatBuilder.kt index 831f05ee1..cca7943e2 100644 --- a/core/common/src/format/DateTimeFormatBuilder.kt +++ b/core/common/src/format/DateTimeFormatBuilder.kt @@ -106,6 +106,8 @@ public sealed interface DateTimeFormatBuilder { * The hour of the day, from 0 to 23. * * By default, it's zero-padded to two digits, but this can be changed with [padding]. + * + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.hhmmss */ public fun hour(padding: Padding = Padding.ZERO) @@ -122,6 +124,7 @@ public sealed interface DateTimeFormatBuilder { * By default, it's zero-padded to two digits, but this can be changed with [padding]. * * @see [amPmMarker] + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.amPm */ public fun amPmHour(padding: Padding = Padding.ZERO) @@ -134,6 +137,7 @@ public sealed interface DateTimeFormatBuilder { * [IllegalArgumentException] is thrown if either [am] or [pm] is empty or if they are equal. * * @see [amPmHour] + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.amPm */ public fun amPmMarker(am: String, pm: String) @@ -141,6 +145,8 @@ public sealed interface DateTimeFormatBuilder { * The minute of hour. * * By default, it's zero-padded to two digits, but this can be changed with [padding]. + * + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.hhmmss */ public fun minute(padding: Padding = Padding.ZERO) @@ -150,6 +156,8 @@ public sealed interface DateTimeFormatBuilder { * By default, it's zero-padded to two digits, but this can be changed with [padding]. * * This field has the default value of 0. If you want to omit it, use [optional]. + * + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.hhmmss */ public fun second(padding: Padding = Padding.ZERO) @@ -169,6 +177,8 @@ public sealed interface DateTimeFormatBuilder { * part. * * @throws IllegalArgumentException if [minLength] is greater than [maxLength] or if either is not in the range 1..9. + * + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.hhmmss */ public fun secondFraction(minLength: Int = 1, maxLength: Int = 9) @@ -189,6 +199,7 @@ public sealed interface DateTimeFormatBuilder { * @throws IllegalArgumentException if [fixedLength] is not in the range 1..9. * * @see secondFraction that accepts two parameters. + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.fixedLengthSecondFraction */ public fun secondFraction(fixedLength: Int) { secondFraction(fixedLength, fixedLength) @@ -197,10 +208,7 @@ public sealed interface DateTimeFormatBuilder { /** * An existing [DateTimeFormat] for the time part. * - * Example: - * ``` - * time(LocalTime.Formats.ISO) - * ``` + * @sample kotlinx.datetime.test.samples.format.LocalTimeFormatSamples.time */ public fun time(format: DateTimeFormat) } @@ -212,10 +220,7 @@ public sealed interface DateTimeFormatBuilder { /** * An existing [DateTimeFormat] for the date-time part. * - * Example: - * ``` - * dateTime(LocalDateTime.Formats.ISO) - * ``` + * @sample kotlinx.datetime.test.samples.format.LocalDateTimeFormatSamples.dateTime */ public fun dateTime(format: DateTimeFormat) } @@ -230,6 +235,8 @@ public sealed interface DateTimeFormatBuilder { * By default, it's zero-padded to two digits, but this can be changed with [padding]. * * This field has the default value of 0. If you want to omit it, use [optional]. + * + * @sample kotlinx.datetime.test.samples.format.UtcOffsetFormatSamples.isoOrGmt */ public fun offsetHours(padding: Padding = Padding.ZERO) @@ -239,6 +246,8 @@ public sealed interface DateTimeFormatBuilder { * By default, it's zero-padded to two digits, but this can be changed with [padding]. * * This field has the default value of 0. If you want to omit it, use [optional]. + * + * @sample kotlinx.datetime.test.samples.format.UtcOffsetFormatSamples.isoOrGmt */ public fun offsetMinutesOfHour(padding: Padding = Padding.ZERO) @@ -248,16 +257,15 @@ public sealed interface DateTimeFormatBuilder { * By default, it's zero-padded to two digits, but this can be changed with [padding]. * * This field has the default value of 0. If you want to omit it, use [optional]. + * + * @sample kotlinx.datetime.test.samples.format.UtcOffsetFormatSamples.isoOrGmt */ public fun offsetSecondsOfMinute(padding: Padding = Padding.ZERO) /** * An existing [DateTimeFormat] for the UTC offset part. * - * Example: - * ``` - * offset(UtcOffset.Formats.FOUR_DIGITS) - * ``` + * @sample kotlinx.datetime.test.samples.format.UtcOffsetFormatSamples.offset */ public fun offset(format: DateTimeFormat) } @@ -272,16 +280,15 @@ public sealed interface DateTimeFormatBuilder { * * When formatting, the timezone identifier is supplied as is, without any validation. * On parsing, [TimeZone.availableZoneIds] is used to validate the identifier. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsFormatSamples.timeZoneId */ public fun timeZoneId() /** * An existing [DateTimeFormat]. * - * Example: - * ``` - * dateTimeComponents(DateTimeComponents.Formats.RFC_1123) - * ``` + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsFormatSamples.dateTimeComponents */ public fun dateTimeComponents(format: DateTimeFormat) } diff --git a/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt new file mode 100644 index 000000000..a418a4ef3 --- /dev/null +++ b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class DateTimeComponentsFormatSamples { + @Test + fun timeZoneId() { + val format = DateTimeComponents.Format { + dateTime(LocalDateTime.Formats.ISO) + char('[') + timeZoneId() + char(']') + } + val formatted = format.format { + setDateTime(LocalDate(2021, 1, 13).atTime(9, 34, 58, 120_000_000)) + timeZoneId = "Europe/Paris" + } + check(formatted == "2021-01-13T09:34:58.12[Europe/Paris]") + val parsed = format.parse("2021-01-13T09:34:58.12[Europe/Paris]") + check(parsed.toLocalDateTime() == LocalDate(2021, 1, 13).atTime(9, 34, 58, 120_000_000)) + check(parsed.timeZoneId == "Europe/Paris") + } + + @Test + fun dateTimeComponents() { + val format = DateTimeComponents.Format { + char('{') + dateTimeComponents(DateTimeComponents.Formats.RFC_1123) + char('}') + } + val formatted = format.format { + setDateTimeOffset( + LocalDate(2021, 1, 13).atTime(9, 34, 58, 120_000_000), + UtcOffset(hours = 3, minutes = 30) + ) + } + check(formatted == "{Wed, 13 Jan 2021 09:34:58 +0330}") + } +} diff --git a/core/common/test/samples/format/LocalDateTimeFormatSamples.kt b/core/common/test/samples/format/LocalDateTimeFormatSamples.kt new file mode 100644 index 000000000..6cd75bdcc --- /dev/null +++ b/core/common/test/samples/format/LocalDateTimeFormatSamples.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class LocalDateTimeFormatSamples { + @Test + fun dateTime() { + val format = DateTimeComponents.Format { + dateTime(LocalDateTime.Formats.ISO) + offset(UtcOffset.Formats.FOUR_DIGITS) + } + val formatted = format.format { + setDateTimeOffset( + LocalDate(2021, 1, 13).atTime(9, 34, 58, 120_000_000), + UtcOffset(hours = 2) + ) + } + check(formatted == "2021-01-13T09:34:58.12+0200") + } +} diff --git a/core/common/test/samples/format/LocalTimeFormatSamples.kt b/core/common/test/samples/format/LocalTimeFormatSamples.kt new file mode 100644 index 000000000..67459f18f --- /dev/null +++ b/core/common/test/samples/format/LocalTimeFormatSamples.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class LocalTimeFormatSamples { + @Test + fun hhmmss() { + // format the local time as a single number + val format = LocalTime.Format { + hour(); minute(); second() + optional { char('.'); secondFraction(1, 9) } + } + val formatted = format.format(LocalTime(9, 34, 58, 120_000_000)) + check(formatted == "093458.12") + } + + @Test + fun amPm() { + val format = LocalTime.Format { + amPmHour(); char(':'); minute(); char(':'); second() + char(' '); amPmMarker("AM", "PM") + } + val formatted = format.format(LocalTime(9, 34, 58, 120_000_000)) + check(formatted == "09:34:58 AM") + } + + @Test + fun fixedLengthSecondFraction() { + val format = LocalTime.Format { + hour(); char(':'); minute(); char(':'); second() + char('.'); secondFraction(fixedLength = 3) + } + val formatted = format.format(LocalTime(9, 34, 58, 120_000_000)) + check(formatted == "09:34:58.120") + } + + @Test + fun time() { + val format = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + time(LocalTime.Formats.ISO) + } + val formatted = format.format(LocalDateTime(2021, 1, 13, 9, 34, 58, 120_000_000)) + check(formatted == "2021-01-13 09:34:58.12") + } +} diff --git a/core/common/test/samples/format/UtcOffsetFormatSamples.kt b/core/common/test/samples/format/UtcOffsetFormatSamples.kt new file mode 100644 index 000000000..6a300c84e --- /dev/null +++ b/core/common/test/samples/format/UtcOffsetFormatSamples.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class UtcOffsetFormatSamples { + @Test + fun isoOrGmt() { + val format = UtcOffset.Format { + // if the offset is zero, `GMT` is printed + optional("GMT") { + offsetHours(); char(':'); offsetMinutesOfHour() + // if seconds are zero, they are omitted + optional { char(':'); offsetSecondsOfMinute() } + } + } + check(format.format(UtcOffset.ZERO) == "GMT") + check(format.format(UtcOffset(hours = -2)) == "-02:00") + check(format.format(UtcOffset(hours = -2, minutes = -30)) == "-02:30") + check(format.format(UtcOffset(hours = -2, minutes = -30, seconds = -59)) == "-02:30:59") + } + + @Test + fun offset() { + val format = DateTimeComponents.Format { + dateTime(LocalDateTime.Formats.ISO) + offset(UtcOffset.Formats.FOUR_DIGITS) + } + val formatted = format.format { + setDateTimeOffset( + LocalDate(2021, 1, 13).atTime(9, 34, 58, 120_000_000), + UtcOffset(hours = 2) + ) + } + check(formatted == "2021-01-13T09:34:58.12+0200") + } +} From 85ab1023c05049558f4eae06bf9f4177925a4664 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 16 Apr 2024 14:21:01 +0200 Subject: [PATCH 11/35] Samples for Date(Time)Period and Clock --- core/common/src/Clock.kt | 16 +- core/common/src/DateTimePeriod.kt | 52 ++++--- core/common/test/samples/ClockSamples.kt | 28 ++++ .../test/samples/DateTimePeriodSamples.kt | 137 ++++++++++++++++++ 4 files changed, 205 insertions(+), 28 deletions(-) create mode 100644 core/common/test/samples/ClockSamples.kt create mode 100644 core/common/test/samples/DateTimePeriodSamples.kt diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 882ac0f88..0e59ed673 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -22,7 +22,8 @@ public interface Clock { * Returns the [Instant] corresponding to the current time, according to this clock. * * It is not guaranteed that calling [now] later will return a larger [Instant]. - * In particular, for [System], violations of this are completely expected and must be taken into account. + * In particular, for [System] it is completely expected that the opposite will happen, + * and it must be taken into account. * See the documentation of [System] for details. * * Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling @@ -46,6 +47,8 @@ public interface Clock { * * For improved testability, one could avoid using [Clock.System] directly in the implementation, * instead passing a [Clock] explicitly. + * + * @sample kotlinx.datetime.test.samples.ClockSamples.system */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() @@ -59,14 +62,9 @@ public interface Clock { /** * Returns the current date at the given [time zone][timeZone], according to [this Clock][this]. * - * The time zone is important because the current date is not the same in all time zones at the same time. - * ``` - * val clock = object : Clock { - * override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z") - * } - * val dateInUTC = clock.todayIn(TimeZone.UTC) // 2020-01-01 - * val dateInNewYork = clock.todayIn(TimeZone.of("America/New_York")) // 2019-12-31 - * ``` + * The time zone is important because the current date is not the same in all time zones at the same instant. + * + * @sample kotlinx.datetime.test.samples.ClockSamples.todayIn */ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = now().toLocalDateTime(timeZone).date diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 17f9c46fc..49c57cef5 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -52,25 +52,20 @@ import kotlinx.serialization.Serializable * will be returned if all time components happen to be zero. * * A `DateTimePeriod` can be constructed using the constructor function with the same name. - * - * ``` - * val dateTimePeriod = DateTimePeriod(months = 24, days = -3) - * val datePeriod = dateTimePeriod as DatePeriod // the same as DatePeriod(years = 2, days = -3) - * ``` + * See sample 1. * * [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the * ISO 8601 extended format. - * - * ``` - * val dateTimePeriod = DateTimePeriod.parse("P1Y2M6DT13H1S") // 1 year, 2 months, 6 days, 13 hours, 1 second - * val string = dateTimePeriod.toString() // "P1Y2M6DT13H1S" - * ``` + * See sample 2. * * `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]). * * Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]: * - [DateTimePeriodIso8601Serializer] for the ISO 8601 format; * - [DateTimePeriodComponentSerializer] for an object with components. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.construction + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.simpleParsingAndFormatting */ @Serializable(with = DateTimePeriodIso8601Serializer::class) // TODO: could be error-prone without explicitly named params @@ -82,17 +77,23 @@ public sealed class DateTimePeriod { * * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details. * Also, this field does not overflow into months, so values larger than 31 can be present. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ public abstract val days: Int internal abstract val totalNanoseconds: Long /** * The number of whole years. Can be negative. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ 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)`. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ public val months: Int get() = totalMonths % 12 @@ -100,22 +101,30 @@ public sealed class DateTimePeriod { * The number of whole hours in this period. Can be negative. * * This field does not overflow into days, so values larger than 23 or smaller than -23 can be present. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ 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)`. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ 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)`. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ 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)`. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ public open val nanoseconds: Int get() = (totalNanoseconds % NANOS_PER_ONE).toInt() @@ -136,6 +145,7 @@ public sealed class DateTimePeriod { * minus four seconds, minus 123456789 nanoseconds; * * @see DateTimePeriod.parse for the detailed description of the format. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.toStringSample */ override fun toString(): String = buildString { val sign = if (allNonpositive()) { append('-'); -1 } else 1 @@ -215,6 +225,7 @@ public sealed class DateTimePeriod { * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are * exceeded. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parsing */ public fun parse(text: String): DateTimePeriod { fun parseException(message: String, position: Int): Nothing = @@ -400,14 +411,10 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * are not zero, and [DatePeriodIso8601Serializer] and [DatePeriodComponentSerializer], mirroring those of * [DateTimePeriod]. * - * ``` - * val datePeriod1 = DatePeriod(years = 1, days = 3) - * val string = datePeriod1.toString() // "P1Y3D" - * val datePeriod2 = DatePeriod.parse(string) // 1 year and 3 days - * ``` - * * `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. + * + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting */ @Serializable(with = DatePeriodIso8601Serializer::class) public class DatePeriod internal constructor( @@ -428,6 +435,7 @@ public class DatePeriod internal constructor( * For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`. * * @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int]. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.construction */ public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days) // avoiding excessive computations @@ -455,6 +463,7 @@ public class DatePeriod internal constructor( * or any time components are not zero. * * @see DateTimePeriod.parse + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.parsing */ public fun parse(text: String): DatePeriod = when (val period = DateTimePeriod.parse(text)) { @@ -521,6 +530,7 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos * @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]. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.constructorFunction */ public fun DateTimePeriod( years: Int = 0, @@ -544,9 +554,7 @@ public fun DateTimePeriod( * whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours. * See [DateTimeUnit.DayBased] for details. * - * ``` - * 2.days.toDateTimePeriod() // 0 days, 48 hours - * ``` + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.durationToDateTimePeriod */ // TODO: maybe it's more consistent to throw here on overflow? public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds) @@ -554,6 +562,9 @@ public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(tot /** * Adds two [DateTimePeriod] instances. * + * **Pitfall**: given three instants, adding together the periods between the first and the second and between the + * second and the third *does not* necessarily equal the period between the first and the third. + * * @throws DateTimeArithmeticException if arithmetic overflow happens. */ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = buildDateTimePeriod( @@ -565,6 +576,9 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = /** * Adds two [DatePeriod] instances. * + * **Pitfall**: given three dates, adding together the periods between the first and the second and between the + * second and the third *does not* necessarily equal the period between the first and the third. + * * @throws DateTimeArithmeticException if arithmetic overflow happens. */ public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( diff --git a/core/common/test/samples/ClockSamples.kt b/core/common/test/samples/ClockSamples.kt new file mode 100644 index 000000000..cd37fadc2 --- /dev/null +++ b/core/common/test/samples/ClockSamples.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.test.* + +class ClockSamples { + @Test + fun system() { + val zone = TimeZone.of("Europe/Berlin") + val currentInstant = Clock.System.now() + val currentLocalDateTime = currentInstant.toLocalDateTime(zone) + currentLocalDateTime.toString() // show the current date and time, according to the OS + } + + @Test + fun todayIn() { + val clock = object : Clock { + override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z") + } + check(clock.todayIn(TimeZone.UTC) == LocalDate(2020, 1, 1)) + check(clock.todayIn(TimeZone.of("America/New_York")) == LocalDate(2019, 12, 31)) + } +} diff --git a/core/common/test/samples/DateTimePeriodSamples.kt b/core/common/test/samples/DateTimePeriodSamples.kt new file mode 100644 index 000000000..c7e04d85a --- /dev/null +++ b/core/common/test/samples/DateTimePeriodSamples.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.test.* +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes + +class DateTimePeriodSamples { + + @Test + fun construction() { + val period = DateTimePeriod(years = 5, months = 21, days = 36, seconds = 3601) + check(period.years == 6) // 5 years + (21 months / 12) + check(period.months == 9) // 21 months % 12 + check(period.days == 36) + check(period.hours == 1) // 3601 seconds / 3600 + check(period.minutes == 0) + check(period.seconds == 1) + check(period.nanoseconds == 0) + check(DateTimePeriod(months = -24) as DatePeriod == DatePeriod(years = -2)) + } + + @Test + fun simpleParsingAndFormatting() { + val string = "-P2M-3DT-4H" + val period = DateTimePeriod.parse(string) + check(period.toString() == "P-2M3DT4H") + } + + @Test + fun valueNormalization() { + val period = DateTimePeriod( + years = -12, months = 122, days = -1440, + hours = 400, minutes = -80, seconds = 123, nanoseconds = -123456789 + ) + // years and months have the same sign and are normalized together: + check(period.years == -1) // -12 years + (122 months % 12) + 1 year + check(period.months == -10) // (122 months % 12) - 1 year + // days are separate from months and are not normalized: + check(period.days == -1440) + // hours, minutes, seconds, and nanoseconds are normalized together and have the same sign: + check(period.hours == 398) // 400 hours - 2 hours' worth of minutes + check(period.minutes == 42) // -80 minutes + 2 hours' worth of minutes + 120 seconds + check(period.seconds == 2) // 123 seconds - 2 minutes' worth of seconds - 1 second + check(period.nanoseconds == 876543211) // -123456789 nanoseconds + 1 second + } + + @Test + fun toStringSample() { + check(DateTimePeriod(years = 1, months = 2, days = 3, hours = 4, minutes = 5, seconds = 6, nanoseconds = 7).toString() == "P1Y2M3DT4H5M6.000000007S") + check(DateTimePeriod(months = 14, days = -16, hours = 5).toString() == "P1Y2M-16DT5H") + check(DateTimePeriod(months = -2, days = -16, hours = -5).toString() == "-P2M16DT5H") + } + + @Test + fun parsing() { + DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S").apply { + check(years == 1) + check(months == 2) + check(days == 3) + check(hours == 4) + check(minutes == 5) + check(seconds == 6) + check(nanoseconds == 7) + } + DateTimePeriod.parse("P14M-16DT5H").apply { + check(years == 1) + check(months == 2) + check(days == -16) + check(hours == 5) + } + DateTimePeriod.parse("-P2M16DT5H").apply { + check(years == 0) + check(months == -2) + check(days == -16) + check(hours == -5) + } + } + + @Test + fun constructorFunction() { + val dateTimePeriod = DateTimePeriod(months = 16, days = -60, hours = 16, minutes = -61) + check(dateTimePeriod.years == 1) // months overflowed to years + check(dateTimePeriod.months == 4) // 16 months % 12 + check(dateTimePeriod.days == -60) // days are separate from months and are not normalized + check(dateTimePeriod.hours == 14) // the negative minutes overflowed to hours + check(dateTimePeriod.minutes == 59) // (-61 minutes) + (2 hours) * (60 minutes / hour) + val datePeriod = DateTimePeriod(months = 15, days = 3, hours = 2, minutes = -120) + check(datePeriod is DatePeriod) // the time components are zero + } + + @Test + fun durationToDateTimePeriod() { + check(130.minutes.toDateTimePeriod() == DateTimePeriod(minutes = 130)) + check(2.days.toDateTimePeriod() == DateTimePeriod(days = 0, hours = 48)) + } + + class DatePeriodSamples { + + @Test + fun simpleParsingAndFormatting() { + val datePeriod1 = DatePeriod(years = 1, days = 3) + val string = datePeriod1.toString() + check(string == "P1Y3D") + val datePeriod2 = DatePeriod.parse(string) + check(datePeriod1 == datePeriod2) + } + + @Test + fun construction() { + val datePeriod = DatePeriod(years = 1, months = 16, days = 60) + check(datePeriod.years == 2) // 1 year + (16 months / 12) + check(datePeriod.months == 4) // 16 months % 12 + check(datePeriod.days == 60) + // the time components are always zero: + check(datePeriod.hours == 0) + check(datePeriod.minutes == 0) + check(datePeriod.seconds == 0) + check(datePeriod.nanoseconds == 0) + } + + @Test + fun parsing() { + // ISO duration strings are supported: + val datePeriod = DatePeriod.parse("P1Y16M60D") + check(datePeriod == DatePeriod(years = 2, months = 4, days = 60)) + // it's okay to have time components as long as they amount to zero in total: + val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M") + check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3)) + } + } +} From cccad0151b1ba67e9288a036fae08120fd5fb767 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 16 Apr 2024 16:19:42 +0200 Subject: [PATCH 12/35] fixup --- core/common/test/samples/TimeZoneSamples.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/test/samples/TimeZoneSamples.kt b/core/common/test/samples/TimeZoneSamples.kt index 2436082e0..d346eaa8b 100644 --- a/core/common/test/samples/TimeZoneSamples.kt +++ b/core/common/test/samples/TimeZoneSamples.kt @@ -61,6 +61,7 @@ class TimeZoneSamples { } return "[$formattedTime] $message" } + // Outputs a text like `[2024-06-02 08:30:02.515+0200] Starting the application` logEntry("Starting the application") } @@ -173,6 +174,7 @@ class TimeZoneSamples { check(instant == Instant.parse("2023-06-02T11:00:00Z")) } + @Ignore // fails on Windows; TODO investigate @Test fun atStartOfDayIn() { val zone = TimeZone.of("America/Cuiaba") From f3d4653b42cac415df3339eac892b00d5bbf7233 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 16 Apr 2024 16:36:55 +0200 Subject: [PATCH 13/35] More samples; only LocalDate and Instant left --- core/common/src/DateTimeUnit.kt | 20 +- core/common/src/DayOfWeek.kt | 5 + core/common/src/LocalDateTime.kt | 126 +++++++---- core/common/test/samples/ClockSamples.kt | 2 +- .../test/samples/DateTimeUnitSamples.kt | 52 +++++ core/common/test/samples/DayOfWeekSamples.kt | 49 ++++ .../test/samples/LocalDateTimeSamples.kt | 209 ++++++++++++++++++ 7 files changed, 418 insertions(+), 45 deletions(-) create mode 100644 core/common/test/samples/DateTimeUnitSamples.kt create mode 100644 core/common/test/samples/DayOfWeekSamples.kt create mode 100644 core/common/test/samples/LocalDateTimeSamples.kt diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 576b61971..a17c29337 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -56,6 +56,8 @@ import kotlin.time.Duration.Companion.nanoseconds * [DateTimeUnitSerializer], [DateBasedDateTimeUnitSerializer], [DayBasedDateTimeUnitSerializer], * [MonthBasedDateTimeUnitSerializer], and [TimeBasedDateTimeUnitSerializer] are provided, with varying levels of * specificity of the type they handle. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.construction */ @Serializable(with = DateTimeUnitSerializer::class) public sealed class DateTimeUnit { @@ -63,11 +65,8 @@ public sealed class DateTimeUnit { /** * Produces a date-time unit that is a multiple of this unit times the specified integer [scalar] value. * - * ``` - * val quarter = DateTimeUnit.MONTH * 3 - * ``` - * * @throws ArithmeticException if the result overflows. + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.multiplication */ public abstract operator fun times(scalar: Int): DateTimeUnit @@ -78,11 +77,14 @@ public sealed class DateTimeUnit { * Any such unit can be represented as some fixed number of nanoseconds. * * @see DateTimeUnit for a description of date-time units in general. + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.timeBasedUnit */ @Serializable(with = TimeBasedDateTimeUnitSerializer::class) public class TimeBased( /** * The length of this unit in nanoseconds. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.timeBasedUnit */ public val nanoseconds: Long ) : DateTimeUnit() { @@ -124,6 +126,8 @@ public sealed class DateTimeUnit { /** * The length of this unit as a [Duration]. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.timeBasedUnit */ public val duration: Duration get() = nanoseconds.nanoseconds @@ -144,6 +148,8 @@ public sealed class DateTimeUnit { * the operation with the date component of these `LocalDateTime` values. * * @see DateTimeUnit for a description of date-time units in general. + * @see DateTimeUnit.DayBased for specifically day-based units. + * @see DateTimeUnit.MonthBased for specifically month-based units. */ @Serializable(with = DateBasedDateTimeUnitSerializer::class) public sealed class DateBased : DateTimeUnit() { @@ -167,11 +173,14 @@ public sealed class DateTimeUnit { * between the two date-times. * * @see DateTimeUnit for a description of date-time units in general. + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.dayBasedUnit */ @Serializable(with = DayBasedDateTimeUnitSerializer::class) public class DayBased( /** * The length of this unit in days. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.dayBasedUnit */ public val days: Int ) : DateBased() { @@ -198,11 +207,14 @@ public sealed class DateTimeUnit { * Since different months have different number of days, a `MonthBased`-unit cannot be expressed a multiple of some [DayBased]-unit. * * @see DateTimeUnit for a description of date-time units in general. + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.monthBasedUnit */ @Serializable(with = MonthBasedDateTimeUnitSerializer::class) public class MonthBased( /** * The length of this unit in months. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.monthBasedUnit */ public val months: Int ) : DateBased() { diff --git a/core/common/src/DayOfWeek.kt b/core/common/src/DayOfWeek.kt index d664174fb..32c18f367 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -10,6 +10,8 @@ package kotlinx.datetime * * Usually acquired from [LocalDate.dayOfWeek], but can be constructed using the `DayOfWeek` factory function that * accepts the ISO 8601 day number. This number can be obtained from the [isoDayNumber] property. + * + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.usage */ public expect enum class DayOfWeek { MONDAY, @@ -23,6 +25,8 @@ public expect enum class DayOfWeek { /** * The ISO 8601 number of the given day of the week. Monday is 1, Sunday is 7. + * + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.isoDayNumber */ public val DayOfWeek.isoDayNumber: Int get() = ordinal + 1 @@ -30,6 +34,7 @@ public val DayOfWeek.isoDayNumber: Int get() = ordinal + 1 * Returns the [DayOfWeek] instance for the given ISO 8601 week day number. Monday is 1, Sunday is 7. * * @throws IllegalArgumentException if the day number is not in the range 1..7 + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.constructorFunction */ public fun DayOfWeek(isoDayNumber: Int): DayOfWeek { require(isoDayNumber in 1..7) { "Expected ISO day-of-week number in 1..7, got $isoDayNumber" } diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 2d937d514..d63debc83 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -69,41 +69,18 @@ import kotlinx.serialization.Serializable * The recommended pattern is to convert a [LocalDateTime] to [Instant] as soon as possible (see * [LocalDateTime.toInstant]) and work with [Instant] values instead. * - * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor: - * - * ``` - * val date = LocalDate(2021, 3, 27) - * val time = LocalTime(hour = 2, minute = 16, second = 20) - * LocalDateTime(date, time) - * ``` + * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor. + * See sample 1. * * Some additional constructors that accept the date's and time's fields directly are provided for convenience. - * - * ``` - * LocalDateTime(year = 2021, monthNumber = 3, dayOfMonth = 27, hour = 2, minute = 16, second = 20) - * LocalDateTime( - * year = 2021, month = Month.MARCH, dayOfMonth = 27, - * hour = 2, minute = 16, second = 20, nanosecond = 999_999_999 - * ) - * ``` + * See sample 2. * * [parse] and [toString] methods can be used to obtain a [LocalDateTime] from and convert it to a string in the * ISO 8601 extended format (for example, `2023-01-02T22:35:01`). - * - * ``` - * LocalDateTime.parse("2023-01-02T22:35:01").toString() // 2023-01-02T22:35:01 - * ``` + * See sample 3. * * [parse] and [LocalDateTime.format] both support custom formats created with [Format] or defined in [Formats]. - * - * ``` - * val customFormat = LocalDateTime.Format { - * date(LocalDate.Formats.ISO) - * char(' ') - * time(LocalTime.Formats.ISO) - * } - * LocalDateTime.parse("2023-01-02 22:35:01", customFormat).format(customFormat) // 2023-01-02 22:35:01 - * ``` + * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format, @@ -112,6 +89,10 @@ import kotlinx.serialization.Serializable * @see LocalDate for only the date part of the date/time value. * @see LocalTime for only the time part of the date/time value. * @see Instant for the representation of a specific moment in time independent of a time zone. + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.fromDateAndTime + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.alternativeConstruction + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.customFormat */ @Serializable(with = LocalDateTimeIso8601Serializer::class) public expect class LocalDateTime : Comparable { @@ -130,6 +111,8 @@ public expect class LocalDateTime : Comparable { * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDateTime] are * exceeded. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoDateTimeFormat()): LocalDateTime @@ -156,6 +139,8 @@ public expect class LocalDateTime : Comparable { * There is a collection of predefined formats in [LocalDateTime.Formats]. * * @throws IllegalArgumentException if parsing using this format is ambiguous. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.customFormat */ @Suppress("FunctionName") public fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat @@ -186,6 +171,8 @@ public expect class LocalDateTime : Comparable { * Fractional parts of the second are included if non-zero. * * Guaranteed to parse all strings that [LocalDateTime.toString] produces. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.Formats.iso */ public val ISO: DateTimeFormat } @@ -207,6 +194,8 @@ public expect class LocalDateTime : Comparable { * * @throws IllegalArgumentException if any parameter is out of range, * or if [dayOfMonth] is invalid for the given [monthNumber] and [year]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunctionWithMonthNumber */ public constructor( year: Int, @@ -233,6 +222,8 @@ public expect class LocalDateTime : Comparable { * * @throws IllegalArgumentException if any parameter is out of range, * or if [dayOfMonth] is invalid for the given [month] and [year]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunction */ public constructor( year: Int, @@ -246,43 +237,93 @@ public expect class LocalDateTime : Comparable { /** * Constructs a [LocalDateTime] instance by combining the given [date] and [time] parts. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.fromDateAndTime */ public constructor(date: LocalDate, time: LocalTime) - /** Returns the year component of the date. */ + /** + * Returns the year component of the [date]. Can be negative. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val year: Int - /** Returns the number-of-the-month (1..12) component of the date. */ + /** + * Returns the number-of-the-month (1..12) component of the [date]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val monthNumber: Int - /** Returns the month ([Month]) component of the date. */ + /** + * Returns the month ([Month]) component of the [date]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val month: Month - /** Returns the day-of-month component of the date. */ + /** + * Returns the day-of-month (`1..31`) component of the [date]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val dayOfMonth: Int - /** Returns the day-of-week component of the date. */ + /** + * Returns the day-of-week component of the [date]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val dayOfWeek: DayOfWeek - /** Returns the 1-based day-of-year component of the date. */ + /** + * Returns the 1-based day-of-year component of the [date]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateComponents + */ public val dayOfYear: Int - /** Returns the hour-of-day time component of this date/time value. */ + /** + * Returns the hour-of-day (`0..59`) [time] component of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.timeComponents + */ public val hour: Int - /** Returns the minute-of-hour time component of this date/time value. */ + /** + * Returns the minute-of-hour (`0..59`) [time] component of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.timeComponents + */ public val minute: Int - /** Returns the second-of-minute time component of this date/time value. */ + /** + * Returns the second-of-minute (`0..59`) [time] component of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.timeComponents + */ public val second: Int - /** Returns the nanosecond-of-second time component of this date/time value. */ + /** + * Returns the nanosecond-of-second (`0..999_999_999`) [time] component of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.timeComponents + */ public val nanosecond: Int - /** Returns the date part of this date/time value. */ + /** + * Returns the date part of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateAndTime + */ public val date: LocalDate - /** Returns the time part of this date/time value. */ + /** + * Returns the time part of this date/time value. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.dateAndTime + */ public val time: LocalTime /** @@ -300,6 +341,8 @@ public expect class LocalDateTime : Comparable { * val ldt2 = Clock.System.now().toLocalDateTime(zone) // 2021-10-31T02:01:20 * ldt2 > ldt1 // returns `false` * ``` + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.compareToSample */ public override operator fun compareTo(other: LocalDateTime): Int @@ -321,6 +364,7 @@ public expect class LocalDateTime : Comparable { * even if they are zero, and will not add trailing zeros to the fractional part of the second for readability. * @see parse for the dual operation: obtaining [LocalDateTime] from a string. * @see LocalDateTime.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.toStringSample */ public override fun toString(): String } @@ -330,6 +374,8 @@ public expect class LocalDateTime : Comparable { * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. * * See [LocalDateTime.Formats] and [LocalDateTime.Format] for predefined and custom formats. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.formatting */ public fun LocalDateTime.format(format: DateTimeFormat): String = format.format(this) diff --git a/core/common/test/samples/ClockSamples.kt b/core/common/test/samples/ClockSamples.kt index cd37fadc2..0687700cb 100644 --- a/core/common/test/samples/ClockSamples.kt +++ b/core/common/test/samples/ClockSamples.kt @@ -20,7 +20,7 @@ class ClockSamples { @Test fun todayIn() { val clock = object : Clock { - override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z") + override fun now(): Instant = Instant.parse("2020-01-01T02:00:00Z") } check(clock.todayIn(TimeZone.UTC) == LocalDate(2020, 1, 1)) check(clock.todayIn(TimeZone.of("America/New_York")) == LocalDate(2019, 12, 31)) diff --git a/core/common/test/samples/DateTimeUnitSamples.kt b/core/common/test/samples/DateTimeUnitSamples.kt new file mode 100644 index 000000000..f335e8958 --- /dev/null +++ b/core/common/test/samples/DateTimeUnitSamples.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.test.* +import kotlin.time.Duration.Companion.hours + +class DateTimeUnitSamples { + @Test + fun construction() { + check(DateTimeUnit.HOUR == DateTimeUnit.TimeBased(nanoseconds = 60 * 60 * 1_000_000_000L)) + check(DateTimeUnit.WEEK == DateTimeUnit.DayBased(days = 7)) + check(DateTimeUnit.WEEK * 2 == DateTimeUnit.DayBased(days = 14)) + check(DateTimeUnit.CENTURY == DateTimeUnit.MonthBased(months = 12 * 100)) + } + + @Test + fun multiplication() { + val twoWeeks = DateTimeUnit.WEEK * 2 + check(twoWeeks.days == 14) + } + + @Test + fun timeBasedUnit() { + val halfDay = DateTimeUnit.TimeBased(nanoseconds = 12 * 60 * 60 * 1_000_000_000L) + check(halfDay.nanoseconds == 12 * 60 * 60 * 1_000_000_000L) + check(halfDay.duration == 12.hours) + check(halfDay == DateTimeUnit.HOUR * 12) + check(halfDay == DateTimeUnit.MINUTE * 720) + check(halfDay == DateTimeUnit.SECOND * 43_200) + } + + @Test + fun dayBasedUnit() { + val iteration = DateTimeUnit.DayBased(days = 14) + check(iteration.days == 14) + check(iteration == DateTimeUnit.DAY * 14) + check(iteration == DateTimeUnit.WEEK * 2) + } + + @Test + fun monthBasedUnit() { + val halfYear = DateTimeUnit.MonthBased(months = 6) + check(halfYear.months == 6) + check(halfYear == DateTimeUnit.QUARTER * 2) + check(halfYear == DateTimeUnit.MONTH * 6) + } +} diff --git a/core/common/test/samples/DayOfWeekSamples.kt b/core/common/test/samples/DayOfWeekSamples.kt new file mode 100644 index 000000000..8403f0bb1 --- /dev/null +++ b/core/common/test/samples/DayOfWeekSamples.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.test.* + +class DayOfWeekSamples { + + @Test + fun usage() { + val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) + when (today.dayOfWeek) { + DayOfWeek.MONDAY -> check(today.dayOfWeek.isoDayNumber == 1) + DayOfWeek.TUESDAY -> check(today.dayOfWeek.isoDayNumber == 2) + DayOfWeek.WEDNESDAY -> check(today.dayOfWeek.isoDayNumber == 3) + DayOfWeek.THURSDAY -> check(today.dayOfWeek.isoDayNumber == 4) + DayOfWeek.FRIDAY -> check(today.dayOfWeek.isoDayNumber == 5) + DayOfWeek.SATURDAY -> check(today.dayOfWeek.isoDayNumber == 6) + DayOfWeek.SUNDAY -> check(today.dayOfWeek.isoDayNumber == 7) + else -> TODO("A new day was added to the week?") + } + } + + @Test + fun isoDayNumber() { + check(DayOfWeek.MONDAY.isoDayNumber == 1) + check(DayOfWeek.TUESDAY.isoDayNumber == 2) + // ... + check(DayOfWeek.SUNDAY.isoDayNumber == 7) + } + + @Test + fun constructorFunction() { + check(DayOfWeek(isoDayNumber = 1) == DayOfWeek.MONDAY) + check(DayOfWeek(isoDayNumber = 2) == DayOfWeek.TUESDAY) + // ... + check(DayOfWeek(isoDayNumber = 7) == DayOfWeek.SUNDAY) + try { + DayOfWeek(0) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Expected + } + } +} diff --git a/core/common/test/samples/LocalDateTimeSamples.kt b/core/common/test/samples/LocalDateTimeSamples.kt new file mode 100644 index 000000000..fcdd5e52b --- /dev/null +++ b/core/common/test/samples/LocalDateTimeSamples.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class LocalDateTimeSamples { + + @Test + fun alternativeConstruction() { + val dateTime1 = LocalDateTime(year = 2021, monthNumber = 3, dayOfMonth = 27, hour = 2, minute = 16, second = 20) + val dateTime2 = LocalDateTime( + year = 2021, month = Month.MARCH, dayOfMonth = 27, + hour = 2, minute = 16, second = 20, nanosecond = 0 + ) + check(dateTime1 == dateTime2) + } + + @Test + fun simpleParsingAndFormatting() { + val dateTime = LocalDateTime.parse("2024-02-15T08:30:15.1234567") + check(dateTime == LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_456_700)) + val formatted = dateTime.toString() + check(formatted == "2024-02-15T08:30:15.123456700") + } + + @Test + fun parsing() { + check(LocalDateTime.parse("2024-02-15T08:30:15.123456789") == + LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_456_789)) + check(LocalDateTime.parse("2024-02-15T08:30") == + LocalDate(2024, 2, 15).atTime(8, 30)) + val customFormat = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second() + char(','); secondFraction(fixedLength = 3) + } + check(LocalDateTime.parse("2024-02-15 08:30:15,123", customFormat) == + LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_000_000)) + } + + @Test + fun customFormat() { + val customFormat = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second() + char(','); secondFraction(fixedLength = 3) + } + val dateTime = LocalDate(2024, 2, 15) + .atTime(8, 30, 15, 123_456_789) + check(dateTime.format(customFormat) == "2024-02-15 08:30:15,123") + check(dateTime.format(LocalDateTime.Formats.ISO) == "2024-02-15T08:30:15.123456789") + } + + @Test + fun constructorFunctionWithMonthNumber() { + val dateTime = LocalDateTime( + year = 2024, + monthNumber = 2, + dayOfMonth = 15, + hour = 16, + minute = 48, + second = 59, + nanosecond = 999_999_999, + ) + check(dateTime.date == LocalDate(2024, 2, 15)) + check(dateTime.time == LocalTime(16, 48, 59, 999_999_999)) + val dateTimeWithoutSeconds = LocalDateTime( + year = 2024, + monthNumber = 2, + dayOfMonth = 15, + hour = 16, + minute = 48, + ) + check(dateTimeWithoutSeconds.date == LocalDate(2024, 2, 15)) + check(dateTimeWithoutSeconds.time == LocalTime(16, 48)) + } + + @Test + fun constructorFunction() { + val dateTime = LocalDateTime( + year = 2024, + month = Month.FEBRUARY, + dayOfMonth = 15, + hour = 16, + minute = 48, + second = 59, + nanosecond = 999_999_999, + ) + check(dateTime.date == LocalDate(2024, Month.FEBRUARY, 15)) + check(dateTime.time == LocalTime(16, 48, 59, 999_999_999)) + val dateTimeWithoutSeconds = LocalDateTime( + year = 2024, + month = Month.FEBRUARY, + dayOfMonth = 15, + hour = 16, + minute = 48, + ) + check(dateTimeWithoutSeconds.date == LocalDate(2024, Month.FEBRUARY, 15)) + check(dateTimeWithoutSeconds.time == LocalTime(16, 48)) + } + + @Test + fun fromDateAndTime() { + val date = LocalDate(2024, 2, 15) + val time = LocalTime(16, 48) + val dateTime = LocalDateTime(date, time) + check(dateTime.date == date) + check(dateTime.time == time) + check(dateTime == date.atTime(time)) + check(dateTime == time.atDate(date)) + } + + @Test + fun dateComponents() { + val date = LocalDate(2024, 2, 15) + val time = LocalTime(hour = 16, minute = 48, second = 59, nanosecond = 999_999_999) + val dateTime = LocalDateTime(date, time) + check(dateTime.year == dateTime.date.year) + check(dateTime.month == dateTime.date.month) + check(dateTime.monthNumber == dateTime.date.monthNumber) + check(dateTime.dayOfMonth == dateTime.date.dayOfMonth) + check(dateTime.dayOfWeek == dateTime.date.dayOfWeek) + check(dateTime.dayOfYear == dateTime.date.dayOfYear) + } + + @Test + fun timeComponents() { + val date = LocalDate(2024, 2, 15) + val time = LocalTime(hour = 16, minute = 48, second = 59, nanosecond = 999_999_999) + val dateTime = LocalDateTime(date, time) + check(dateTime.hour == dateTime.time.hour) + check(dateTime.minute == dateTime.time.minute) + check(dateTime.second == dateTime.time.second) + check(dateTime.nanosecond == dateTime.time.nanosecond) + } + + @Test + fun dateAndTime() { + val date = LocalDate(2024, 2, 15) + val time = LocalTime(16, 48) + val dateTime = LocalDateTime(date, time) + check(dateTime.date == date) + check(dateTime.time == time) + } + + @Test + fun compareToSample() { + val date = LocalDate(2024, 2, 15) + val laterDate = LocalDate(2024, 2, 16) + check(date.atTime(hour = 23, minute = 59) < laterDate.atTime(hour = 0, minute = 0)) + check(date.atTime(hour = 8, minute = 30) < date.atTime(hour = 17, minute = 10)) + check(date.atTime(hour = 8, minute = 30) < date.atTime(hour = 8, minute = 31)) + check(date.atTime(hour = 8, minute = 30) < date.atTime(hour = 8, minute = 30, second = 1)) + check(date.atTime(hour = 8, minute = 30) < date.atTime(hour = 8, minute = 30, second = 0, nanosecond = 1)) + } + + @Test + fun toStringSample() { + check(LocalDate(2024, 2, 15).atTime(16, 48).toString() == "2024-02-15T16:48") + check(LocalDate(2024, 2, 15).atTime(16, 48, 15).toString() == "2024-02-15T16:48:15") + check(LocalDate(2024, 2, 15).atTime(16, 48, 15, 120_000_000).toString() == "2024-02-15T16:48:15.120") + } + + @Test + fun formatting() { + check(LocalDate(2024, 2, 15).atTime(16, 48).format(LocalDateTime.Formats.ISO) == "2024-02-15T16:48:00") + val customFormat = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute() + optional { + char(':'); second() + optional { + char('.'); secondFraction(minLength = 3) + } + } + } + val dateTime1 = LocalDate(2024, 2, 15).atTime(8, 30) + check(dateTime1.format(customFormat) == "2024-02-15 08:30") + val dateTime2 = LocalDate(2023, 12, 31).atTime(8, 30, 0, 120_000_000) + check(dateTime2.format(customFormat) == "2023-12-31 08:30:00.120") + } + + class Formats { + @Test + fun iso() { + val dateTime1 = LocalDate(2024, 2, 15) + .atTime(hour = 8, minute = 30, second = 15, nanosecond = 160_000_000) + val dateTime2 = LocalDate(2024, 2, 15) + .atTime(hour = 8, minute = 30, second = 15) + val dateTime3 = LocalDate(2024, 2, 15) + .atTime(hour = 8, minute = 30) + check(LocalDateTime.Formats.ISO.parse("2024-02-15T08:30:15.16") == dateTime1) + check(LocalDateTime.Formats.ISO.parse("2024-02-15T08:30:15") == dateTime2) + check(LocalDateTime.Formats.ISO.parse("2024-02-15T08:30") == dateTime3) + check(LocalDateTime.Formats.ISO.format(dateTime1) == "2024-02-15T08:30:15.16") + check(LocalDateTime.Formats.ISO.format(dateTime2) == "2024-02-15T08:30:15") + check(LocalDateTime.Formats.ISO.format(dateTime3) == "2024-02-15T08:30:00") + } + } +} From 77aff41833ebb85fed5fc4a9e76be48a3063f329 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 16 Apr 2024 19:31:32 +0200 Subject: [PATCH 14/35] Samples for LocalDate --- core/common/src/LocalDate.kt | 143 +++++----- core/common/test/samples/LocalDateSamples.kt | 274 +++++++++++++++++++ 2 files changed, 344 insertions(+), 73 deletions(-) create mode 100644 core/common/test/samples/LocalDateSamples.kt diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 87c9eaac6..a1eda54c8 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -32,44 +32,27 @@ import kotlinx.serialization.Serializable * ### Construction, serialization, and deserialization * * [LocalDate] can be constructed directly from its components, using the constructor. - * - * ``` - * LocalDate(year = 2023, monthNumber = 1, dayOfMonth = 2) == LocalDate(2023, Month.JANUARY, 2) - * ``` + * See sample 1. * * [fromEpochDays] can be used to obtain a [LocalDate] from the number of days since the epoch day `1970-01-01`; * [toEpochDays] is the inverse operation. - * - * ``` - * LocalDate.fromEpochDays(0) == LocalDate(1970, Month.JANUARY, 1) - * LocalDate(1970, Month.JANUARY, 31).toEpochDays() == 30 - * ``` + * See sample 2. * * [parse] and [toString] methods can be used to obtain a [LocalDate] from and convert it to a string in the * ISO 8601 extended format. - * - * ``` - * LocalDate.parse("2023-01-02") == LocalDate(2023, Month.JANUARY, 2) - * LocalDate(2023, Month.JANUARY, 2).toString() == "2023-01-02" - * ``` + * See sample 3. * * [parse] and [LocalDate.format] both support custom formats created with [Format] or defined in [Formats]. - * - * ``` - * val customFormat = LocalDate.Format { - * monthName(MonthNames.ENGLISH_ABBREVIATED) - * char(' ') - * dayOfMonth() - * char(' ') - * year() - * } - * LocalDate.parse("Jan 05 2020", customFormat) == LocalDate(2020, Month.JANUARY, 5) - * LocalDate(2020, Month.JANUARY, 5).format(customFormat) == "Jan 05 2020" - * ``` + * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: * - [LocalDateIso8601Serializer] for the ISO 8601 extended format, * - [LocalDateComponentSerializer] for an object with components. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunctionMonthNumber + * @sample kotlinx.datetime.test.samples.LocalDateSamples.fromAndToEpochDays + * @sample kotlinx.datetime.test.samples.LocalDateSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.LocalDateSamples.customFormat */ @Serializable(with = LocalDateIso8601Serializer::class) public expect class LocalDate : Comparable { @@ -86,6 +69,7 @@ public expect class LocalDate : Comparable { * * @see LocalDate.toString for formatting using the default format. * @see LocalDate.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoDateFormat()): LocalDate @@ -93,32 +77,21 @@ public expect class LocalDate : Comparable { * Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`. * * @throws IllegalArgumentException if the result exceeds the platform-specific boundaries of [LocalDate]. - * * @see LocalDate.toEpochDays + * @sample kotlinx.datetime.test.samples.LocalDateSamples.fromAndToEpochDays */ public fun fromEpochDays(epochDays: Int): LocalDate /** * Creates a new format for parsing and formatting [LocalDate] values. * - * Example: - * ``` - * // 2020 Jan 05 - * LocalDate.Format { - * year() - * char(' ') - * monthName(MonthNames.ENGLISH_ABBREVIATED) - * char(' ') - * dayOfMonth() - * } - * ``` - * * Only parsing and formatting of well-formed values is supported. If the input does not fit the boundaries * (for example, [dayOfMonth] is 31 for February), consider using [DateTimeComponents.Format] instead. * * There is a collection of predefined formats in [LocalDate.Formats]. * * @throws IllegalArgumentException if parsing using this format is ambiguous. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.customFormat */ @Suppress("FunctionName") public fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat @@ -146,6 +119,8 @@ public expect class LocalDate : Comparable { * - `+12020-08-30` * - `0000-08-30` * - `-0001-08-30` + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.Formats.iso */ public val ISO: DateTimeFormat @@ -157,6 +132,8 @@ public expect class LocalDate : Comparable { * - `+120200830` * - `00000830` * - `-00010830` + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.Formats.isoBasic */ public val ISO_BASIC: DateTimeFormat } @@ -174,6 +151,7 @@ public expect class LocalDate : Comparable { * * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the * given [monthNumber] and [year]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunctionMonthNumber */ public constructor(year: Int, monthNumber: Int, dayOfMonth: Int) @@ -188,25 +166,50 @@ public expect class LocalDate : Comparable { * * @throws IllegalArgumentException if any parameter is out of range, or if [dayOfMonth] is invalid for the * given [month] and [year]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunction */ public constructor(year: Int, month: Month, dayOfMonth: Int) - /** Returns the year component of the date. */ + /** + * Returns the year component of the date. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.year + */ public val year: Int - /** Returns the number-of-the-month (1..12) component of the date. */ + /** + * Returns the number-of-the-month (1..12) component of the date. + * + * Shortcut for `month.number`. + */ public val monthNumber: Int - /** Returns the month ([Month]) component of the date. */ + /** + * Returns the month ([Month]) component of the date. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.month + */ public val month: Month - /** Returns the day-of-month component of the date. */ + /** + * Returns the day-of-month (`1..31`) component of the date. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.dayOfMonth + */ public val dayOfMonth: Int - /** Returns the day-of-week component of the date. */ + /** + * Returns the day-of-week component of the date. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.dayOfWeek + */ public val dayOfWeek: DayOfWeek - /** Returns the day-of-year component of the date. */ + /** + * Returns the day-of-year (`1..366`) component of the date. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.dayOfYear + */ public val dayOfYear: Int /** @@ -215,6 +218,7 @@ public expect class LocalDate : Comparable { * 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.fromEpochDays + * @sample kotlinx.datetime.test.samples.LocalDateSamples.toEpochDays */ public fun toEpochDays(): Int @@ -223,6 +227,8 @@ public expect class LocalDate : Comparable { * Returns zero if this date represents the same day as the other (i.e., equal to other), * a negative number if this date is earlier than the other, * and a positive number if this date is later than the other. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.compareToSample */ public override fun compareTo(other: LocalDate): Int @@ -232,6 +238,7 @@ public expect class LocalDate : Comparable { * @see Formats.ISO for the format details. * @see parse for the dual operation: obtaining [LocalDate] from a string. * @see LocalDate.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.toStringSample */ public override fun toString(): String } @@ -239,6 +246,8 @@ public expect class LocalDate : Comparable { /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.formatting */ public fun LocalDate.format(format: DateTimeFormat): String = format.format(this) @@ -253,6 +262,8 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) * * 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. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.atTimeInline */ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalDateTime = LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) @@ -267,6 +278,8 @@ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: * exist in the implicit time zone. * For example, `LocalDate(2021, 3, 28).atTime(LocalTime(2, 16, 20))` will successfully create a [LocalDateTime], * even though in Berlin, times between 2:00 and 3:00 do not exist on March 28, 2021 due to the transition to DST. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.atTime */ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this, time) @@ -275,16 +288,10 @@ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this * Returns a date that is the result of adding components of [DatePeriod] to this date. The components are * added in the order from the largest units to the smallest: first years and months, then days. * - * ``` - * LocalDate(2023, Month.JANUARY, 30) + DatePeriod(years = 1, months = 2, days = 2) == LocalDate(2024, Month.APRIL, 1) - * // 2023-01-30 + 1 year = 2024-01-30 - * // 2024-01-30 + 2 months = 2024-03-30 - * // 2024-03-30 + 2 days = 2024-04-01 - * ``` - * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plusPeriod */ public expect operator fun LocalDate.plus(period: DatePeriod): LocalDate @@ -292,16 +299,10 @@ public expect operator fun LocalDate.plus(period: DatePeriod): LocalDate * Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are * subtracted in the order from the largest units to the smallest: first years and months, then days. * - * ``` - * LocalDate(2023, Month.JANUARY, 2) - DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2021, Month.OCTOBER, 30) - * // 2023-01-02 - 1 year = 2022-01-02 - * // 2022-01-02 - 2 months = 2021-11-02 - * // 2021-11-02 - 3 days = 2021-10-30 - * ``` - * * @see LocalDate.periodUntil * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusPeriod */ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) { @@ -322,13 +323,10 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = * - negative or zero if this date is later than the other, * - exactly zero if this date is equal to the other. * - * ``` - * LocalDate(2023, Month.JANUARY, 2).periodUntil(LocalDate(2024, Month.APRIL, 1)) == DatePeriod(years = 1, months = 2, days = 30) - * ``` - * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * * @see LocalDate.minus for the same operation with the order of arguments reversed. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.periodUntil */ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod @@ -342,13 +340,10 @@ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod * - positive or zero if this date is later than the other, * - exactly zero if this date is equal to the other. * - * ``` - * LocalDate(2024, Month.APRIL, 1) - LocalDate(2023, Month.JANUARY, 2) == DatePeriod(years = 1, months = 2, days = 30) - * ``` - * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * * @see LocalDate.periodUntil for the same operation with the order of arguments reversed. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusDate */ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.periodUntil(this) @@ -361,18 +356,13 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * - zero if this date is equal to the other. * * The value is rounded toward zero. - * - * ``` - * LocalDate(2023, Month.JANUARY, 2).until(LocalDate(2024, Month.APRIL, 1), DateTimeUnit.MONTH) == 14 - * // one year, two months, and 30 days, rounded toward zero. - * ``` * 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 - * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.until */ public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int @@ -384,6 +374,7 @@ public expect fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased * 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.until + * @sample kotlinx.datetime.test.samples.LocalDateSamples.daysUntil */ public expect fun LocalDate.daysUntil(other: LocalDate): Int @@ -395,6 +386,7 @@ public expect fun LocalDate.daysUntil(other: LocalDate): Int * 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.until + * @sample kotlinx.datetime.test.samples.LocalDateSamples.monthsUntil */ public expect fun LocalDate.monthsUntil(other: LocalDate): Int @@ -404,6 +396,7 @@ public expect fun LocalDate.monthsUntil(other: LocalDate): Int * 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.until + * @sample kotlinx.datetime.test.samples.LocalDateSamples.yearsUntil */ public expect fun LocalDate.yearsUntil(other: LocalDate): Int @@ -440,6 +433,7 @@ public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, u * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plusInt */ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -452,6 +446,7 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusInt */ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -464,6 +459,7 @@ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): Loc * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plusLong */ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate @@ -476,6 +472,7 @@ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): Loc * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusLong */ public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt new file mode 100644 index 000000000..e3b1e7500 --- /dev/null +++ b/core/common/test/samples/LocalDateSamples.kt @@ -0,0 +1,274 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.random.* +import kotlin.test.* + +class LocalDateSamples { + + @Test + fun simpleParsingAndFormatting() { + check(LocalDate.parse("2023-01-02") == LocalDate(2023, Month.JANUARY, 2)) + check(LocalDate(2023, Month.JANUARY, 2).toString() == "2023-01-02") + } + + @Test + fun parsing() { + check(LocalDate.parse("2024-04-16") == LocalDate(2024, Month.APRIL, 16)) + val customFormat = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() + } + check(LocalDate.parse("Apr 16, 2024", customFormat) == LocalDate(2024, Month.APRIL, 16)) + } + + @Test + fun fromAndToEpochDays() { + check(LocalDate.fromEpochDays(0) == LocalDate(1970, Month.JANUARY, 1)) + val randomEpochDay = Random.nextInt(-50_000..50_000) + val randomDate = LocalDate.fromEpochDays(randomEpochDay) + check(randomDate.toEpochDays() == randomEpochDay) + } + + @Test + fun customFormat() { + val customFormat = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() + } + val date = customFormat.parse("Apr 16, 2024") + check(date == LocalDate(2024, Month.APRIL, 16)) + val formatted = date.format(customFormat) + check(formatted == "Apr 16, 2024") + } + + @Test + fun constructorFunctionMonthNumber() { + val date = LocalDate(2024, 4, 16) + check(date.year == 2024) + check(date.monthNumber == 4) + check(date.month == Month.APRIL) + check(date.dayOfMonth == 16) + } + + @Test + fun constructorFunction() { + val date = LocalDate(2024, Month.APRIL, 16) + check(date.year == 2024) + check(date.month == Month.APRIL) + check(date.dayOfMonth == 16) + } + + @Test + fun year() { + check(LocalDate(2024, Month.APRIL, 16).year == 2024) + check(LocalDate(0, Month.APRIL, 16).year == 0) + check(LocalDate(-2024, Month.APRIL, 16).year == -2024) + } + + @Test + fun month() { + for (month in Month.entries) { + check(LocalDate(2024, month, 16).month == month) + } + } + + @Test + fun dayOfMonth() { + repeat(30) { + val dayOfMonth = it + 1 + check(LocalDate(2024, Month.APRIL, dayOfMonth).dayOfMonth == dayOfMonth) + } + } + + @Test + fun dayOfWeek() { + check(LocalDate(2024, Month.APRIL, 16).dayOfWeek == DayOfWeek.TUESDAY) + check(LocalDate(2024, Month.APRIL, 17).dayOfWeek == DayOfWeek.WEDNESDAY) + check(LocalDate(2024, Month.APRIL, 18).dayOfWeek == DayOfWeek.THURSDAY) + } + + @Test + fun dayOfYear() { + check(LocalDate(2024, Month.APRIL, 16).dayOfYear == 107) + check(LocalDate(2024, Month.JANUARY, 1).dayOfYear == 1) + check(LocalDate(2024, Month.DECEMBER, 31).dayOfYear == 366) + } + + @Test + fun toEpochDays() { + check(LocalDate(2024, Month.APRIL, 16).toEpochDays() == 19829) + check(LocalDate(1970, Month.JANUARY, 1).toEpochDays() == 0) + check(LocalDate(1969, Month.DECEMBER, 25).toEpochDays() == -7) + } + + @Test + fun compareToSample() { + check(LocalDate(2023, 4, 16) < LocalDate(2024, 3, 15)) + check(LocalDate(2023, 4, 16) < LocalDate(2023, 5, 15)) + check(LocalDate(2023, 4, 16) < LocalDate(2023, 4, 17)) + check(LocalDate(-1000, 4, 16) < LocalDate(0, 4, 17)) + } + + @Test + fun toStringSample() { + check(LocalDate(2024, 4, 16).toString() == "2024-04-16") + check(LocalDate(12024, 4, 16).toString() == "+12024-04-16") + check(LocalDate(-2024, 4, 16).toString() == "-2024-04-16") + } + + @Test + fun formatting() { + check(LocalDate(2024, 4, 16).format(LocalDate.Formats.ISO) == "2024-04-16") + val customFormat = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() + } + check(LocalDate(2024, 4, 16).format(customFormat) == "Apr 16, 2024") + } + + @Test + fun atTimeInline() { + val date = LocalDate(2024, Month.APRIL, 16) + val dateTime = date.atTime(13, 30) + check(dateTime == LocalDateTime(2024, Month.APRIL, 16, 13, 30)) + } + + @Test + fun atTime() { + val date = LocalDate(2024, Month.APRIL, 16) + val time = LocalTime(13, 30) + val dateTime = date.atTime(time) + check(dateTime == LocalDateTime(2024, Month.APRIL, 16, 13, 30)) + } + + @Test + fun plusPeriod() { + val startDate = LocalDate(2021, Month.OCTOBER, 30) + check(startDate + DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2023, Month.JANUARY, 2)) + // Step by step explanation: + // 1. Months and years are added first as one step. + val intermediateDate = LocalDate(2022, Month.DECEMBER, 30) + check(startDate.plus(14, DateTimeUnit.MONTH) == intermediateDate) + // 2. Days are added. + check(intermediateDate.plus(3, DateTimeUnit.DAY) == LocalDate(2023, Month.JANUARY, 2)) + } + + @Test + fun minusPeriod() { + val startDate = LocalDate(2023, Month.JANUARY, 2) + check(startDate - DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2021, Month.OCTOBER, 30)) + // Step by step explanation: + // 1. Months and years are subtracted first as one step. + val intermediateDate = LocalDate(2021, Month.NOVEMBER, 2) + check(startDate.minus(14, DateTimeUnit.MONTH) == intermediateDate) + // 2. Days are subtracted. + check(intermediateDate.minus(3, DateTimeUnit.DAY) == LocalDate(2021, Month.OCTOBER, 30)) + } + + @Test + fun periodUntil() { + val startDate = LocalDate(2023, Month.JANUARY, 2) + val endDate = LocalDate(2024, Month.APRIL, 1) + val period = startDate.periodUntil(endDate) + check(period == DatePeriod(years = 1, months = 2, days = 30)) + } + + @Test + fun minusDate() { + val startDate = LocalDate(2023, Month.JANUARY, 2) + val endDate = LocalDate(2024, Month.APRIL, 1) + val period = endDate - startDate + check(period == DatePeriod(years = 1, months = 2, days = 30)) + } + + @Test + fun until() { + val startDate = LocalDate(2023, Month.JANUARY, 2) + val endDate = LocalDate(2024, Month.APRIL, 1) + val differenceInMonths = startDate.until(endDate, DateTimeUnit.MONTH) + check(differenceInMonths == 14) + // one year, two months, and 30 days, rounded toward zero. + } + + @Test + fun daysUntil() { + val dateOfConcert = LocalDate(2024, Month.SEPTEMBER, 26) + val today = LocalDate(2024, Month.APRIL, 16) + val daysUntilConcert = today.daysUntil(dateOfConcert) + check(daysUntilConcert == 163) + } + + @Test + fun monthsUntil() { + val babyDateOfBirth = LocalDate(2023, Month.DECEMBER, 14) + val today = LocalDate(2024, Month.APRIL, 16) + val ageInMonths = babyDateOfBirth.monthsUntil(today) + check(ageInMonths == 4) + } + + @Test + fun yearsUntil() { + val dateOfBirth = LocalDate(2016, Month.JANUARY, 14) + val today = LocalDate(2024, Month.APRIL, 16) + val age = dateOfBirth.yearsUntil(today) + check(age == 8) + } + + @Test + fun plusInt() { + val today = LocalDate(2024, Month.APRIL, 16) + val tenDaysLater = today.plus(10, DateTimeUnit.DAY) + check(tenDaysLater == LocalDate(2024, Month.APRIL, 26)) + val twoMonthsLater = today.plus(2, DateTimeUnit.MONTH) + check(twoMonthsLater == LocalDate(2024, Month.JUNE, 16)) + } + + @Test + fun minusInt() { + val today = LocalDate(2024, Month.APRIL, 16) + val tenDaysAgo = today.minus(10, DateTimeUnit.DAY) + check(tenDaysAgo == LocalDate(2024, Month.APRIL, 6)) + val twoMonthsAgo = today.minus(2, DateTimeUnit.MONTH) + check(twoMonthsAgo == LocalDate(2024, Month.FEBRUARY, 16)) + } + + @Test + @Ignore // only the JVM has the range wide enough + fun plusLong() { + val today = LocalDate(2024, Month.APRIL, 16) + val tenTrillionDaysLater = today.plus(10_000_000_000L, DateTimeUnit.DAY) + assertEquals(LocalDate(2024, Month.APRIL, 16).plus(10_000_000_000L, DateTimeUnit.DAY), LocalDate(27_381_094, Month.MAY, 12)) + check(tenTrillionDaysLater == LocalDate(27_381_094, Month.MAY, 12)) + } + + @Test + @Ignore // only the JVM has the range wide enough + fun minusLong() { + val today = LocalDate(2024, Month.APRIL, 16) + val tenTrillionDaysAgo = today.minus(10_000_000_000L, DateTimeUnit.DAY) + assertEquals(LocalDate(2024, Month.APRIL, 16).minus(10_000_000_000L, DateTimeUnit.DAY), LocalDate(-27_377_046, Month.MARCH, 22)) + check(tenTrillionDaysAgo == LocalDate(-27_377_046, Month.MARCH, 22)) + } + + class Formats { + @Test + fun iso() { + val date = LocalDate.Formats.ISO.parse("2024-04-16") + check(date == LocalDate(2024, Month.APRIL, 16)) + val formatted = LocalDate.Formats.ISO.format(date) + check(formatted == "2024-04-16") + } + + @Test + fun isoBasic() { + val date = LocalDate.Formats.ISO_BASIC.parse("20240416") + check(date == LocalDate(2024, Month.APRIL, 16)) + val formatted = LocalDate.Formats.ISO_BASIC.format(date) + check(formatted == "20240416") + } + } +} From d70657b8f4a2a2595e9068733c3e9c1d1f8d4eb9 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 17 Apr 2024 13:41:09 +0200 Subject: [PATCH 15/35] Finish adding samples --- core/common/src/Instant.kt | 116 +++---- core/common/test/samples/InstantSamples.kt | 325 +++++++++++++++++++ core/common/test/samples/LocalDateSamples.kt | 2 - 3 files changed, 376 insertions(+), 67 deletions(-) create mode 100644 core/common/test/samples/InstantSamples.kt diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 5f35ee402..bb40cec39 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -199,6 +199,7 @@ public expect class Instant : Comparable { * Note that this number doesn't include leap seconds added or removed since the epoch. * * @see fromEpochSeconds + * @sample kotlinx.datetime.test.samples.InstantSamples.epochSeconds */ public val epochSeconds: Long @@ -208,6 +209,7 @@ public expect class Instant : Comparable { * The value is always positive and lies in the range `0..999_999_999`. * * @see fromEpochSeconds + * @sample kotlinx.datetime.test.samples.InstantSamples.nanosecondsOfSecond */ public val nanosecondsOfSecond: Int @@ -219,6 +221,7 @@ public expect class Instant : Comparable { * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * * @see fromEpochMilliseconds + * @sample kotlinx.datetime.test.samples.InstantSamples.toEpochMilliseconds */ public fun toEpochMilliseconds(): Long @@ -234,6 +237,8 @@ public expect class Instant : Comparable { * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers * a day to be 24 hours. * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.plusDuration */ public operator fun plus(duration: Duration): Instant @@ -249,6 +254,8 @@ public expect class Instant : Comparable { * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers * a day to be 24 hours. * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusDuration */ public operator fun minus(duration: Duration): Instant @@ -266,6 +273,8 @@ public expect class Instant : Comparable { * or even monotonic, so the result of this operation may be negative even if the other instant was observed later * than this one, or vice versa. * For measuring time intervals, consider using [TimeSource.Monotonic]. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusInstant */ public operator fun minus(other: Instant): Duration @@ -274,6 +283,8 @@ public expect class Instant : Comparable { * 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.compareToSample */ public override operator fun compareTo(other: Instant): Int @@ -289,6 +300,7 @@ public expect class Instant : Comparable { * @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET for a very similar format. The difference is that * [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] will not add trailing zeros for readability to the * fractional part of the second. + * @sample kotlinx.datetime.test.samples.InstantSamples.toStringSample */ public override fun toString(): String @@ -306,6 +318,7 @@ public expect class Instant : Comparable { * Note that [Instant] also supports nanosecond precision via [fromEpochSeconds]. * * @see Instant.toEpochMilliseconds + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochMilliseconds */ public fun fromEpochMilliseconds(epochMilliseconds: Long): Instant @@ -320,6 +333,7 @@ public expect class Instant : Comparable { * * @see Instant.epochSeconds * @see Instant.nanosecondsOfSecond + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochSeconds */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long = 0): Instant @@ -334,6 +348,7 @@ public expect class Instant : Comparable { * * @see Instant.epochSeconds * @see Instant.nanosecondsOfSecond + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochSecondsIntNanos */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant @@ -356,6 +371,7 @@ public expect class Instant : Comparable { * * @see Instant.toString for formatting using the default format. * @see Instant.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.InstantSamples.parsing */ public fun parse( input: CharSequence, @@ -387,11 +403,19 @@ public expect class Instant : Comparable { } } -/** Returns true if the instant is [Instant.DISTANT_PAST] or earlier. */ +/** + * Returns true if the instant is [Instant.DISTANT_PAST] or earlier. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.isDistantPast + */ public val Instant.isDistantPast: Boolean get() = this <= Instant.DISTANT_PAST -/** Returns true if the instant is [Instant.DISTANT_FUTURE] or later. */ +/** + * Returns true if the instant is [Instant.DISTANT_FUTURE] or later. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.isDistantFuture + */ public val Instant.isDistantFuture: Boolean get() = this >= Instant.DISTANT_FUTURE @@ -412,12 +436,9 @@ public fun String.toInstant(): Instant = Instant.parse(this) * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], like in * `Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. * - * ``` - * Clock.System.now().plus(DateTimePeriod(months = 1, days = -1), TimeZone.UTC) // one day short from a month later - * ``` - * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.plusPeriod */ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant @@ -432,12 +453,9 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst * please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], as in * `Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`. * - * ``` - * Clock.System.now().minus(DateTimePeriod(months = 1, days = -1), TimeZone.UTC) // one day short from a month earlier - * ``` - * * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusPeriod */ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = /* An overflow can happen for any component, but we are only worried about nanoseconds, as having an overflow in @@ -463,6 +481,7 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = * * @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. + * @sample kotlinx.datetime.test.samples.InstantSamples.periodUntil */ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod @@ -477,13 +496,8 @@ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * - * ``` - * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) - * val currentMoment = Clock.System.now() - * val daysLived = momentOfBirth.until(currentMoment, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) - * ``` - * * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.untilAsDateTimeUnit */ public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long @@ -497,11 +511,7 @@ public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * - * ``` - * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) - * val currentMoment = Clock.System.now() - * val minutesLived = momentOfBirth.until(currentMoment, DateTimeUnit.MINUTE) - * ``` + * @sample kotlinx.datetime.test.samples.InstantSamples.untilAsTimeBasedUnit */ public fun Instant.until(other: Instant, unit: DateTimeUnit.TimeBased): Long = try { @@ -520,6 +530,7 @@ public fun Instant.until(other: Instant, unit: DateTimeUnit.TimeBased): Long = * * @see Instant.until * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.daysUntil */ public fun Instant.daysUntil(other: Instant, timeZone: TimeZone): Int = until(other, DateTimeUnit.DAY, timeZone).clampToInt() @@ -531,6 +542,7 @@ public fun Instant.daysUntil(other: Instant, timeZone: TimeZone): Int = * * @see Instant.until * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.monthsUntil */ public fun Instant.monthsUntil(other: Instant, timeZone: TimeZone): Int = until(other, DateTimeUnit.MONTH, timeZone).clampToInt() @@ -542,6 +554,7 @@ public fun Instant.monthsUntil(other: Instant, timeZone: TimeZone): Int = * * @see Instant.until * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.yearsUntil */ public fun Instant.yearsUntil(other: Instant, timeZone: TimeZone): Int = until(other, DateTimeUnit.YEAR, timeZone).clampToInt() @@ -559,6 +572,7 @@ public fun Instant.yearsUntil(other: Instant, timeZone: TimeZone): 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 + * @sample kotlinx.datetime.test.samples.InstantSamples.minusInstantInZone */ public fun Instant.minus(other: Instant, timeZone: TimeZone): DateTimePeriod = other.periodUntil(this, timeZone) @@ -619,11 +633,8 @@ public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant = * Note that the time zone does not need to be passed when the [unit] is a time-based unit. * It is also not needed when adding date-based units to a [LocalDate]. * - * ``` - * Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days from now in Berlin - * ``` - * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit */ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -637,14 +648,11 @@ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon * Note that the time zone does not need to be passed when the [unit] is a time-based unit. * It is also not needed when subtracting date-based units from a [LocalDate]. * - * ``` - * Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days earlier than now in Berlin - * ``` - * * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnit */ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -654,11 +662,9 @@ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZo * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * - * ``` - * Clock.System.now().plus(5, DateTimeUnit.HOUR) // 5 hours from now - * ``` - * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit */ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = plus(value.toLong(), unit) @@ -669,11 +675,9 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * - * ``` - * Clock.System.now().minus(5, DateTimeUnit.HOUR) // 5 hours earlier than now - * ``` - * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit */ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = minus(value.toLong(), unit) @@ -688,11 +692,8 @@ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * Note that the time zone does not need to be passed when the [unit] is a time-based unit. * It is also not needed when adding date-based units to a [LocalDate]. * - * ``` - * Clock.System.now().plus(5L, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days from now in Berlin - * ``` - * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnitLong */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -706,11 +707,8 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo * Note that the time zone does not need to be passed when the [unit] is a time-based unit. * It is also not needed when subtracting date-based units from a [LocalDate]. * - * ``` - * Clock.System.now().minus(5L, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // 5 days earlier than now in Berlin - * ``` - * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnitLong */ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = if (value != Long.MIN_VALUE) { @@ -725,11 +723,9 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I * If the [value] is positive, the returned instant is later than this instant. * If the [value] is negative, the returned instant is earlier than this instant. * - * ``` - * Clock.System.now().plus(5L, DateTimeUnit.HOUR) // 5 hours from now - * ``` - * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnitLong */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant @@ -739,11 +735,9 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta * If the [value] is positive, the returned instant is earlier than this instant. * If the [value] is negative, the returned instant is later than this instant. * - * ``` - * Clock.System.now().minus(5L, DateTimeUnit.HOUR) // 5 hours earlier than now - * ``` - * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnitLong */ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = if (value != Long.MIN_VALUE) { @@ -759,16 +753,11 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = * The value returned is negative or zero if this instant is earlier than the other, * and positive or zero if this instant is later than the other. * - * ``` - * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) - * val currentMoment = Clock.System.now() - * val daysLived = currentMoment.minus(momentOfBirth, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) - * ``` - * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. * @see Instant.until for the same operation but with swapped arguments. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusAsDateTimeUnit */ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = other.until(this, unit, timeZone) @@ -779,15 +768,10 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone) * The value returned is negative or zero if this instant is earlier than the other, * and positive or zero if this instant is later than the other. * - * ``` - * val momentOfBirth = LocalDateTime.parse("1990-02-20T12:03:53Z").toInstant(TimeZone.of("Europe/Berlin")) - * val currentMoment = Clock.System.now() - * val minutesLived = currentMoment.minus(momentOfBirth, DateTimeUnit.MINUTE) - * ``` - * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * * @see Instant.until for the same operation but with swapped arguments. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusAsTimeBasedUnit */ public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long = other.until(this, unit) @@ -801,6 +785,8 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long = * [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is a format very similar to the one used by [toString]. * The only difference is that [Instant.toString] adds trailing zeros to the fraction-of-second component so that the * number of digits after a dot is a multiple of three. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.formatting */ public fun Instant.format(format: DateTimeFormat, offset: UtcOffset = UtcOffset.ZERO): String { val instant = this diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt new file mode 100644 index 000000000..92706f5a6 --- /dev/null +++ b/core/common/test/samples/InstantSamples.kt @@ -0,0 +1,325 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.random.* +import kotlin.test.* +import kotlin.time.Duration.Companion.hours + +class InstantSamples { + + @Test + fun epochSeconds() { + val instant1 = Instant.fromEpochSeconds(999_999, nanosecondAdjustment = 123_456_789) + check(instant1.epochSeconds == 999_999L) + val instant2 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = 100_123_456_789) + check(instant2.epochSeconds == 1_000_000 + 100L) + val instant3 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = -100_876_543_211) + check(instant3.epochSeconds == 1_000_000 - 101L) + } + + @Test + fun nanosecondsOfSecond() { + val instant1 = Instant.fromEpochSeconds(999_999, nanosecondAdjustment = 123_456_789) + check(instant1.nanosecondsOfSecond == 123_456_789) + val instant2 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = 100_123_456_789) + check(instant2.nanosecondsOfSecond == 123_456_789) + val instant3 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = -100_876_543_211) + check(instant3.nanosecondsOfSecond == 123_456_789) + } + + @Test + fun toEpochMilliseconds() { + check(Instant.fromEpochMilliseconds(0).toEpochMilliseconds() == 0L) + check(Instant.fromEpochMilliseconds(1_000_000_000_123).toEpochMilliseconds() == 1_000_000_000_123L) + check(Instant.fromEpochSeconds(1_000_000_000, nanosecondAdjustment = 123_999_999) + .toEpochMilliseconds() == 1_000_000_000_123L) + } + + @Test + fun plusDuration() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val fiveHoursLater = instant + 5.hours + check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) + } + + @Test + fun minusDuration() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val fiveHoursEarlier = instant - 5.hours + check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) + } + + @Test + fun minusInstant() { + check(Instant.fromEpochSeconds(0) - Instant.fromEpochSeconds(epochSeconds = 7 * 60 * 60) == (-7).hours) + } + + @Test + fun compareToSample() { + fun randomInstant() = Instant.fromEpochMilliseconds( + Random.nextLong(Instant.DISTANT_PAST.toEpochMilliseconds(), Instant.DISTANT_FUTURE.toEpochMilliseconds()) + ) + repeat(100) { + val instant1 = randomInstant() + val instant2 = randomInstant() + check((instant1 < instant2) == (instant1.toEpochMilliseconds() < instant2.toEpochMilliseconds())) + } + } + + @Test + fun toStringSample() { + check(Instant.fromEpochMilliseconds(0).toString() == "1970-01-01T00:00:00Z") + } + + @Test + fun fromEpochMilliseconds() { + check(Instant.fromEpochMilliseconds(epochMilliseconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) + check(Instant.fromEpochMilliseconds(epochMilliseconds = 1_000_000_000_123) + == Instant.parse("2001-09-09T01:46:40.123Z")) + } + + @Test + fun fromEpochSeconds() { + check(Instant.fromEpochSeconds(epochSeconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) + check(Instant.fromEpochSeconds(epochSeconds = 1_000_001_234, nanosecondAdjustment = -1_234_000_000_001) + == Instant.parse("2001-09-09T01:46:39.999999999Z")) + } + + @Test + fun fromEpochSecondsIntNanos() { + check(Instant.fromEpochSeconds(epochSeconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) + check(Instant.fromEpochSeconds(epochSeconds = 1_000_000_000, nanosecondAdjustment = -1) == Instant.parse("2001-09-09T01:46:39.999999999Z")) + } + + @Test + fun parsing() { + check(Instant.parse("1970-01-01T00:00:00Z") == Instant.fromEpochMilliseconds(0)) + check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochMilliseconds(0)) + } + + @Test + fun isDistantPast() { + val currentInstant = Clock.System.now() + val tenThousandYearsAgo = currentInstant.minus(1_000, DateTimeUnit.YEAR, TimeZone.UTC) + check(!tenThousandYearsAgo.isDistantPast) + check(Instant.DISTANT_PAST.isDistantPast) + } + + @Test + fun isDistantFuture() { + val currentInstant = Clock.System.now() + val tenThousandYearsLater = currentInstant.plus(10_000, DateTimeUnit.YEAR, TimeZone.UTC) + check(!tenThousandYearsLater.isDistantFuture) + check(Instant.DISTANT_FUTURE.isDistantFuture) + } + + @Test + fun plusPeriod() { + val startInstant = Instant.parse("2024-03-09T07:16:39.688Z") + val period = DateTimePeriod(months = 1, days = -1) // one day short from a month later + val afterPeriodInBerlin = startInstant.plus(period, TimeZone.of("Europe/Berlin")) + check(afterPeriodInBerlin == Instant.parse("2024-04-08T06:16:39.688Z")) + val afterPeriodInSydney = startInstant.plus(period, TimeZone.of("Australia/Sydney")) + check(afterPeriodInSydney == Instant.parse("2024-04-08T08:16:39.688Z")) + } + + @Test + fun minusPeriod() { + val period = DateTimePeriod(months = 1, days = -1) // one day short from a month earlier + val startInstant = Instant.parse("2024-03-23T16:50:41.926Z") + val afterPeriodInBerlin = startInstant.minus(period, TimeZone.of("Europe/Berlin")) + check(afterPeriodInBerlin == Instant.parse("2024-02-24T16:50:41.926Z")) + val afterPeriodInNewYork = startInstant.minus(period, TimeZone.of("America/New_York")) + check(afterPeriodInNewYork == Instant.parse("2024-02-24T17:50:41.926Z")) + } + + /** copy of [minusInstantInZone] */ + @Test + fun periodUntil() { + val startInstant = Instant.parse("2024-01-01T02:00:00Z") + val endInstant = Instant.parse("2024-03-01T03:15:03Z") + // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months + val periodInNewYork = startInstant.periodUntil(endInstant, TimeZone.of("America/New_York")) + check(periodInNewYork == DateTimePeriod(months = 1, days = 29, hours = 1, minutes = 15, seconds = 3)) + // In Berlin, we find the difference between 2024-01-01 and 2024-03-01, which is exactly two months + val periodInBerlin = startInstant.periodUntil(endInstant, TimeZone.of("Europe/Berlin")) + check(periodInBerlin == DateTimePeriod(months = 2, days = 0, hours = 1, minutes = 15, seconds = 3)) + } + + /** copy of [minusAsDateTimeUnit] */ + @Test + fun untilAsDateTimeUnit() { + val startInstant = Instant.parse("2024-01-01T02:00:00Z") + val endInstant = Instant.parse("2024-03-01T02:00:00Z") + // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months + val monthsBetweenInNewYork = startInstant.until(endInstant, DateTimeUnit.MONTH, TimeZone.of("America/New_York")) + check(monthsBetweenInNewYork == 1L) + // In Berlin, we find the difference between 2024-01-01 and 2024-03-01, which is exactly two months + val monthsBetweenInBerlin = startInstant.until(endInstant, DateTimeUnit.MONTH, TimeZone.of("Europe/Berlin")) + check(monthsBetweenInBerlin == 2L) + } + + /** copy of [minusAsTimeBasedUnit] */ + @Test + fun untilAsTimeBasedUnit() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) + val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val hoursBetweenInstants = instant.until(otherInstant, DateTimeUnit.HOUR) + check(hoursBetweenInstants == 7L) + } + + @Test + fun daysUntil() { + val startInstant = Instant.parse("2023-03-26T00:30:00Z") + val endInstant = Instant.parse("2023-03-28T00:15:00Z") + // In New York, these days are both 24 hour long, so the difference is 15 minutes short of 2 days + val daysBetweenInNewYork = startInstant.daysUntil(endInstant, TimeZone.of("America/New_York")) + check(daysBetweenInNewYork == 1) + // In Berlin, 2023-03-26 is 23 hours long, so the difference more than 2 days + val daysBetweenInBerlin = startInstant.daysUntil(endInstant, TimeZone.of("Europe/Berlin")) + check(daysBetweenInBerlin == 2) + } + + @Test + fun monthsUntil() { + val startInstant = Instant.parse("2024-01-01T02:00:00Z") + val endInstant = Instant.parse("2024-03-01T02:00:00Z") + // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months + val monthsBetweenInNewYork = startInstant.monthsUntil(endInstant, TimeZone.of("America/New_York")) + check(monthsBetweenInNewYork == 1) + // In Berlin, we find the difference between 2024-01-01 and 2024-03-01, which is exactly two months + val monthsBetweenInBerlin = startInstant.monthsUntil(endInstant, TimeZone.of("Europe/Berlin")) + check(monthsBetweenInBerlin == 2) + } + + @Test + fun yearsUntil() { + val startInstant = Instant.parse("2024-03-01T02:01:00Z") + val endInstant = Instant.parse("2025-03-01T02:01:00Z") + // In New York, we find the difference between 2024-02-29 and 2025-02-28, which is just short of a year + val yearsBetweenInNewYork = startInstant.yearsUntil(endInstant, TimeZone.of("America/New_York")) + check(yearsBetweenInNewYork == 0) + // In Berlin, we find the difference between 2024-03-01 and 2025-03-01, which is exactly a year + val yearsBetweenInBerlin = startInstant.yearsUntil(endInstant, TimeZone.of("Europe/Berlin")) + check(yearsBetweenInBerlin == 1) + } + + /** copy of [periodUntil] */ + @Test + fun minusInstantInZone() { + val startInstant = Instant.parse("2024-01-01T02:00:00Z") + val endInstant = Instant.parse("2024-03-01T03:15:03Z") + // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months + val periodInNewYork = endInstant.minus(startInstant, TimeZone.of("America/New_York")) + check(periodInNewYork == DateTimePeriod(months = 1, days = 29, hours = 1, minutes = 15, seconds = 3)) + // In Berlin, we find the difference between 2024-01-01 and 2024-03-01, which is exactly two months + val periodInBerlin = endInstant.minus(startInstant, TimeZone.of("Europe/Berlin")) + check(periodInBerlin == DateTimePeriod(months = 2, days = 0, hours = 1, minutes = 15, seconds = 3)) + } + + @Test + fun plusDateTimeUnit() { + val startInstant = Instant.parse("2024-04-05T22:51:45.586Z") + val twoYearsLaterInBerlin = startInstant.plus(2, DateTimeUnit.YEAR, TimeZone.of("Europe/Berlin")) + check(twoYearsLaterInBerlin == Instant.parse("2026-04-05T22:51:45.586Z")) + val twoYearsLaterInSydney = startInstant.plus(2, DateTimeUnit.YEAR, TimeZone.of("Australia/Sydney")) + check(twoYearsLaterInSydney == Instant.parse("2026-04-05T23:51:45.586Z")) + } + + @Test + fun minusDateTimeUnit() { + val startInstant = Instant.parse("2024-05-02T08:55:40.322Z") + val twoYearsEarlierInBerlin = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Europe/Berlin")) + check(twoYearsEarlierInBerlin == Instant.parse("2022-05-02T08:55:40.322Z")) + val twoYearsEarlierInCairo = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Africa/Cairo")) + check(twoYearsEarlierInCairo == Instant.parse("2022-05-02T09:55:40.322Z")) + } + + @Test + fun plusTimeBasedUnit() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val fiveHoursLater = instant.plus(5, DateTimeUnit.HOUR) + check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) + } + + @Test + fun minusTimeBasedUnit() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val fiveHoursEarlier = instant.minus(5, DateTimeUnit.HOUR) + check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) + } + + @Test + @Ignore // only the JVM has the range wide enough + fun plusDateTimeUnitLong() { + val zone = TimeZone.of("Europe/Berlin") + val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) + val tenTrillionDaysLater = now.plus(10_000_000_000L, DateTimeUnit.DAY, zone) + check(tenTrillionDaysLater.toLocalDateTime(zone).date == LocalDate(27_381_094, Month.MAY, 12)) + } + + @Test + @Ignore // only the JVM has the range wide enough + fun minusDateTimeUnitLong() { + val zone = TimeZone.of("Europe/Berlin") + val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) + val tenTrillionDaysAgo = now.minus(10_000_000_000L, DateTimeUnit.DAY, zone) + check(tenTrillionDaysAgo.toLocalDateTime(zone).date == LocalDate(-27_377_046, Month.MARCH, 22)) + } + + @Test + fun plusTimeBasedUnitLong() { + val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) + val quadrillion = 1_000_000_000_000L + val quadrillionSecondsLater = startInstant.plus(quadrillion, DateTimeUnit.SECOND) + check(quadrillionSecondsLater.epochSeconds == quadrillion) + } + + @Test + fun minusTimeBasedUnitLong() { + val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) + val quadrillion = 1_000_000_000_000L + val quadrillionSecondsEarlier = startInstant.minus(quadrillion, DateTimeUnit.SECOND) + check(quadrillionSecondsEarlier.epochSeconds == -quadrillion) + } + + /** copy of [untilAsDateTimeUnit] */ + @Test + fun minusAsDateTimeUnit() { + val startInstant = Instant.parse("2024-01-01T02:00:00Z") + val endInstant = Instant.parse("2024-03-01T02:00:00Z") + // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months + val monthsBetweenInNewYork = endInstant.minus(startInstant, DateTimeUnit.MONTH, TimeZone.of("America/New_York")) + check(monthsBetweenInNewYork == 1L) + // In Berlin, we find the difference between 2024-01-01 and 2024-03-01, which is exactly two months + val monthsBetweenInBerlin = endInstant.minus(startInstant, DateTimeUnit.MONTH, TimeZone.of("Europe/Berlin")) + check(monthsBetweenInBerlin == 2L) + } + + /** copy of [untilAsTimeBasedUnit] */ + @Test + fun minusAsTimeBasedUnit() { + val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) + val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val hoursBetweenInstants = otherInstant.minus(instant, DateTimeUnit.HOUR) + check(hoursBetweenInstants == 7L) + } + + @Test + fun formatting() { + val epochStart = Instant.fromEpochMilliseconds(0) + check(epochStart.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) == "1970-01-01T00:00:00Z") + val customFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO_BASIC) + hour(); minute(); second(); char('.'); secondFraction(3) + offset(UtcOffset.Formats.FOUR_DIGITS) + } + check(epochStart.format(customFormat, UtcOffset(hours = 3, minutes = 30)) == "19700101033000.000+0330") + } +} diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index e3b1e7500..885af3866 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -241,7 +241,6 @@ class LocalDateSamples { fun plusLong() { val today = LocalDate(2024, Month.APRIL, 16) val tenTrillionDaysLater = today.plus(10_000_000_000L, DateTimeUnit.DAY) - assertEquals(LocalDate(2024, Month.APRIL, 16).plus(10_000_000_000L, DateTimeUnit.DAY), LocalDate(27_381_094, Month.MAY, 12)) check(tenTrillionDaysLater == LocalDate(27_381_094, Month.MAY, 12)) } @@ -250,7 +249,6 @@ class LocalDateSamples { fun minusLong() { val today = LocalDate(2024, Month.APRIL, 16) val tenTrillionDaysAgo = today.minus(10_000_000_000L, DateTimeUnit.DAY) - assertEquals(LocalDate(2024, Month.APRIL, 16).minus(10_000_000_000L, DateTimeUnit.DAY), LocalDate(-27_377_046, Month.MARCH, 22)) check(tenTrillionDaysAgo == LocalDate(-27_377_046, Month.MARCH, 22)) } From 3c956e0e83a0709d3a5e7682b7ce8f5a40025a0a Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 17 Apr 2024 13:52:55 +0200 Subject: [PATCH 16/35] Address the reviews --- core/common/src/Clock.kt | 5 +++-- core/common/src/DateTimePeriod.kt | 11 +++++------ core/common/src/TimeZone.kt | 2 ++ core/common/src/format/DateTimeFormat.kt | 5 +++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 0e59ed673..7f2e239d9 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -22,9 +22,9 @@ public interface Clock { * Returns the [Instant] corresponding to the current time, according to this clock. * * It is not guaranteed that calling [now] later will return a larger [Instant]. - * In particular, for [System] it is completely expected that the opposite will happen, + * In particular, for [Clock.System] it is completely expected that the opposite will happen, * and it must be taken into account. - * See the documentation of [System] for details. + * See the documentation of [Clock.System] for details. * * Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling * leap seconds, [now] is not guaranteed to handle leap seconds in any specific way. @@ -54,6 +54,7 @@ public interface Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() } + /** A companion object used purely for namespacing. */ public companion object { } diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 49c57cef5..a5eab8e5f 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -22,8 +22,8 @@ import kotlinx.serialization.Serializable * The time components are: [hours] ([DateTimeUnit.HOUR]), [minutes] ([DateTimeUnit.MINUTE]), * [seconds] ([DateTimeUnit.SECOND]), [nanoseconds] ([DateTimeUnit.NANOSECOND]). * - * The time components are not independent and always overflow into one another. - * Likewise, months overflow into years. + * The time components are not independent and are always normalized together. + * Likewise, months are normalized together with years. * For example, there is no difference between `DateTimePeriod(months = 24, hours = 2, minutes = 63)` and * `DateTimePeriod(years = 2, hours = 3, minutes = 3)`. * @@ -73,10 +73,10 @@ public sealed class DateTimePeriod { internal abstract val totalMonths: Int /** - * The number of calendar days. + * The number of calendar days. Can be negative. * * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details. - * Also, this field does not overflow into months, so values larger than 31 can be present. + * Also, this field does not get normalized together with months, so values larger than 31 can be present. * * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ @@ -100,7 +100,7 @@ public sealed class DateTimePeriod { /** * The number of whole hours in this period. Can be negative. * - * This field does not overflow into days, so values larger than 23 or smaller than -23 can be present. + * This field does not get normalized together with days, so values larger than 23 or smaller than -23 can be present. * * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization */ @@ -210,7 +210,6 @@ public sealed class DateTimePeriod { * - Optionally, the number of years, followed by `Y`. * - Optionally, the number of months, followed by `M`. * - Optionally, the number of weeks, followed by `W`. - * This is not a part of the ISO 8601 format but an extension. * - Optionally, the number of days, followed by `D`. * - The string can end here if there are no more time components. * If there are time components, the letter `T` is required. diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 1d9ea91ae..f93b39f47 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -65,6 +65,8 @@ public expect open class TimeZone { /** * Returns the time zone with the fixed UTC+0 offset. * + * The [id] of this time zone is `"UTC"`. + * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.utc */ public val UTC: FixedOffsetTimeZone diff --git a/core/common/src/format/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index c04473d5b..5fc755e9a 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -11,6 +11,11 @@ import kotlinx.datetime.internal.format.parser.* /** * A format for parsing and formatting date-time-related values. + * + * By convention, predefined formats for each applicable class can be found in the `Formats` object of the class, and + * custom formats can be created using the `Format` function in the companion object of that class. + * For example, [LocalDate.Formats] contains predefined formats for [LocalDate], and [LocalDate.Format] can be used + * to define new ones. */ public sealed interface DateTimeFormat { /** From 788285c0a7dc75740583d4f998e597210e7af4b4 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 17 Apr 2024 14:47:04 +0200 Subject: [PATCH 17/35] Try to find the edge cases that work across platforms --- core/common/test/samples/InstantSamples.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index 92706f5a6..3b9ecf94c 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -234,11 +234,11 @@ class InstantSamples { @Test fun minusDateTimeUnit() { - val startInstant = Instant.parse("2024-05-02T08:55:40.322Z") + val startInstant = Instant.parse("2024-03-28T02:04:56.256Z") val twoYearsEarlierInBerlin = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Europe/Berlin")) - check(twoYearsEarlierInBerlin == Instant.parse("2022-05-02T08:55:40.322Z")) + check(twoYearsEarlierInBerlin == Instant.parse("2022-03-28T01:04:56.256Z")) val twoYearsEarlierInCairo = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Africa/Cairo")) - check(twoYearsEarlierInCairo == Instant.parse("2022-05-02T09:55:40.322Z")) + check(twoYearsEarlierInCairo == Instant.parse("2022-03-28T02:04:56.256Z")) } @Test From 60ed7652897c2218033befe34035bb90d4230655 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 17 Apr 2024 15:45:12 +0200 Subject: [PATCH 18/35] reword --- core/common/src/DateTimePeriod.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index a5eab8e5f..4f9dc12c5 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -219,8 +219,8 @@ public sealed class DateTimePeriod { * Seconds can optionally have a fractional part with up to nine digits. * The fractional part is separated with a `.`. * - * All numbers can be negative, in which case, `-` is prepended to them. - * Otherwise, a number can have `+` prepended to it, which does not have an effect. + * An explicit `+` or `-` sign can be prepended to any number. + * `-` means that the number is negative, and `+` has no effect. * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are * exceeded. From fd8be7dff960be2bf8a0c17476a09e63b327c30e Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:04:45 +0300 Subject: [PATCH 19/35] Update core/common/src/DateTimeUnit.kt Co-authored-by: ilya-g --- core/common/src/DateTimeUnit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index a17c29337..8c6cecd18 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -204,7 +204,7 @@ public sealed class DateTimeUnit { /** * A [date-time unit][DateTimeUnit] 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. + * Since different months have different number of days, a `MonthBased`-unit cannot be expressed as a multiple of some [DayBased]-unit. * * @see DateTimeUnit for a description of date-time units in general. * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.monthBasedUnit From 1302b7eee203b0e23133c3e87a2e62b5beec4d13 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 19 Apr 2024 11:10:59 +0200 Subject: [PATCH 20/35] Address the comments --- core/common/src/DateTimeUnit.kt | 10 +++++----- core/common/src/Instant.kt | 4 ++-- core/common/src/LocalDate.kt | 9 ++++++++- core/common/src/LocalDateTime.kt | 18 ++++++++++-------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 8c6cecd18..591cf0955 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -35,11 +35,11 @@ import kotlin.time.Duration.Companion.nanoseconds * Arithmetic operations on [LocalDateTime] are not provided. * Please see the [LocalDateTime] documentation for a discussion. * - * [DateTimePeriod] is a combination of all [DateTimeUnit] values, used to express things like + * [DateTimePeriod] is a combination of [DateTimeUnit] values of every kind, used to express things like * "two days and three hours." * [DatePeriod] is specifically a combination of [DateTimeUnit.DateBased] values. - * [DateTimePeriod] is more flexible than [DateTimeUnit] because it can express a combination of different units or - * have the length of zero, but in exchange, the duration of time between two [Instant] or [LocalDate] values can be + * [DateTimePeriod] is more flexible than [DateTimeUnit] because it can express a combination of values with different + * kinds of units, but in exchange, the duration of time between two [Instant] or [LocalDate] values can be * measured in terms of some [DateTimeUnit], but not [DateTimePeriod] or [DatePeriod]. * * ### Construction, serialization, and deserialization @@ -48,9 +48,9 @@ import kotlin.time.Duration.Companion.nanoseconds * [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 multiplying an existing unit on the right by an integer scalar: for example, `DateTimeUnit.MICROSECOND * 10`. * - By constructing an instance manually with [TimeBased], [DayBased], or [MonthBased]: for example, - * `DateTimeUnit.TimeBased(nanoseconds = 10)`. + * `DateTimeUnit.TimeBased(nanoseconds = 10_000)`. * * Also, [DateTimeUnit] can be serialized and deserialized using `kotlinx.serialization`: * [DateTimeUnitSerializer], [DateBasedDateTimeUnitSerializer], [DayBasedDateTimeUnitSerializer], diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index bb40cec39..b4e99ed4c 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -76,7 +76,7 @@ import kotlin.time.* * The [plus] and [minus] operators can be used to add [Duration]s to and subtract them from an [Instant]: * * ``` - * Clock.System.now() + Duration.seconds(5) // 5 seconds from now + * Clock.System.now() + 5.seconds // 5 seconds from now * ``` * * Durations can also be represented as multiples of some [time-based date-time unit][DateTimeUnit.TimeBased]: @@ -631,7 +631,7 @@ public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant = * If the [value] is negative, the returned instant is earlier than this instant. * * Note that the time zone does not need to be passed when the [unit] is a time-based unit. - * It is also not needed when adding date-based units to a [LocalDate]. + * It is also not needed when adding date-based units to a [LocalDate][LocalDate.plus]. * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index a1eda54c8..8db1cf3ce 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -263,6 +263,11 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) * 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. * + * **Pitfall**: since [LocalDateTime] is not tied to a particular time zone, the resulting [LocalDateTime] may not + * exist in the implicit time zone. + * For example, `LocalDate(2021, 3, 28).atTime(2, 16, 20)` will successfully create a [LocalDateTime], + * even though in Berlin, times between 2:00 and 3:00 do not exist on March 28, 2021 due to the transition to DST. + * * @sample kotlinx.datetime.test.samples.LocalDateSamples.atTimeInline */ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalDateTime = @@ -356,7 +361,7 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * - zero if this date is equal to the other. * * The value is rounded toward zero. - + * * 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 @@ -393,6 +398,8 @@ public expect fun LocalDate.monthsUntil(other: LocalDate): Int /** * Returns the number of whole years between two dates. * + * The value is rounded toward zero. + * * 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.until diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index d63debc83..1d55bc246 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -18,10 +18,10 @@ import kotlinx.serialization.Serializable * 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, but it is a [LocalDateTime]. * - * The main purpose of this class is to provide human-readable representations of [Instant] values, or to transfer them - * as data. - * Instances of [LocalDateTime] should not be stored when a specific time zone is known: in this case, it is recommended - * to use [Instant] instead. + * The main purpose of this class is to provide human-readable representations of [Instant] values, to transfer them + * as data, or to define future planned events that will have the same local date-time even if the time zone rules + * change. + * In all other cases when a specific time zone is known, it is recommended to use [Instant] instead. * * ### Arithmetic operations * @@ -30,7 +30,8 @@ import kotlinx.serialization.Serializable * * For example, in Berlin, naively adding one day to `2021-03-27T02:16:20` without accounting for the time zone would * result in `2021-03-28T02:16:20`. - * However, this local date-time is invalid, because the clocks moved forward from `02:00` to `03:00` on that day. + * However, the resulting local date-time cannot be observed in that time zone, + * because the clocks moved forward from `02:00` to `03:00` on that day. * This is known as a "time gap", or a "spring forward" transition. * * Similarly, the local date-time `2021-10-31T02:16:20` is ambiguous, @@ -39,8 +40,8 @@ import kotlinx.serialization.Serializable * * For these reasons, using [LocalDateTime] as an input to arithmetic operations is discouraged. * - * When only arithmetic on the date component is needed, without touching the time, use [LocalDate] instead, - * as it provides well-defined date arithmetic. + * When only the date component is needed, without the time, use [LocalDate] instead. + * It provides well-defined date arithmetic. * * If the time component must be taken into account, [LocalDateTime] * should be converted to [Instant] using a specific time zone, and the arithmetic on [Instant] should be used. @@ -65,7 +66,8 @@ import kotlinx.serialization.Serializable * whether the given date and time components are valid in the implied time zone. * For example, `2021-03-28T02:16:20` is invalid in Berlin, as it falls into a time gap, but nothing prevents one * from constructing such a [LocalDateTime]. - * Before constructing a [LocalDateTime] using any API, please ensure that the result is valid in the implied time zone. + * Before using a [LocalDateTime] constructed using any API, + * please ensure that the result is valid in the implied time zone. * The recommended pattern is to convert a [LocalDateTime] to [Instant] as soon as possible (see * [LocalDateTime.toInstant]) and work with [Instant] values instead. * From 8f73a08fd806aed4d52dad06eef0ae68c66d9819 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 19 Apr 2024 11:25:15 +0200 Subject: [PATCH 21/35] Clarify setting DateTimeComponents.month --- core/common/src/format/DateTimeComponents.kt | 6 ++++++ .../test/samples/format/DateTimeComponentsSamples.kt | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 80382ca5d..be46c55d7 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -274,8 +274,14 @@ public class DateTimeComponents internal constructor(internal val contents: Date /** * The month ([Month]) component of the date. + * + * This is a view of [monthNumber]. + * Setting it will set [monthNumber], and getting it will return a [Month] instance if [monthNumber] is a valid + * month. + * * @throws IllegalArgumentException during getting if [monthNumber] is outside the `1..12` range. * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.date + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.setMonth */ public var month: Month? get() = monthNumber?.let { Month(it) } diff --git a/core/common/test/samples/format/DateTimeComponentsSamples.kt b/core/common/test/samples/format/DateTimeComponentsSamples.kt index e463b0777..1dce80b3c 100644 --- a/core/common/test/samples/format/DateTimeComponentsSamples.kt +++ b/core/common/test/samples/format/DateTimeComponentsSamples.kt @@ -155,6 +155,17 @@ class DateTimeComponentsSamples { check(parsedDate.dayOfWeek == null) } + @Test + fun setMonth() { + val input = "Mon, 30 Jul 2008 11:05:30 GMT" + val parsed = DateTimeComponents.Formats.RFC_1123.parse(input) + check(parsed.monthNumber == 7) + check(parsed.month == Month.JULY) + parsed.month = Month.JUNE + check(parsed.monthNumber == 6) + check(parsed.month == Month.JUNE) + } + @Test fun timeAmPm() { val format = DateTimeComponents.Format { From 33a3717c571382b2e6dc700051f73445e6765df7 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 19 Apr 2024 12:53:23 +0200 Subject: [PATCH 22/35] Provide a one-line description for every sample --- core/common/test/samples/ClockSamples.kt | 2 ++ .../test/samples/DateTimePeriodSamples.kt | 10 ++++++ .../test/samples/DateTimeUnitSamples.kt | 5 +++ core/common/test/samples/DayOfWeekSamples.kt | 3 ++ core/common/test/samples/InstantSamples.kt | 35 +++++++++++++++++++ core/common/test/samples/LocalDateSamples.kt | 32 +++++++++++++++++ .../test/samples/LocalDateTimeSamples.kt | 18 ++++++++++ core/common/test/samples/LocalTimeSamples.kt | 24 +++++++++++++ core/common/test/samples/MonthSamples.kt | 3 ++ core/common/test/samples/TimeZoneSamples.kt | 20 +++++++++++ core/common/test/samples/UtcOffsetSamples.kt | 15 ++++++++ .../format/DateTimeComponentsFormatSamples.kt | 2 ++ .../format/DateTimeComponentsSamples.kt | 24 +++++++++++++ .../format/DateTimeFormatBuilderSamples.kt | 4 +++ .../samples/format/DateTimeFormatSamples.kt | 9 +++++ .../samples/format/LocalDateFormatSamples.kt | 21 +++++++++-- .../format/LocalDateTimeFormatSamples.kt | 1 + .../samples/format/LocalTimeFormatSamples.kt | 4 +++ .../test/samples/format/UnicodeSamples.kt | 1 + .../samples/format/UtcOffsetFormatSamples.kt | 2 ++ 20 files changed, 233 insertions(+), 2 deletions(-) diff --git a/core/common/test/samples/ClockSamples.kt b/core/common/test/samples/ClockSamples.kt index 0687700cb..f48217cb8 100644 --- a/core/common/test/samples/ClockSamples.kt +++ b/core/common/test/samples/ClockSamples.kt @@ -11,6 +11,7 @@ import kotlin.test.* class ClockSamples { @Test fun system() { + // Getting the current date and time val zone = TimeZone.of("Europe/Berlin") val currentInstant = Clock.System.now() val currentLocalDateTime = currentInstant.toLocalDateTime(zone) @@ -19,6 +20,7 @@ class ClockSamples { @Test fun todayIn() { + // Getting the current date in different time zones val clock = object : Clock { override fun now(): Instant = Instant.parse("2020-01-01T02:00:00Z") } diff --git a/core/common/test/samples/DateTimePeriodSamples.kt b/core/common/test/samples/DateTimePeriodSamples.kt index c7e04d85a..0d1636097 100644 --- a/core/common/test/samples/DateTimePeriodSamples.kt +++ b/core/common/test/samples/DateTimePeriodSamples.kt @@ -14,6 +14,7 @@ class DateTimePeriodSamples { @Test fun construction() { + // Constructing a DateTimePeriod using its constructor function val period = DateTimePeriod(years = 5, months = 21, days = 36, seconds = 3601) check(period.years == 6) // 5 years + (21 months / 12) check(period.months == 9) // 21 months % 12 @@ -27,6 +28,7 @@ class DateTimePeriodSamples { @Test fun simpleParsingAndFormatting() { + // Parsing and formatting a DateTimePeriod val string = "-P2M-3DT-4H" val period = DateTimePeriod.parse(string) check(period.toString() == "P-2M3DT4H") @@ -34,6 +36,7 @@ class DateTimePeriodSamples { @Test fun valueNormalization() { + // Reading the normalized values that make up a DateTimePeriod val period = DateTimePeriod( years = -12, months = 122, days = -1440, hours = 400, minutes = -80, seconds = 123, nanoseconds = -123456789 @@ -52,6 +55,7 @@ class DateTimePeriodSamples { @Test fun toStringSample() { + // Formatting a DateTimePeriod to a string check(DateTimePeriod(years = 1, months = 2, days = 3, hours = 4, minutes = 5, seconds = 6, nanoseconds = 7).toString() == "P1Y2M3DT4H5M6.000000007S") check(DateTimePeriod(months = 14, days = -16, hours = 5).toString() == "P1Y2M-16DT5H") check(DateTimePeriod(months = -2, days = -16, hours = -5).toString() == "-P2M16DT5H") @@ -59,6 +63,7 @@ class DateTimePeriodSamples { @Test fun parsing() { + // Parsing a string representation of a DateTimePeriod DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S").apply { check(years == 1) check(months == 2) @@ -84,6 +89,7 @@ class DateTimePeriodSamples { @Test fun constructorFunction() { + // Constructing a DateTimePeriod using its constructor function val dateTimePeriod = DateTimePeriod(months = 16, days = -60, hours = 16, minutes = -61) check(dateTimePeriod.years == 1) // months overflowed to years check(dateTimePeriod.months == 4) // 16 months % 12 @@ -96,6 +102,7 @@ class DateTimePeriodSamples { @Test fun durationToDateTimePeriod() { + // Converting a Duration to a DateTimePeriod that only has time-based components check(130.minutes.toDateTimePeriod() == DateTimePeriod(minutes = 130)) check(2.days.toDateTimePeriod() == DateTimePeriod(days = 0, hours = 48)) } @@ -104,6 +111,7 @@ class DateTimePeriodSamples { @Test fun simpleParsingAndFormatting() { + // Parsing and formatting a DatePeriod val datePeriod1 = DatePeriod(years = 1, days = 3) val string = datePeriod1.toString() check(string == "P1Y3D") @@ -113,6 +121,7 @@ class DateTimePeriodSamples { @Test fun construction() { + // Constructing a DatePeriod using its constructor val datePeriod = DatePeriod(years = 1, months = 16, days = 60) check(datePeriod.years == 2) // 1 year + (16 months / 12) check(datePeriod.months == 4) // 16 months % 12 @@ -126,6 +135,7 @@ class DateTimePeriodSamples { @Test fun parsing() { + // Parsing a string representation of a DatePeriod // ISO duration strings are supported: val datePeriod = DatePeriod.parse("P1Y16M60D") check(datePeriod == DatePeriod(years = 2, months = 4, days = 60)) diff --git a/core/common/test/samples/DateTimeUnitSamples.kt b/core/common/test/samples/DateTimeUnitSamples.kt index f335e8958..bea41aa58 100644 --- a/core/common/test/samples/DateTimeUnitSamples.kt +++ b/core/common/test/samples/DateTimeUnitSamples.kt @@ -12,6 +12,7 @@ import kotlin.time.Duration.Companion.hours class DateTimeUnitSamples { @Test fun construction() { + // Constructing various measurement units check(DateTimeUnit.HOUR == DateTimeUnit.TimeBased(nanoseconds = 60 * 60 * 1_000_000_000L)) check(DateTimeUnit.WEEK == DateTimeUnit.DayBased(days = 7)) check(DateTimeUnit.WEEK * 2 == DateTimeUnit.DayBased(days = 14)) @@ -20,12 +21,14 @@ class DateTimeUnitSamples { @Test fun multiplication() { + // Obtaining a measurement unit that's several times larger than another one val twoWeeks = DateTimeUnit.WEEK * 2 check(twoWeeks.days == 14) } @Test fun timeBasedUnit() { + // Constructing various time-based measurement units val halfDay = DateTimeUnit.TimeBased(nanoseconds = 12 * 60 * 60 * 1_000_000_000L) check(halfDay.nanoseconds == 12 * 60 * 60 * 1_000_000_000L) check(halfDay.duration == 12.hours) @@ -36,6 +39,7 @@ class DateTimeUnitSamples { @Test fun dayBasedUnit() { + // Constructing various day-based measurement units val iteration = DateTimeUnit.DayBased(days = 14) check(iteration.days == 14) check(iteration == DateTimeUnit.DAY * 14) @@ -44,6 +48,7 @@ class DateTimeUnitSamples { @Test fun monthBasedUnit() { + // Constructing various month-based measurement units val halfYear = DateTimeUnit.MonthBased(months = 6) check(halfYear.months == 6) check(halfYear == DateTimeUnit.QUARTER * 2) diff --git a/core/common/test/samples/DayOfWeekSamples.kt b/core/common/test/samples/DayOfWeekSamples.kt index 8403f0bb1..c8393ae98 100644 --- a/core/common/test/samples/DayOfWeekSamples.kt +++ b/core/common/test/samples/DayOfWeekSamples.kt @@ -12,6 +12,7 @@ class DayOfWeekSamples { @Test fun usage() { + // Providing different behavior based on what day of the week it is today val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) when (today.dayOfWeek) { DayOfWeek.MONDAY -> check(today.dayOfWeek.isoDayNumber == 1) @@ -27,6 +28,7 @@ class DayOfWeekSamples { @Test fun isoDayNumber() { + // Getting the ISO day-of-week number check(DayOfWeek.MONDAY.isoDayNumber == 1) check(DayOfWeek.TUESDAY.isoDayNumber == 2) // ... @@ -35,6 +37,7 @@ class DayOfWeekSamples { @Test fun constructorFunction() { + // Constructing a DayOfWeek from the ISO day-of-week number check(DayOfWeek(isoDayNumber = 1) == DayOfWeek.MONDAY) check(DayOfWeek(isoDayNumber = 2) == DayOfWeek.TUESDAY) // ... diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index 3b9ecf94c..465fc6338 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -15,6 +15,7 @@ class InstantSamples { @Test fun epochSeconds() { + // Getting the number of whole seconds that passed since the Unix epoch val instant1 = Instant.fromEpochSeconds(999_999, nanosecondAdjustment = 123_456_789) check(instant1.epochSeconds == 999_999L) val instant2 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = 100_123_456_789) @@ -25,6 +26,7 @@ class InstantSamples { @Test fun nanosecondsOfSecond() { + // Getting the number of nanoseconds that passed since the start of the second val instant1 = Instant.fromEpochSeconds(999_999, nanosecondAdjustment = 123_456_789) check(instant1.nanosecondsOfSecond == 123_456_789) val instant2 = Instant.fromEpochSeconds(1_000_000, nanosecondAdjustment = 100_123_456_789) @@ -35,6 +37,7 @@ class InstantSamples { @Test fun toEpochMilliseconds() { + // Converting an Instant to the number of milliseconds since the Unix epoch check(Instant.fromEpochMilliseconds(0).toEpochMilliseconds() == 0L) check(Instant.fromEpochMilliseconds(1_000_000_000_123).toEpochMilliseconds() == 1_000_000_000_123L) check(Instant.fromEpochSeconds(1_000_000_000, nanosecondAdjustment = 123_999_999) @@ -43,6 +46,7 @@ class InstantSamples { @Test fun plusDuration() { + // Finding a moment that's later than the starting point by the given amount of real time val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val fiveHoursLater = instant + 5.hours check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) @@ -50,6 +54,7 @@ class InstantSamples { @Test fun minusDuration() { + // Finding a moment that's earlier than the starting point by the given amount of real time val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val fiveHoursEarlier = instant - 5.hours check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) @@ -57,11 +62,13 @@ class InstantSamples { @Test fun minusInstant() { + // Finding the difference between two instants in terms of elapsed time check(Instant.fromEpochSeconds(0) - Instant.fromEpochSeconds(epochSeconds = 7 * 60 * 60) == (-7).hours) } @Test fun compareToSample() { + // Finding out which of two instants is earlier fun randomInstant() = Instant.fromEpochMilliseconds( Random.nextLong(Instant.DISTANT_PAST.toEpochMilliseconds(), Instant.DISTANT_FUTURE.toEpochMilliseconds()) ) @@ -74,11 +81,13 @@ class InstantSamples { @Test fun toStringSample() { + // Converting an Instant to a string check(Instant.fromEpochMilliseconds(0).toString() == "1970-01-01T00:00:00Z") } @Test fun fromEpochMilliseconds() { + // Constructing an Instant from the number of milliseconds since the Unix epoch check(Instant.fromEpochMilliseconds(epochMilliseconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) check(Instant.fromEpochMilliseconds(epochMilliseconds = 1_000_000_000_123) == Instant.parse("2001-09-09T01:46:40.123Z")) @@ -86,6 +95,7 @@ class InstantSamples { @Test fun fromEpochSeconds() { + // Constructing an Instant from the number of seconds and nanoseconds since the Unix epoch check(Instant.fromEpochSeconds(epochSeconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) check(Instant.fromEpochSeconds(epochSeconds = 1_000_001_234, nanosecondAdjustment = -1_234_000_000_001) == Instant.parse("2001-09-09T01:46:39.999999999Z")) @@ -93,18 +103,21 @@ class InstantSamples { @Test fun fromEpochSecondsIntNanos() { + // Constructing an Instant from the number of seconds and nanoseconds since the Unix epoch check(Instant.fromEpochSeconds(epochSeconds = 0) == Instant.parse("1970-01-01T00:00:00Z")) check(Instant.fromEpochSeconds(epochSeconds = 1_000_000_000, nanosecondAdjustment = -1) == Instant.parse("2001-09-09T01:46:39.999999999Z")) } @Test fun parsing() { + // Parsing an Instant from a string using predefined and custom formats check(Instant.parse("1970-01-01T00:00:00Z") == Instant.fromEpochMilliseconds(0)) check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochMilliseconds(0)) } @Test fun isDistantPast() { + // Checking if an instant is so far in the past that it's probably irrelevant val currentInstant = Clock.System.now() val tenThousandYearsAgo = currentInstant.minus(1_000, DateTimeUnit.YEAR, TimeZone.UTC) check(!tenThousandYearsAgo.isDistantPast) @@ -113,6 +126,7 @@ class InstantSamples { @Test fun isDistantFuture() { + // Checking if an instant is so far in the future that it's probably irrelevant val currentInstant = Clock.System.now() val tenThousandYearsLater = currentInstant.plus(10_000, DateTimeUnit.YEAR, TimeZone.UTC) check(!tenThousandYearsLater.isDistantFuture) @@ -121,6 +135,7 @@ class InstantSamples { @Test fun plusPeriod() { + // Finding a moment that's later than the starting point by the given length of calendar time val startInstant = Instant.parse("2024-03-09T07:16:39.688Z") val period = DateTimePeriod(months = 1, days = -1) // one day short from a month later val afterPeriodInBerlin = startInstant.plus(period, TimeZone.of("Europe/Berlin")) @@ -131,6 +146,7 @@ class InstantSamples { @Test fun minusPeriod() { + // Finding a moment that's earlier than the starting point by the given length of calendar time val period = DateTimePeriod(months = 1, days = -1) // one day short from a month earlier val startInstant = Instant.parse("2024-03-23T16:50:41.926Z") val afterPeriodInBerlin = startInstant.minus(period, TimeZone.of("Europe/Berlin")) @@ -142,6 +158,7 @@ class InstantSamples { /** copy of [minusInstantInZone] */ @Test fun periodUntil() { + // Finding a period that it would take to get from the starting instant to the ending instant val startInstant = Instant.parse("2024-01-01T02:00:00Z") val endInstant = Instant.parse("2024-03-01T03:15:03Z") // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months @@ -155,6 +172,7 @@ class InstantSamples { /** copy of [minusAsDateTimeUnit] */ @Test fun untilAsDateTimeUnit() { + // Finding the difference between two instants in terms of the given calendar-based measurement unit val startInstant = Instant.parse("2024-01-01T02:00:00Z") val endInstant = Instant.parse("2024-03-01T02:00:00Z") // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months @@ -168,6 +186,7 @@ class InstantSamples { /** copy of [minusAsTimeBasedUnit] */ @Test fun untilAsTimeBasedUnit() { + // Finding the difference between two instants in terms of the given measurement unit val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val hoursBetweenInstants = instant.until(otherInstant, DateTimeUnit.HOUR) @@ -176,6 +195,7 @@ class InstantSamples { @Test fun daysUntil() { + // Finding the number of full days between two instants in the given time zone val startInstant = Instant.parse("2023-03-26T00:30:00Z") val endInstant = Instant.parse("2023-03-28T00:15:00Z") // In New York, these days are both 24 hour long, so the difference is 15 minutes short of 2 days @@ -188,6 +208,7 @@ class InstantSamples { @Test fun monthsUntil() { + // Finding the number of months between two instants in the given time zone val startInstant = Instant.parse("2024-01-01T02:00:00Z") val endInstant = Instant.parse("2024-03-01T02:00:00Z") // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months @@ -200,6 +221,7 @@ class InstantSamples { @Test fun yearsUntil() { + // Finding the number of full years between two instants in the given time zone val startInstant = Instant.parse("2024-03-01T02:01:00Z") val endInstant = Instant.parse("2025-03-01T02:01:00Z") // In New York, we find the difference between 2024-02-29 and 2025-02-28, which is just short of a year @@ -213,6 +235,7 @@ class InstantSamples { /** copy of [periodUntil] */ @Test fun minusInstantInZone() { + // Finding a period that it would take to get from the starting instant to the ending instant val startInstant = Instant.parse("2024-01-01T02:00:00Z") val endInstant = Instant.parse("2024-03-01T03:15:03Z") // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months @@ -225,6 +248,7 @@ class InstantSamples { @Test fun plusDateTimeUnit() { + // Finding a moment that's later than the starting point by the given length of calendar time val startInstant = Instant.parse("2024-04-05T22:51:45.586Z") val twoYearsLaterInBerlin = startInstant.plus(2, DateTimeUnit.YEAR, TimeZone.of("Europe/Berlin")) check(twoYearsLaterInBerlin == Instant.parse("2026-04-05T22:51:45.586Z")) @@ -234,6 +258,7 @@ class InstantSamples { @Test fun minusDateTimeUnit() { + // Finding a moment that's earlier than the starting point by the given length of calendar time val startInstant = Instant.parse("2024-03-28T02:04:56.256Z") val twoYearsEarlierInBerlin = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Europe/Berlin")) check(twoYearsEarlierInBerlin == Instant.parse("2022-03-28T01:04:56.256Z")) @@ -243,6 +268,7 @@ class InstantSamples { @Test fun plusTimeBasedUnit() { + // Finding a moment that's later than the starting point by the given amount of real time val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val fiveHoursLater = instant.plus(5, DateTimeUnit.HOUR) check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) @@ -250,6 +276,7 @@ class InstantSamples { @Test fun minusTimeBasedUnit() { + // Finding a moment that's earlier than the starting point by the given amount of real time val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val fiveHoursEarlier = instant.minus(5, DateTimeUnit.HOUR) check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) @@ -258,6 +285,7 @@ class InstantSamples { @Test @Ignore // only the JVM has the range wide enough fun plusDateTimeUnitLong() { + // Finding a moment that's later than the starting point by the given large length of calendar time val zone = TimeZone.of("Europe/Berlin") val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) val tenTrillionDaysLater = now.plus(10_000_000_000L, DateTimeUnit.DAY, zone) @@ -267,6 +295,7 @@ class InstantSamples { @Test @Ignore // only the JVM has the range wide enough fun minusDateTimeUnitLong() { + // Finding a moment that's earlier than the starting point by the given large length of calendar time val zone = TimeZone.of("Europe/Berlin") val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) val tenTrillionDaysAgo = now.minus(10_000_000_000L, DateTimeUnit.DAY, zone) @@ -275,6 +304,7 @@ class InstantSamples { @Test fun plusTimeBasedUnitLong() { + // Finding a moment that's later than the starting point by the given amount of real time val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) val quadrillion = 1_000_000_000_000L val quadrillionSecondsLater = startInstant.plus(quadrillion, DateTimeUnit.SECOND) @@ -283,6 +313,7 @@ class InstantSamples { @Test fun minusTimeBasedUnitLong() { + // Finding a moment that's earlier than the starting point by the given amount of real time val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) val quadrillion = 1_000_000_000_000L val quadrillionSecondsEarlier = startInstant.minus(quadrillion, DateTimeUnit.SECOND) @@ -292,6 +323,7 @@ class InstantSamples { /** copy of [untilAsDateTimeUnit] */ @Test fun minusAsDateTimeUnit() { + // Finding a moment that's earlier than the starting point by the given length of calendar time val startInstant = Instant.parse("2024-01-01T02:00:00Z") val endInstant = Instant.parse("2024-03-01T02:00:00Z") // In New York, we find the difference between 2023-12-31 and 2024-02-29, which is just short of two months @@ -305,6 +337,7 @@ class InstantSamples { /** copy of [untilAsTimeBasedUnit] */ @Test fun minusAsTimeBasedUnit() { + // Finding a moment that's earlier than the starting point by a given amount of real time val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) val hoursBetweenInstants = otherInstant.minus(instant, DateTimeUnit.HOUR) @@ -313,7 +346,9 @@ class InstantSamples { @Test fun formatting() { + // Formatting an Instant to a string using predefined and custom formats val epochStart = Instant.fromEpochMilliseconds(0) + check(epochStart.toString() == "1970-01-01T00:00:00Z") check(epochStart.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) == "1970-01-01T00:00:00Z") val customFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO_BASIC) diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index 885af3866..32a29cbac 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -14,12 +14,14 @@ class LocalDateSamples { @Test fun simpleParsingAndFormatting() { + // Parsing and formatting LocalDate values check(LocalDate.parse("2023-01-02") == LocalDate(2023, Month.JANUARY, 2)) check(LocalDate(2023, Month.JANUARY, 2).toString() == "2023-01-02") } @Test fun parsing() { + // Parsing LocalDate values using predefined and custom formats check(LocalDate.parse("2024-04-16") == LocalDate(2024, Month.APRIL, 16)) val customFormat = LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() @@ -29,6 +31,7 @@ class LocalDateSamples { @Test fun fromAndToEpochDays() { + // Converting LocalDate values to the number of days since 1970-01-01 and back check(LocalDate.fromEpochDays(0) == LocalDate(1970, Month.JANUARY, 1)) val randomEpochDay = Random.nextInt(-50_000..50_000) val randomDate = LocalDate.fromEpochDays(randomEpochDay) @@ -37,6 +40,7 @@ class LocalDateSamples { @Test fun customFormat() { + // Parsing and formatting LocalDate values using a custom format val customFormat = LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() } @@ -48,6 +52,7 @@ class LocalDateSamples { @Test fun constructorFunctionMonthNumber() { + // Constructing a LocalDate value using its constructor val date = LocalDate(2024, 4, 16) check(date.year == 2024) check(date.monthNumber == 4) @@ -57,6 +62,7 @@ class LocalDateSamples { @Test fun constructorFunction() { + // Constructing a LocalDate value using its constructor val date = LocalDate(2024, Month.APRIL, 16) check(date.year == 2024) check(date.month == Month.APRIL) @@ -65,6 +71,7 @@ class LocalDateSamples { @Test fun year() { + // Getting the year check(LocalDate(2024, Month.APRIL, 16).year == 2024) check(LocalDate(0, Month.APRIL, 16).year == 0) check(LocalDate(-2024, Month.APRIL, 16).year == -2024) @@ -72,6 +79,7 @@ class LocalDateSamples { @Test fun month() { + // Getting the month for (month in Month.entries) { check(LocalDate(2024, month, 16).month == month) } @@ -79,6 +87,7 @@ class LocalDateSamples { @Test fun dayOfMonth() { + // Getting the day of the month repeat(30) { val dayOfMonth = it + 1 check(LocalDate(2024, Month.APRIL, dayOfMonth).dayOfMonth == dayOfMonth) @@ -87,6 +96,7 @@ class LocalDateSamples { @Test fun dayOfWeek() { + // Getting the day of the week check(LocalDate(2024, Month.APRIL, 16).dayOfWeek == DayOfWeek.TUESDAY) check(LocalDate(2024, Month.APRIL, 17).dayOfWeek == DayOfWeek.WEDNESDAY) check(LocalDate(2024, Month.APRIL, 18).dayOfWeek == DayOfWeek.THURSDAY) @@ -94,6 +104,7 @@ class LocalDateSamples { @Test fun dayOfYear() { + // Getting the 1-based day of the year check(LocalDate(2024, Month.APRIL, 16).dayOfYear == 107) check(LocalDate(2024, Month.JANUARY, 1).dayOfYear == 1) check(LocalDate(2024, Month.DECEMBER, 31).dayOfYear == 366) @@ -101,6 +112,7 @@ class LocalDateSamples { @Test fun toEpochDays() { + // Converting LocalDate values to the number of days since 1970-01-01 check(LocalDate(2024, Month.APRIL, 16).toEpochDays() == 19829) check(LocalDate(1970, Month.JANUARY, 1).toEpochDays() == 0) check(LocalDate(1969, Month.DECEMBER, 25).toEpochDays() == -7) @@ -108,6 +120,7 @@ class LocalDateSamples { @Test fun compareToSample() { + // Comparing LocalDate values check(LocalDate(2023, 4, 16) < LocalDate(2024, 3, 15)) check(LocalDate(2023, 4, 16) < LocalDate(2023, 5, 15)) check(LocalDate(2023, 4, 16) < LocalDate(2023, 4, 17)) @@ -116,6 +129,7 @@ class LocalDateSamples { @Test fun toStringSample() { + // Converting LocalDate values to strings check(LocalDate(2024, 4, 16).toString() == "2024-04-16") check(LocalDate(12024, 4, 16).toString() == "+12024-04-16") check(LocalDate(-2024, 4, 16).toString() == "-2024-04-16") @@ -123,6 +137,8 @@ class LocalDateSamples { @Test fun formatting() { + // Formatting a LocalDate value using predefined and custom formats + check(LocalDate(2024, 4, 16).toString() == "2024-04-16") check(LocalDate(2024, 4, 16).format(LocalDate.Formats.ISO) == "2024-04-16") val customFormat = LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); chars(", "); year() @@ -132,6 +148,7 @@ class LocalDateSamples { @Test fun atTimeInline() { + // Constructing a LocalDateTime value from a LocalDate and a LocalTime val date = LocalDate(2024, Month.APRIL, 16) val dateTime = date.atTime(13, 30) check(dateTime == LocalDateTime(2024, Month.APRIL, 16, 13, 30)) @@ -139,6 +156,7 @@ class LocalDateSamples { @Test fun atTime() { + // Constructing a LocalDateTime value from a LocalDate and a LocalTime val date = LocalDate(2024, Month.APRIL, 16) val time = LocalTime(13, 30) val dateTime = date.atTime(time) @@ -147,6 +165,7 @@ class LocalDateSamples { @Test fun plusPeriod() { + // Finding a date that's a given period after another date val startDate = LocalDate(2021, Month.OCTOBER, 30) check(startDate + DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2023, Month.JANUARY, 2)) // Step by step explanation: @@ -159,6 +178,7 @@ class LocalDateSamples { @Test fun minusPeriod() { + // Finding a date that's a given period before another date val startDate = LocalDate(2023, Month.JANUARY, 2) check(startDate - DatePeriod(years = 1, months = 2, days = 3) == LocalDate(2021, Month.OCTOBER, 30)) // Step by step explanation: @@ -171,6 +191,7 @@ class LocalDateSamples { @Test fun periodUntil() { + // Finding the period between two dates val startDate = LocalDate(2023, Month.JANUARY, 2) val endDate = LocalDate(2024, Month.APRIL, 1) val period = startDate.periodUntil(endDate) @@ -179,6 +200,7 @@ class LocalDateSamples { @Test fun minusDate() { + // Finding the period between two dates val startDate = LocalDate(2023, Month.JANUARY, 2) val endDate = LocalDate(2024, Month.APRIL, 1) val period = endDate - startDate @@ -187,6 +209,7 @@ class LocalDateSamples { @Test fun until() { + // Measuring the difference between two dates in terms of the given unit val startDate = LocalDate(2023, Month.JANUARY, 2) val endDate = LocalDate(2024, Month.APRIL, 1) val differenceInMonths = startDate.until(endDate, DateTimeUnit.MONTH) @@ -196,6 +219,7 @@ class LocalDateSamples { @Test fun daysUntil() { + // Finding how many days have passed between two dates val dateOfConcert = LocalDate(2024, Month.SEPTEMBER, 26) val today = LocalDate(2024, Month.APRIL, 16) val daysUntilConcert = today.daysUntil(dateOfConcert) @@ -204,6 +228,7 @@ class LocalDateSamples { @Test fun monthsUntil() { + // Finding how many months have passed between two dates val babyDateOfBirth = LocalDate(2023, Month.DECEMBER, 14) val today = LocalDate(2024, Month.APRIL, 16) val ageInMonths = babyDateOfBirth.monthsUntil(today) @@ -212,6 +237,7 @@ class LocalDateSamples { @Test fun yearsUntil() { + // Finding how many years have passed between two dates val dateOfBirth = LocalDate(2016, Month.JANUARY, 14) val today = LocalDate(2024, Month.APRIL, 16) val age = dateOfBirth.yearsUntil(today) @@ -220,6 +246,7 @@ class LocalDateSamples { @Test fun plusInt() { + // Adding a number of days or months to a date val today = LocalDate(2024, Month.APRIL, 16) val tenDaysLater = today.plus(10, DateTimeUnit.DAY) check(tenDaysLater == LocalDate(2024, Month.APRIL, 26)) @@ -229,6 +256,7 @@ class LocalDateSamples { @Test fun minusInt() { + // Subtracting a number of days or months from a date val today = LocalDate(2024, Month.APRIL, 16) val tenDaysAgo = today.minus(10, DateTimeUnit.DAY) check(tenDaysAgo == LocalDate(2024, Month.APRIL, 6)) @@ -239,6 +267,7 @@ class LocalDateSamples { @Test @Ignore // only the JVM has the range wide enough fun plusLong() { + // Adding a large number of days to a date val today = LocalDate(2024, Month.APRIL, 16) val tenTrillionDaysLater = today.plus(10_000_000_000L, DateTimeUnit.DAY) check(tenTrillionDaysLater == LocalDate(27_381_094, Month.MAY, 12)) @@ -247,6 +276,7 @@ class LocalDateSamples { @Test @Ignore // only the JVM has the range wide enough fun minusLong() { + // Subtracting a large number of days from a date val today = LocalDate(2024, Month.APRIL, 16) val tenTrillionDaysAgo = today.minus(10_000_000_000L, DateTimeUnit.DAY) check(tenTrillionDaysAgo == LocalDate(-27_377_046, Month.MARCH, 22)) @@ -255,6 +285,7 @@ class LocalDateSamples { class Formats { @Test fun iso() { + // Using the extended ISO format for parsing and formatting LocalDate values val date = LocalDate.Formats.ISO.parse("2024-04-16") check(date == LocalDate(2024, Month.APRIL, 16)) val formatted = LocalDate.Formats.ISO.format(date) @@ -263,6 +294,7 @@ class LocalDateSamples { @Test fun isoBasic() { + // Using the basic ISO format for parsing and formatting LocalDate values val date = LocalDate.Formats.ISO_BASIC.parse("20240416") check(date == LocalDate(2024, Month.APRIL, 16)) val formatted = LocalDate.Formats.ISO_BASIC.format(date) diff --git a/core/common/test/samples/LocalDateTimeSamples.kt b/core/common/test/samples/LocalDateTimeSamples.kt index fcdd5e52b..7b8c7ae90 100644 --- a/core/common/test/samples/LocalDateTimeSamples.kt +++ b/core/common/test/samples/LocalDateTimeSamples.kt @@ -13,6 +13,7 @@ class LocalDateTimeSamples { @Test fun alternativeConstruction() { + // Constructing a LocalDateTime value by specifying its components val dateTime1 = LocalDateTime(year = 2021, monthNumber = 3, dayOfMonth = 27, hour = 2, minute = 16, second = 20) val dateTime2 = LocalDateTime( year = 2021, month = Month.MARCH, dayOfMonth = 27, @@ -23,6 +24,7 @@ class LocalDateTimeSamples { @Test fun simpleParsingAndFormatting() { + // Parsing and formatting LocalDateTime values val dateTime = LocalDateTime.parse("2024-02-15T08:30:15.1234567") check(dateTime == LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_456_700)) val formatted = dateTime.toString() @@ -31,6 +33,7 @@ class LocalDateTimeSamples { @Test fun parsing() { + // Parsing LocalDateTime values using predefined and custom formats check(LocalDateTime.parse("2024-02-15T08:30:15.123456789") == LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_456_789)) check(LocalDateTime.parse("2024-02-15T08:30") == @@ -47,6 +50,7 @@ class LocalDateTimeSamples { @Test fun customFormat() { + // Parsing and formatting LocalDateTime values using a custom format val customFormat = LocalDateTime.Format { date(LocalDate.Formats.ISO) char(' ') @@ -56,11 +60,15 @@ class LocalDateTimeSamples { val dateTime = LocalDate(2024, 2, 15) .atTime(8, 30, 15, 123_456_789) check(dateTime.format(customFormat) == "2024-02-15 08:30:15,123") + check(customFormat.parse("2024-02-15 08:30:15,123") == + LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_000_000) + ) check(dateTime.format(LocalDateTime.Formats.ISO) == "2024-02-15T08:30:15.123456789") } @Test fun constructorFunctionWithMonthNumber() { + // Constructing a LocalDateTime value using its constructor val dateTime = LocalDateTime( year = 2024, monthNumber = 2, @@ -85,6 +93,7 @@ class LocalDateTimeSamples { @Test fun constructorFunction() { + // Constructing a LocalDateTime value using its constructor val dateTime = LocalDateTime( year = 2024, month = Month.FEBRUARY, @@ -109,6 +118,7 @@ class LocalDateTimeSamples { @Test fun fromDateAndTime() { + // Converting a LocalDate and a LocalTime to a LocalDateTime value and getting them back val date = LocalDate(2024, 2, 15) val time = LocalTime(16, 48) val dateTime = LocalDateTime(date, time) @@ -120,6 +130,7 @@ class LocalDateTimeSamples { @Test fun dateComponents() { + // Accessing the date components of a LocalDateTime value val date = LocalDate(2024, 2, 15) val time = LocalTime(hour = 16, minute = 48, second = 59, nanosecond = 999_999_999) val dateTime = LocalDateTime(date, time) @@ -133,6 +144,7 @@ class LocalDateTimeSamples { @Test fun timeComponents() { + // Accessing the time components of a LocalDateTime value val date = LocalDate(2024, 2, 15) val time = LocalTime(hour = 16, minute = 48, second = 59, nanosecond = 999_999_999) val dateTime = LocalDateTime(date, time) @@ -144,6 +156,7 @@ class LocalDateTimeSamples { @Test fun dateAndTime() { + // Constructing a LocalDateTime value from a LocalDate and a LocalTime val date = LocalDate(2024, 2, 15) val time = LocalTime(16, 48) val dateTime = LocalDateTime(date, time) @@ -153,6 +166,7 @@ class LocalDateTimeSamples { @Test fun compareToSample() { + // Comparing LocalDateTime values val date = LocalDate(2024, 2, 15) val laterDate = LocalDate(2024, 2, 16) check(date.atTime(hour = 23, minute = 59) < laterDate.atTime(hour = 0, minute = 0)) @@ -164,6 +178,7 @@ class LocalDateTimeSamples { @Test fun toStringSample() { + // Converting LocalDateTime values to strings check(LocalDate(2024, 2, 15).atTime(16, 48).toString() == "2024-02-15T16:48") check(LocalDate(2024, 2, 15).atTime(16, 48, 15).toString() == "2024-02-15T16:48:15") check(LocalDate(2024, 2, 15).atTime(16, 48, 15, 120_000_000).toString() == "2024-02-15T16:48:15.120") @@ -171,6 +186,8 @@ class LocalDateTimeSamples { @Test fun formatting() { + // Formatting LocalDateTime values using predefined and custom formats + check(LocalDate(2024, 2, 15).atTime(16, 48).toString() == "2024-02-15T16:48") check(LocalDate(2024, 2, 15).atTime(16, 48).format(LocalDateTime.Formats.ISO) == "2024-02-15T16:48:00") val customFormat = LocalDateTime.Format { date(LocalDate.Formats.ISO) @@ -192,6 +209,7 @@ class LocalDateTimeSamples { class Formats { @Test fun iso() { + // Parsing and formatting LocalDateTime values using the ISO format val dateTime1 = LocalDate(2024, 2, 15) .atTime(hour = 8, minute = 30, second = 15, nanosecond = 160_000_000) val dateTime2 = LocalDate(2024, 2, 15) diff --git a/core/common/test/samples/LocalTimeSamples.kt b/core/common/test/samples/LocalTimeSamples.kt index 5f6ae3581..df75f6ce2 100644 --- a/core/common/test/samples/LocalTimeSamples.kt +++ b/core/common/test/samples/LocalTimeSamples.kt @@ -14,6 +14,7 @@ class LocalTimeSamples { @Test fun construction() { + // Constructing a LocalTime using its constructor val night = LocalTime(hour = 23, minute = 13, second = 16, nanosecond = 153_200_001) check(night.hour == 23) check(night.minute == 13) @@ -32,6 +33,7 @@ class LocalTimeSamples { @Test fun representingAsNumbers() { + // Representing a LocalTime as the number of seconds, milliseconds, or nanoseconds since the start of the day val time = LocalTime(hour = 8, minute = 30, second = 15, nanosecond = 123_456_789) // The number of whole seconds since the start of the day: val timeAsSecondOfDay = time.toSecondOfDay() @@ -49,6 +51,7 @@ class LocalTimeSamples { @Test fun simpleParsingAndFormatting() { + // Parsing a LocalTime from a string and formatting it back val time = LocalTime.parse("08:30:15.1234567") check(time == LocalTime(8, 30, 15, 123_456_700)) val formatted = time.toString() @@ -57,6 +60,7 @@ class LocalTimeSamples { @Test fun parsing() { + // Parsing a LocalTime from a string using predefined and custom formats check(LocalTime.parse("08:30:15.123456789") == LocalTime(8, 30, 15, 123_456_789)) check(LocalTime.parse("08:30:15") == LocalTime(8, 30, 15)) check(LocalTime.parse("08:30") == LocalTime(8, 30)) @@ -70,6 +74,7 @@ class LocalTimeSamples { @Test fun fromAndToSecondOfDay() { + // Converting a LocalTime to the number of seconds since the start of the day and back val secondsInDay = 24 * 60 * 60 val randomNumberOfSeconds = Random.nextInt(secondsInDay) val time = LocalTime.fromSecondOfDay(randomNumberOfSeconds) @@ -79,6 +84,7 @@ class LocalTimeSamples { @Test fun fromAndToMillisecondOfDay() { + // Converting a LocalTime to the number of milliseconds since the start of the day and back val millisecondsInDay = 24 * 60 * 60 * 1_000 val randomNumberOfMilliseconds = Random.nextInt(millisecondsInDay) val time = LocalTime.fromMillisecondOfDay(randomNumberOfMilliseconds) @@ -88,6 +94,7 @@ class LocalTimeSamples { @Test fun fromAndToNanosecondOfDay() { + // Converting a LocalTime to the number of nanoseconds since the start of the day and back val originalTime = LocalTime( hour = Random.nextInt(24), minute = Random.nextInt(60), @@ -101,6 +108,7 @@ class LocalTimeSamples { @Test fun customFormat() { + // Parsing and formatting LocalTime values using a custom format val customFormat = LocalTime.Format { hour(); char(':'); minute(); char(':'); second() char(','); secondFraction(fixedLength = 3) @@ -112,6 +120,7 @@ class LocalTimeSamples { @Test fun constructorFunction() { + // Constructing a LocalTime using its constructor val time = LocalTime(8, 30, 15, 123_456_789) check(time.hour == 8) check(time.minute == 30) @@ -126,22 +135,26 @@ class LocalTimeSamples { @Test fun hour() { + // Getting the number of whole hours shown on the clock check(LocalTime(8, 30, 15, 123_456_789).hour == 8) } @Test fun minute() { + // Getting the number of whole minutes that don't form a whole hour check(LocalTime(8, 30, 15, 123_456_789).minute == 30) } @Test fun second() { + // Getting the number of whole seconds that don't form a whole minute check(LocalTime(8, 30).second == 0) check(LocalTime(8, 30, 15, 123_456_789).second == 15) } @Test fun nanosecond() { + // Getting the sub-second part of a LocalTime check(LocalTime(8, 30).nanosecond == 0) check(LocalTime(8, 30, 15).nanosecond == 0) check(LocalTime(8, 30, 15, 123_456_789).nanosecond == 123_456_789) @@ -149,6 +162,7 @@ class LocalTimeSamples { @Test fun toSecondOfDay() { + // Obtaining the number of seconds a clock has to advance since 00:00 to reach the given time check(LocalTime(0, 0, 0, 0).toSecondOfDay() == 0) check(LocalTime(0, 0, 0, 1).toSecondOfDay() == 0) check(LocalTime(0, 0, 1, 0).toSecondOfDay() == 1) @@ -160,6 +174,7 @@ class LocalTimeSamples { @Test fun toMillisecondOfDay() { + // Obtaining the number of milliseconds a clock has to advance since 00:00 to reach the given time check(LocalTime(0, 0, 0, 0).toMillisecondOfDay() == 0) check(LocalTime(0, 0, 0, 1).toMillisecondOfDay() == 0) check(LocalTime(0, 0, 1, 0).toMillisecondOfDay() == 1000) @@ -171,6 +186,7 @@ class LocalTimeSamples { @Test fun toNanosecondOfDay() { + // Obtaining the number of nanoseconds a clock has to advance since 00:00 to reach the given time check(LocalTime(0, 0, 0, 0).toNanosecondOfDay() == 0L) check(LocalTime(0, 0, 0, 1).toNanosecondOfDay() == 1L) check(LocalTime(0, 0, 1, 0).toNanosecondOfDay() == 1_000_000_000L) @@ -181,6 +197,7 @@ class LocalTimeSamples { @Test fun compareTo() { + // Comparing LocalTime values check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 17, minute = 10)) check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 31)) check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 30, second = 1)) @@ -189,6 +206,7 @@ class LocalTimeSamples { @Test fun toStringSample() { + // Converting a LocalTime to a human-readable string check(LocalTime(hour = 8, minute = 30).toString() == "08:30") check(LocalTime(hour = 8, minute = 30, second = 15).toString() == "08:30:15") check(LocalTime(hour = 8, minute = 30, second = 0, nanosecond = 120000000).toString() == "08:30:00.120") @@ -196,6 +214,8 @@ class LocalTimeSamples { @Test fun formatting() { + // Formatting LocalTime values using predefined and custom formats + check(LocalTime(hour = 8, minute = 30).toString() == "08:30") check(LocalTime(hour = 8, minute = 30).format(LocalTime.Formats.ISO) == "08:30:00") val customFormat = LocalTime.Format { hour(); char(':'); minute() @@ -217,6 +237,7 @@ class LocalTimeSamples { */ @Test fun atDateComponentWiseMonthNumber() { + // Using the `atDate` function to covert a sequence of `LocalDate` values to `LocalDateTime` val morning = LocalTime(8, 30) val firstMorningOfEveryMonth = (1..12).map { month -> morning.atDate(2021, month, 1) @@ -229,6 +250,7 @@ class LocalTimeSamples { */ @Test fun atDateComponentWise() { + // Using the `atDate` function to covert a sequence of `LocalDate` values to `LocalDateTime` val morning = LocalTime(8, 30) val firstMorningOfEveryMonth = Month.entries.map { month -> morning.atDate(2021, month, 1) @@ -238,6 +260,7 @@ class LocalTimeSamples { @Test fun atDate() { + // Using the `atDate` function to covert a sequence of `LocalDate` values to `LocalDateTime` val workdayStart = LocalTime(8, 30) val startDate = LocalDate(2021, Month.JANUARY, 1) val endDate = LocalDate(2021, Month.DECEMBER, 31) @@ -259,6 +282,7 @@ class LocalTimeSamples { class Formats { @Test fun iso() { + // Parsing and formatting LocalTime values using the ISO format val timeWithNanoseconds = LocalTime(hour = 8, minute = 30, second = 15, nanosecond = 160_000_000) val timeWithSeconds = LocalTime(hour = 8, minute = 30, second = 15) val timeWithoutSeconds = LocalTime(hour = 8, minute = 30) diff --git a/core/common/test/samples/MonthSamples.kt b/core/common/test/samples/MonthSamples.kt index 60708e1f5..70128845a 100644 --- a/core/common/test/samples/MonthSamples.kt +++ b/core/common/test/samples/MonthSamples.kt @@ -12,6 +12,7 @@ class MonthSamples { @Test fun usage() { + // Providing different behavior based on what month it is today val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) when (today.month) { Month.JANUARY -> check(today.month.number == 1) @@ -32,6 +33,7 @@ class MonthSamples { @Test fun number() { + // Getting the number of a month check(Month.JANUARY.number == 1) check(Month.FEBRUARY.number == 2) // ... @@ -40,6 +42,7 @@ class MonthSamples { @Test fun constructorFunction() { + // Constructing a Month using the constructor function check(Month(1) == Month.JANUARY) check(Month(2) == Month.FEBRUARY) // ... diff --git a/core/common/test/samples/TimeZoneSamples.kt b/core/common/test/samples/TimeZoneSamples.kt index d346eaa8b..71ef13fb6 100644 --- a/core/common/test/samples/TimeZoneSamples.kt +++ b/core/common/test/samples/TimeZoneSamples.kt @@ -13,6 +13,7 @@ class TimeZoneSamples { @Test fun usage() { + // Using a time zone to convert a local date-time to an instant and back val zone = TimeZone.of("Europe/Berlin") val localDateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) val instant = localDateTime.toInstant(zone) @@ -23,11 +24,13 @@ class TimeZoneSamples { @Test fun id() { + // Getting the ID of a time zone check(TimeZone.of("America/New_York").id == "America/New_York") } @Test fun toStringSample() { + // Converting a time zone to a string val zone = TimeZone.of("America/New_York") check(zone.toString() == "America/New_York") check(zone.toString() == zone.id) @@ -35,6 +38,7 @@ class TimeZoneSamples { @Test fun equalsSample() { + // Comparing time zones for equality is based on their IDs val zone1 = TimeZone.of("America/New_York") val zone2 = TimeZone.of("America/New_York") check(zone1 == zone2) // different instances, but the same ID @@ -45,6 +49,7 @@ class TimeZoneSamples { @Test fun currentSystemDefault() { + // Obtaining the current system default time zone and using it for formatting // a fixed-width format for log entries val logTimeFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO) @@ -67,6 +72,7 @@ class TimeZoneSamples { @Test fun utc() { + // Using the UTC time zone for arithmetic operations val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) val instant = localDateTime.toInstant(TimeZone.UTC) check(instant == Instant.parse("2023-06-02T12:30:00Z")) @@ -78,12 +84,14 @@ class TimeZoneSamples { @Test fun constructorFunction() { + // Constructing a time zone using the factory function val zone = TimeZone.of("America/New_York") check(zone.id == "America/New_York") } @Test fun availableZoneIds() { + // Constructing every available time zone for (zoneId in TimeZone.availableZoneIds) { val zone = TimeZone.of(zoneId) // for fixed-offset time zones, normalization can happen, e.g. "UTC+01" -> "UTC+01:00" @@ -96,6 +104,7 @@ class TimeZoneSamples { */ @Test fun toLocalDateTimeWithTwoReceivers() { + // Converting an instant to a local date-time in a specific time zone val zone = TimeZone.of("America/New_York") val instant = Instant.parse("2023-06-02T12:30:00Z") val localDateTime = with(zone) { @@ -109,6 +118,7 @@ class TimeZoneSamples { */ @Test fun toInstantWithTwoReceivers() { + // Converting a local date-time to an instant in a specific time zone val zone = TimeZone.of("America/New_York") val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) val instant = with(zone) { @@ -122,6 +132,7 @@ class TimeZoneSamples { */ @Test fun offsetAt() { + // Obtaining the offset of a time zone at a specific instant val zone = TimeZone.of("America/New_York") val instant = Instant.parse("2023-06-02T12:30:00Z") val offset = zone.offsetAt(instant) @@ -130,6 +141,7 @@ class TimeZoneSamples { @Test fun instantToLocalDateTime() { + // Converting an instant to a local date-time in a specific time zone val zone = TimeZone.of("America/New_York") val instant = Instant.parse("2023-06-02T12:30:00Z") val localDateTime = instant.toLocalDateTime(zone) @@ -138,6 +150,7 @@ class TimeZoneSamples { @Test fun instantToLocalDateTimeInOffset() { + // Converting an instant to a local date-time in a specific offset val offset = UtcOffset.parse("+01:30") val instant = Instant.fromEpochMilliseconds(1685709000000) // "2023-06-02T12:30:00Z" val localDateTime = instant.toLocalDateTime(offset) @@ -149,6 +162,7 @@ class TimeZoneSamples { */ @Test fun offsetIn() { + // Obtaining the offset of a time zone at a specific instant val zone = TimeZone.of("America/New_York") val instant = Instant.parse("2023-06-02T12:30:00Z") val offset = instant.offsetIn(zone) @@ -160,6 +174,7 @@ class TimeZoneSamples { */ @Test fun localDateTimeToInstantInZone() { + // Converting a local date-time to an instant in a specific time zone val zone = TimeZone.of("America/New_York") val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) val instant = localDateTime.toInstant(zone) @@ -168,6 +183,7 @@ class TimeZoneSamples { @Test fun localDateTimeToInstantInOffset() { + // Converting a local date-time to an instant in a specific offset val offset = UtcOffset.parse("+01:30") val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) val instant = localDateTime.toInstant(offset) @@ -177,6 +193,7 @@ class TimeZoneSamples { @Ignore // fails on Windows; TODO investigate @Test fun atStartOfDayIn() { + // Finding the start of a given day in specific time zones val zone = TimeZone.of("America/Cuiaba") // The normal case where `atStartOfDayIn` returns the instant of 00:00:00 in the given time zone. val normalDate = LocalDate(2023, 6, 2) @@ -191,6 +208,7 @@ class TimeZoneSamples { class FixedOffsetTimeZoneSamples { @Test fun casting() { + // Providing special behavior for fixed-offset time zones val localDateTime = LocalDate(2023, 6, 2).atTime(12, 30) for ((zoneId, expectedString) in listOf( "UTC+01:30" to "2023-06-02T12:30+01:30", @@ -215,6 +233,7 @@ class TimeZoneSamples { @Test fun constructorFunction() { + // Constructing a fixed-offset time zone using an offset val offset = UtcOffset(hours = 1, minutes = 30) val zone = FixedOffsetTimeZone(offset) check(zone.id == "+01:30") @@ -223,6 +242,7 @@ class TimeZoneSamples { @Test fun offset() { + // Obtaining the offset of a fixed-offset time zone val zone = TimeZone.of("UTC+01:30") as FixedOffsetTimeZone check(zone.id == "UTC+01:30") check(zone.offset == UtcOffset(hours = 1, minutes = 30)) diff --git a/core/common/test/samples/UtcOffsetSamples.kt b/core/common/test/samples/UtcOffsetSamples.kt index 7ea006799..ec651c71d 100644 --- a/core/common/test/samples/UtcOffsetSamples.kt +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -13,21 +13,26 @@ class UtcOffsetSamples { @Test fun construction() { + // Constructing a UtcOffset using the constructor function val offset = UtcOffset(hours = 3, minutes = 30) check(offset.totalSeconds == 12600) + // UtcOffset.ZERO is a constant representing the zero offset check(UtcOffset(seconds = 0) == UtcOffset.ZERO) } @Test fun simpleParsingAndFormatting() { + // Parsing a UtcOffset from a string val offset = UtcOffset.parse("+01:30") check(offset.totalSeconds == 90 * 60) + // Formatting a UtcOffset to a string val formatted = offset.toString() check(formatted == "+01:30") } @Test fun customFormat() { + // Parsing a UtcOffset using a custom format val customFormat = UtcOffset.Format { optional("GMT") { offsetHours(Padding.NONE); char(':'); offsetMinutesOfHour() @@ -35,12 +40,14 @@ class UtcOffsetSamples { } } val offset = customFormat.parse("+01:30:15") + // Formatting a UtcOffset using both a custom format and a predefined one check(offset.format(customFormat) == "+1:30:15") check(offset.format(UtcOffset.Formats.FOUR_DIGITS) == "+0130") } @Test fun equalsSample() { + // Comparing UtcOffset values for equality val offset1 = UtcOffset.parse("+01:30") val offset2 = UtcOffset(minutes = 90) check(offset1 == offset2) @@ -50,6 +57,7 @@ class UtcOffsetSamples { @Test fun parsing() { + // Parsing a UtcOffset from a string using predefined and custom formats check(UtcOffset.parse("+01:30").totalSeconds == 5400) check(UtcOffset.parse("+0130", UtcOffset.Formats.FOUR_DIGITS).totalSeconds == 5400) val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } @@ -58,6 +66,7 @@ class UtcOffsetSamples { @Test fun toStringSample() { + // Converting a UtcOffset to a string check(UtcOffset.parse("+01:30:00").toString() == "+01:30") check(UtcOffset(hours = 1, minutes = 30).toString() == "+01:30") check(UtcOffset(seconds = 5400).toString() == "+01:30") @@ -65,6 +74,7 @@ class UtcOffsetSamples { @Test fun formatting() { + // Formatting a UtcOffset to a string using predefined and custom formats check(UtcOffset(hours = 1, minutes = 30).format(UtcOffset.Formats.FOUR_DIGITS) == "+0130") val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } check(UtcOffset(hours = 1, minutes = 30).format(customFormat) == "+130") @@ -72,6 +82,7 @@ class UtcOffsetSamples { @Test fun constructorFunction() { + // Using the constructor function to create UtcOffset values check(UtcOffset(hours = 3, minutes = 30).totalSeconds == 12600) check(UtcOffset(seconds = -3600) == UtcOffset(hours = -1)) try { @@ -90,6 +101,7 @@ class UtcOffsetSamples { @Test fun asFixedOffsetTimeZone() { + // Converting a UtcOffset to a fixed-offset TimeZone UtcOffset(hours = 3, minutes = 30).asTimeZone().let { timeZone -> check(timeZone.id == "+03:30") check(timeZone.offset == UtcOffset(hours = 3, minutes = 30)) @@ -99,6 +111,7 @@ class UtcOffsetSamples { class Formats { @Test fun isoBasic() { + // Using the basic ISO format for parsing and formatting UtcOffset values val offset = UtcOffset.Formats.ISO_BASIC.parse("+103622") check(offset == UtcOffset(hours = 10, minutes = 36, seconds = 22)) val formatted = UtcOffset.Formats.ISO_BASIC.format(offset) @@ -107,6 +120,7 @@ class UtcOffsetSamples { @Test fun iso() { + // Using the extended ISO format for parsing and formatting UtcOffset values val offset = UtcOffset.Formats.ISO.parse("+10:36:22") check(offset == UtcOffset(hours = 10, minutes = 36, seconds = 22)) val formatted = UtcOffset.Formats.ISO.format(offset) @@ -115,6 +129,7 @@ class UtcOffsetSamples { @Test fun fourDigits() { + // Using the constant-width compact format for parsing and formatting UtcOffset values val offset = UtcOffset.Formats.FOUR_DIGITS.parse("+1036") check(offset == UtcOffset(hours = 10, minutes = 36)) val offsetWithSeconds = UtcOffset(hours = 10, minutes = 36, seconds = 59) diff --git a/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt index a418a4ef3..b91aeb448 100644 --- a/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt +++ b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt @@ -12,6 +12,7 @@ import kotlin.test.* class DateTimeComponentsFormatSamples { @Test fun timeZoneId() { + // Defining a custom format that includes a time zone ID val format = DateTimeComponents.Format { dateTime(LocalDateTime.Formats.ISO) char('[') @@ -30,6 +31,7 @@ class DateTimeComponentsFormatSamples { @Test fun dateTimeComponents() { + // Using a predefined DateTimeComponents format in a larger format val format = DateTimeComponents.Format { char('{') dateTimeComponents(DateTimeComponents.Formats.RFC_1123) diff --git a/core/common/test/samples/format/DateTimeComponentsSamples.kt b/core/common/test/samples/format/DateTimeComponentsSamples.kt index 1dce80b3c..b4c0148e8 100644 --- a/core/common/test/samples/format/DateTimeComponentsSamples.kt +++ b/core/common/test/samples/format/DateTimeComponentsSamples.kt @@ -13,6 +13,7 @@ class DateTimeComponentsSamples { @Test fun parsingComplexInput() { + // Parsing a complex date-time string and extracting all its components val input = "2020-03-16T23:59:59.999999999+03:00" val components = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse(input) check(components.toLocalDateTime() == LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999)) @@ -22,6 +23,7 @@ class DateTimeComponentsSamples { @Test fun parsingInvalidInput() { + // Parsing an invalid input and handling the error val input = "23:59:60" val extraDay: Boolean val time = DateTimeComponents.Format { @@ -39,6 +41,7 @@ class DateTimeComponentsSamples { @Test fun simpleFormatting() { + // Formatting a multi-component date-time entity val formatted = DateTimeComponents.Formats.RFC_1123.format { setDateTimeOffset( LocalDateTime(2020, 3, 16, 23, 59, 59, 999_999_999), @@ -50,6 +53,7 @@ class DateTimeComponentsSamples { @Test fun customFormat() { + // Formatting and parsing a complex entity with a custom format val customFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO) char(' ') @@ -77,6 +81,7 @@ class DateTimeComponentsSamples { @Test fun setDateTime() { + // Setting the date-time components for formatting val dateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) val customFormat = DateTimeComponents.Format { dateTime(LocalDateTime.Formats.ISO) @@ -93,6 +98,7 @@ class DateTimeComponentsSamples { @Test fun setDateTimeOffsetInstant() { + // Setting the Instant and UTC offset components for formatting val instant = Instant.parse("2021-03-28T02:16:20+03:00") val offset = UtcOffset(3, 0) val formatted = DateTimeComponents.Formats.RFC_1123.format { @@ -103,6 +109,7 @@ class DateTimeComponentsSamples { @Test fun setDateTimeOffset() { + // Setting the date-time and UTC offset components for parsing val localDateTime = LocalDate(2021, 3, 28).atTime(2, 16, 20) val offset = UtcOffset(3, 0) val formatted = DateTimeComponents.Formats.RFC_1123.format { @@ -113,6 +120,7 @@ class DateTimeComponentsSamples { @Test fun dayOfWeek() { + // Formatting and parsing a date with the day of the week in complex scenarios val formatWithDayOfWeek = DateTimeComponents.Format { dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) char(' ') @@ -136,6 +144,7 @@ class DateTimeComponentsSamples { @Test fun date() { + // Formatting and parsing a date in complex scenarios val format = DateTimeComponents.Format { year(); char('-'); monthNumber(); char('-'); dayOfMonth() } @@ -157,6 +166,7 @@ class DateTimeComponentsSamples { @Test fun setMonth() { + // Setting the month using the `month` property val input = "Mon, 30 Jul 2008 11:05:30 GMT" val parsed = DateTimeComponents.Formats.RFC_1123.parse(input) check(parsed.monthNumber == 7) @@ -168,6 +178,7 @@ class DateTimeComponentsSamples { @Test fun timeAmPm() { + // Formatting and parsing a time with AM/PM marker in complex scenarios val format = DateTimeComponents.Format { amPmHour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(1, 9) char(' '); amPmMarker("AM", "PM") @@ -194,6 +205,7 @@ class DateTimeComponentsSamples { @Test fun time() { + // Formatting and parsing a time in complex scenarios val format = DateTimeComponents.Format { hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(1, 9) } @@ -219,6 +231,7 @@ class DateTimeComponentsSamples { @Test fun offset() { + // Formatting and parsing a UTC offset in complex scenarios val format = DateTimeComponents.Format { offset(UtcOffset.Formats.ISO) } val formattedOffset = format.format { setOffset(UtcOffset(-3, -30, -15)) @@ -238,6 +251,7 @@ class DateTimeComponentsSamples { @Test fun timeZoneId() { + // Formatting and parsing a time zone ID as part of a complex format val formatWithTimeZone = DateTimeComponents.Format { dateTime(LocalDateTime.Formats.ISO) char('[') @@ -261,6 +275,7 @@ class DateTimeComponentsSamples { @Test fun toUtcOffset() { + // Obtaining a UTC offset from the parsed data val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) val offset = parsed.toUtcOffset() @@ -269,6 +284,7 @@ class DateTimeComponentsSamples { @Test fun toLocalDate() { + // Obtaining a LocalDate from the parsed data val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) val localDate = parsed.toLocalDate() @@ -277,6 +293,7 @@ class DateTimeComponentsSamples { @Test fun toLocalTime() { + // Obtaining a LocalTime from the parsed data val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) val localTime = parsed.toLocalTime() @@ -285,6 +302,7 @@ class DateTimeComponentsSamples { @Test fun toLocalDateTime() { + // Obtaining a LocalDateTime from the parsed data val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) val localDateTime = parsed.toLocalDateTime() @@ -293,6 +311,7 @@ class DateTimeComponentsSamples { @Test fun toInstantUsingOffset() { + // Obtaining an Instant from the parsed data using the given UTC offset val rfc1123Input = "Sun, 06 Nov 1994 08:49:37 +0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123Input) val instant = parsed.toInstantUsingOffset() @@ -304,6 +323,7 @@ class DateTimeComponentsSamples { @Test fun formatting() { + // Formatting partial, complex, or broken data // DateTimeComponents can be used to format complex data that consists of multiple components val compoundFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO) @@ -331,6 +351,7 @@ class DateTimeComponentsSamples { @Test fun parsing() { + // Parsing partial, complex, or broken data // DateTimeComponents can be used to parse complex data that consists of multiple components val compoundFormat = DateTimeComponents.Format { date(LocalDate.Formats.ISO) @@ -362,6 +383,7 @@ class DateTimeComponentsSamples { class Formats { @Test fun rfc1123parsing() { + // Parsing a date-time string in the RFC 1123 format and extracting all its components val rfc1123string = "Mon, 30 Jun 2008 11:05:30 -0300" val parsed = DateTimeComponents.Formats.RFC_1123.parse(rfc1123string) check(parsed.toLocalDate() == LocalDate(2008, 6, 30)) @@ -371,6 +393,7 @@ class DateTimeComponentsSamples { @Test fun rfc1123formatting() { + // Formatting a date-time using the given UTC offset in the RFC 1123 format val today = Instant.fromEpochSeconds(1713182461) val offset = today.offsetIn(TimeZone.of("Europe/Berlin")) val formatted = DateTimeComponents.Formats.RFC_1123.format { @@ -381,6 +404,7 @@ class DateTimeComponentsSamples { @Test fun iso() { + // Using the ISO format for dates, times, and offsets combined val formatted = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.format { setDate(LocalDate(2023, 1, 2)) setTime(LocalTime(3, 46, 58, 530_000_000)) diff --git a/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt index ba21418a4..c660c78c8 100644 --- a/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt +++ b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt @@ -13,6 +13,7 @@ class DateTimeFormatBuilderSamples { @Test fun chars() { + // Defining a custom format that includes verbatim strings val format = LocalDate.Format { monthNumber() char('/') @@ -25,6 +26,7 @@ class DateTimeFormatBuilderSamples { @Test fun alternativeParsing() { + // Defining a custom format that allows parsing one of several alternatives val format = DateTimeComponents.Format { // optionally, date: alternativeParsing({ @@ -49,6 +51,7 @@ class DateTimeFormatBuilderSamples { @Test fun optional() { + // Defining a custom format that includes parts that will be omitted if they are zero val format = UtcOffset.Format { optional(ifZero = "Z") { offsetHours() @@ -87,6 +90,7 @@ class DateTimeFormatBuilderSamples { @Test fun char() { + // Defining a custom format that includes a verbatim character val format = LocalDate.Format { year() char('-') diff --git a/core/common/test/samples/format/DateTimeFormatSamples.kt b/core/common/test/samples/format/DateTimeFormatSamples.kt index 5d6fa78f8..f1596986f 100644 --- a/core/common/test/samples/format/DateTimeFormatSamples.kt +++ b/core/common/test/samples/format/DateTimeFormatSamples.kt @@ -13,11 +13,13 @@ class DateTimeFormatSamples { @Test fun format() { + // Formatting a date using a predefined format check(LocalDate.Formats.ISO.format(LocalDate(2021, 2, 7)) == "2021-02-07") } @Test fun formatTo() { + // Appending a formatted date to an `Appendable` (e.g. a `StringBuilder`) val sb = StringBuilder() sb.append("Today is ") LocalDate.Formats.ISO.formatTo(sb, LocalDate(2024, 4, 5)) @@ -26,6 +28,7 @@ class DateTimeFormatSamples { @Test fun parse() { + // Parsing a string that is expected to be in the given format check(LocalDate.Formats.ISO.parse("2021-02-07") == LocalDate(2021, 2, 7)) try { LocalDate.Formats.ISO.parse("2021-02-07T") @@ -45,6 +48,7 @@ class DateTimeFormatSamples { @Test fun parseOrNull() { + // Attempting to parse a string that may not be in the expected format check(LocalDate.Formats.ISO.parseOrNull("2021-02-07") == LocalDate(2021, 2, 7)) check(LocalDate.Formats.ISO.parseOrNull("2021-02-07T") == null) check(LocalDate.Formats.ISO.parseOrNull("2021-02-40") == null) @@ -56,6 +60,7 @@ class DateTimeFormatSamples { @Test fun formatAsKotlinBuilderDsl() { + // Printing a given date-time format as a Kotlin code snippet that creates the same format val customFormat = LocalDate.Format { @OptIn(FormatStringsInDatetimeFormats::class) byUnicodePattern("MM/dd uuuu") @@ -75,6 +80,7 @@ class DateTimeFormatSamples { class PaddingSamples { @Test fun usage() { + // Defining a custom format that uses various padding rules val format = LocalDate.Format { year(Padding.SPACE) chars(", ") @@ -88,6 +94,7 @@ class DateTimeFormatSamples { @Test fun zero() { + // Defining a custom format that uses '0' for padding val format = LocalDate.Format { monthNumber(Padding.ZERO) // padding with zeros is the default, but can be explicitly specified char('/') @@ -108,6 +115,7 @@ class DateTimeFormatSamples { @Test fun none() { + // Defining a custom format that removes padding requirements val format = LocalDate.Format { monthNumber(Padding.NONE) char('/') @@ -124,6 +132,7 @@ class DateTimeFormatSamples { @Test fun spaces() { + // Defining a custom format that uses spaces for padding val format = LocalDate.Format { monthNumber(Padding.SPACE) char('/') diff --git a/core/common/test/samples/format/LocalDateFormatSamples.kt b/core/common/test/samples/format/LocalDateFormatSamples.kt index bb2bd5c94..5a6d476ce 100644 --- a/core/common/test/samples/format/LocalDateFormatSamples.kt +++ b/core/common/test/samples/format/LocalDateFormatSamples.kt @@ -13,6 +13,7 @@ class LocalDateFormatSamples { @Test fun year() { + // Using the year number in a custom format val format = LocalDate.Format { year(); char(' '); monthNumber(); char('/'); dayOfMonth() } @@ -24,6 +25,7 @@ class LocalDateFormatSamples { @Test fun yearTwoDigits() { + // Using two-digit years in a custom format val format = LocalDate.Format { yearTwoDigits(baseYear = 1960); char(' '); monthNumber(); char('/'); dayOfMonth() } @@ -37,6 +39,7 @@ class LocalDateFormatSamples { @Test fun monthNumber() { + // Using month number with various paddings in a custom format val zeroPaddedMonths = LocalDate.Format { monthNumber(); char('/'); dayOfMonth(); char('/'); year() } @@ -51,6 +54,7 @@ class LocalDateFormatSamples { @Test fun monthName() { + // Using strings for month names in a custom format val format = LocalDate.Format { monthName(MonthNames.ENGLISH_FULL); char(' '); dayOfMonth(); char('/'); year() } @@ -60,6 +64,7 @@ class LocalDateFormatSamples { @Test fun dayOfMonth() { + // Using day-of-month with various paddings in a custom format val zeroPaddedDays = LocalDate.Format { dayOfMonth(); char('/'); monthNumber(); char('/'); year() } @@ -74,6 +79,7 @@ class LocalDateFormatSamples { @Test fun dayOfWeek() { + // Using strings for day-of-week names in a custom format val format = LocalDate.Format { dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED); char(' '); dayOfMonth(); char('/'); monthNumber(); char('/'); year() } @@ -83,6 +89,7 @@ class LocalDateFormatSamples { @Test fun date() { + // Using a predefined format for a date in a larger custom format val format = LocalDateTime.Format { date(LocalDate.Formats.ISO) alternativeParsing({ char('t') }) { char('T') } @@ -94,6 +101,7 @@ class LocalDateFormatSamples { class MonthNamesSamples { @Test fun usage() { + // Using strings for month names in a custom format val format = LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED) // "Jan", "Feb", ... char(' ') @@ -106,7 +114,7 @@ class LocalDateFormatSamples { @Test fun constructionFromStrings() { - // constructing by passing 12 strings + // Constructing a custom set of month names for parsing and formatting by passing 12 strings val myMonthNames = MonthNames( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" @@ -116,6 +124,7 @@ class LocalDateFormatSamples { @Test fun constructionFromList() { + // Constructing a custom set of month names for parsing and formatting val germanMonthNames = listOf( "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" @@ -127,6 +136,7 @@ class LocalDateFormatSamples { @Test fun names() { + // Obtaining the list of month names check(MonthNames.ENGLISH_ABBREVIATED.names == listOf( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" @@ -135,6 +145,7 @@ class LocalDateFormatSamples { @Test fun englishFull() { + // Using the built-in English month names in a custom format val format = LocalDate.Format { monthName(MonthNames.ENGLISH_FULL) char(' ') @@ -147,6 +158,7 @@ class LocalDateFormatSamples { @Test fun englishAbbreviated() { + // Using the built-in English abbreviated month names in a custom format val format = LocalDate.Format { monthName(MonthNames.ENGLISH_ABBREVIATED) char(' ') @@ -161,6 +173,7 @@ class LocalDateFormatSamples { class DayOfWeekNamesSamples { @Test fun usage() { + // Using strings for day-of-week names in a custom format val format = LocalDate.Format { date(LocalDate.Formats.ISO) chars(", ") @@ -171,7 +184,7 @@ class LocalDateFormatSamples { @Test fun constructionFromStrings() { - // constructing by passing 7 strings + // Constructing a custom set of day of week names for parsing and formatting by passing 7 strings val myMonthNames = DayOfWeekNames( "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" ) @@ -180,6 +193,7 @@ class LocalDateFormatSamples { @Test fun constructionFromList() { + // Constructing a custom set of day of week names for parsing and formatting val germanDayOfWeekNames = listOf( "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag" ) @@ -190,6 +204,7 @@ class LocalDateFormatSamples { @Test fun names() { + // Obtaining the list of day of week names check(DayOfWeekNames.ENGLISH_ABBREVIATED.names == listOf( "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" )) @@ -197,6 +212,7 @@ class LocalDateFormatSamples { @Test fun englishFull() { + // Using the built-in English day of week names in a custom format val format = LocalDate.Format { date(LocalDate.Formats.ISO) chars(", ") @@ -207,6 +223,7 @@ class LocalDateFormatSamples { @Test fun englishAbbreviated() { + // Using the built-in English abbreviated day of week names in a custom format val format = LocalDate.Format { date(LocalDate.Formats.ISO) chars(", ") diff --git a/core/common/test/samples/format/LocalDateTimeFormatSamples.kt b/core/common/test/samples/format/LocalDateTimeFormatSamples.kt index 6cd75bdcc..ee9cb01f0 100644 --- a/core/common/test/samples/format/LocalDateTimeFormatSamples.kt +++ b/core/common/test/samples/format/LocalDateTimeFormatSamples.kt @@ -12,6 +12,7 @@ import kotlin.test.* class LocalDateTimeFormatSamples { @Test fun dateTime() { + // Using a predefined LocalDateTime format in a larger format val format = DateTimeComponents.Format { dateTime(LocalDateTime.Formats.ISO) offset(UtcOffset.Formats.FOUR_DIGITS) diff --git a/core/common/test/samples/format/LocalTimeFormatSamples.kt b/core/common/test/samples/format/LocalTimeFormatSamples.kt index 67459f18f..8211601c2 100644 --- a/core/common/test/samples/format/LocalTimeFormatSamples.kt +++ b/core/common/test/samples/format/LocalTimeFormatSamples.kt @@ -12,6 +12,7 @@ import kotlin.test.* class LocalTimeFormatSamples { @Test fun hhmmss() { + // Defining a custom format for the local time // format the local time as a single number val format = LocalTime.Format { hour(); minute(); second() @@ -23,6 +24,7 @@ class LocalTimeFormatSamples { @Test fun amPm() { + // Defining a custom format for the local time that uses AM/PM markers val format = LocalTime.Format { amPmHour(); char(':'); minute(); char(':'); second() char(' '); amPmMarker("AM", "PM") @@ -33,6 +35,7 @@ class LocalTimeFormatSamples { @Test fun fixedLengthSecondFraction() { + // Defining a custom format for the local time with a fixed-length second fraction val format = LocalTime.Format { hour(); char(':'); minute(); char(':'); second() char('.'); secondFraction(fixedLength = 3) @@ -43,6 +46,7 @@ class LocalTimeFormatSamples { @Test fun time() { + // Using a predefined format for the local time val format = LocalDateTime.Format { date(LocalDate.Formats.ISO) char(' ') diff --git a/core/common/test/samples/format/UnicodeSamples.kt b/core/common/test/samples/format/UnicodeSamples.kt index 466ded0a5..26d9a16f0 100644 --- a/core/common/test/samples/format/UnicodeSamples.kt +++ b/core/common/test/samples/format/UnicodeSamples.kt @@ -12,6 +12,7 @@ import kotlin.test.* class UnicodeSamples { @Test fun byUnicodePattern() { + // Using the Unicode pattern to define a custom format and obtain the corresponding Kotlin code val customFormat = LocalDate.Format { @OptIn(FormatStringsInDatetimeFormats::class) byUnicodePattern("MM/dd uuuu") diff --git a/core/common/test/samples/format/UtcOffsetFormatSamples.kt b/core/common/test/samples/format/UtcOffsetFormatSamples.kt index 6a300c84e..f6f51ba76 100644 --- a/core/common/test/samples/format/UtcOffsetFormatSamples.kt +++ b/core/common/test/samples/format/UtcOffsetFormatSamples.kt @@ -12,6 +12,7 @@ import kotlin.test.* class UtcOffsetFormatSamples { @Test fun isoOrGmt() { + // Defining a custom format for the UTC offset val format = UtcOffset.Format { // if the offset is zero, `GMT` is printed optional("GMT") { @@ -28,6 +29,7 @@ class UtcOffsetFormatSamples { @Test fun offset() { + // Using a predefined format for the UTC offset val format = DateTimeComponents.Format { dateTime(LocalDateTime.Formats.ISO) offset(UtcOffset.Formats.FOUR_DIGITS) From a2af2de19c83bda77fb79af4d4dc004b505f22cd Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 19 Apr 2024 13:19:45 +0200 Subject: [PATCH 23/35] More additions according to the reviews --- core/common/src/Clock.kt | 3 ++- core/common/src/DateTimePeriod.kt | 9 +++++++-- core/common/src/Instant.kt | 5 +++++ core/common/src/LocalDate.kt | 9 +++++++++ core/common/src/LocalDateTime.kt | 9 +++++++++ core/common/src/LocalTime.kt | 5 +++++ core/common/src/TimeZone.kt | 7 +++++++ core/common/src/UtcOffset.kt | 5 +++++ core/common/test/samples/ClockSamples.kt | 18 ++++++++++++++++++ 9 files changed, 67 insertions(+), 3 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 7f2e239d9..5dbf94fb0 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -46,9 +46,10 @@ public interface Clock { * [TimeSource.Monotonic]. * * For improved testability, one could avoid using [Clock.System] directly in the implementation, - * instead passing a [Clock] explicitly. + * instead passing a [Clock] explicitly. For example: * * @sample kotlinx.datetime.test.samples.ClockSamples.system + * @sample kotlinx.datetime.test.samples.ClockSamples.dependencyInjection */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 4f9dc12c5..a4fed01a5 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -30,9 +30,11 @@ import kotlinx.serialization.Serializable * All components can also be negative: for example, `DateTimePeriod(months = -5, days = 6, hours = -3)`. * Whereas `months = 5` means "5 months after," `months = -5` means "5 months earlier." * - * Since, semantically, a [DateTimePeriod] is a combination of [DateTimeUnit] values, in cases when the period is a - * fixed time interval (like "yearly" or "quarterly"), please consider using [DateTimeUnit] directly instead: + * A constant time interval that consists of a single non-zero component (like "yearly" or "quarterly") should be + * represented by a [DateTimeUnit] directly instead of a [DateTimePeriod]: * for example, instead of `DateTimePeriod(months = 6)`, one could use `DateTimeUnit.MONTH * 6`. + * This provides a wider variety of operations: for example, finding how many such intervals fit between two instants + * or dates or adding a multiple of such intervals at once. * * ### Interaction with other entities * @@ -413,6 +415,9 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * `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. * + * On the JVM, there are `DatePeriod.toJavaPeriod()` and `java.time.Period.toKotlinDatePeriod()` + * extension functions. + * * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting */ @Serializable(with = DatePeriodIso8601Serializer::class) diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index b4e99ed4c..ba92befc3 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -131,6 +131,11 @@ import kotlin.time.* * // 63 days until the concert, rounded down * ``` * + * ### Platform specifics + * + * On the JVM, there are `Instant.toJavaInstant()` and `java.time.Instant.toKotlinInstant()` extension functions. + * On the Darwin platforms, there are `Instant.toNSDate()` and `NSDate.toKotlinInstant()` extension functions. + * * ### Construction, serialization, and deserialization * * [fromEpochSeconds] can be used to construct an instant from the number of seconds since diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 8db1cf3ce..4965cfdc6 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -29,6 +29,15 @@ import kotlinx.serialization.Serializable * - [LocalDate.periodUntil] (and [LocalDate.minus] that accepts a [LocalDate]) * can be used to find the [DatePeriod] between two dates. * + * ### Platform specifics + * + * The range of supported years is platform-dependent, but at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. + * + * On the JVM, + * there are `LocalDate.toJavaLocalDate()` and `java.time.LocalDate.toKotlinLocalDate()` extension functions. + * On the Darwin platforms, there is a `LocalDate.toNSDateComponents()` extension function. + * * ### Construction, serialization, and deserialization * * [LocalDate] can be constructed directly from its components, using the constructor. diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 1d55bc246..da0f42435 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -60,6 +60,15 @@ import kotlinx.serialization.Serializable * // 2021-03-29T02:16:20 * ``` * + * ### Platform specifics + * + * The range of supported years is platform-dependent, but at least is enough to represent dates of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. + * + * On the JVM, there are `LocalDateTime.toJavaLocalDateTime()` and `java.time.LocalDateTime.toKotlinLocalDateTime()` + * extension functions. + * On the Darwin platforms, there is a `LocalDateTime.toNSDateComponents()` extension function. + * * ### Construction, serialization, and deserialization * * **Pitfall**: since [LocalDateTime] is always constructed without specifying the time zone, it cannot validate diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 0f0e7543f..276c68223 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -45,6 +45,11 @@ import kotlinx.serialization.Serializable * Because this pattern is extremely verbose and difficult to get right, it is recommended to work exclusively * with [Instant] and only obtain a [LocalTime] when it is necessary to display the time to the user. * + * ### Platform specifics + * + * On the JVM, + * there are `LocalTime.toJavaLocalTime()` and `java.time.LocalTime.toKotlinLocalTime()` extension functions. + * * ### Construction, serialization, and deserialization * * [LocalTime] can be constructed directly from its components, using the constructor. See sample 1. diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index f93b39f47..1ce51ee4f 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -25,6 +25,9 @@ import kotlinx.serialization.Serializable * For interaction with `kotlinx-serialization`, [TimeZoneSerializer] is provided that serializes the time zone as its * identifier. * + * On the JVM, there are `TimeZone.toJavaZoneId()` and `java.time.ZoneId.toKotlinTimeZone()` extension functions. + * On the Darwin platforms, there are `TimeZone.toNSTimeZone()` and `NSTimeZone.toKotlinTimeZone()` extension functions. + * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.usage */ @Serializable(with = TimeZoneSerializer::class) @@ -138,6 +141,10 @@ public expect open class TimeZone { * Time zones that are [FixedOffsetTimeZone] at some point in time can become non-fixed in the future due to * changes in legislation or other reasons. * + * On the JVM, there are `FixedOffsetTimeZone.toJavaZoneOffset()` and + * `java.time.ZoneOffset.toKotlinFixedOffsetTimeZone()` extension functions. + * Note also the functions available for [TimeZone] in general. + * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.FixedOffsetTimeZoneSamples.casting */ @Serializable(with = FixedOffsetTimeZoneSerializer::class) diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 5043e7d4f..bbaf179dd 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -27,6 +27,11 @@ import kotlinx.serialization.Serializable * * See [TimeZone] for a type that represents a time zone. * + * ### Platform specifics + * + * On the JVM, there are `UtcOffset.toJavaZoneOffset()` and `java.time.ZoneOffset.toKotlinUtcOffset()` + * extension functions. + * * ### Construction, serialization, and deserialization * * To construct a [UtcOffset] value, use the [UtcOffset] constructor function. diff --git a/core/common/test/samples/ClockSamples.kt b/core/common/test/samples/ClockSamples.kt index f48217cb8..83a3a1563 100644 --- a/core/common/test/samples/ClockSamples.kt +++ b/core/common/test/samples/ClockSamples.kt @@ -18,6 +18,24 @@ class ClockSamples { currentLocalDateTime.toString() // show the current date and time, according to the OS } + @Test + fun dependencyInjection() { + fun formatCurrentTime(clock: Clock, timeZone: TimeZone): String = + clock.now().toLocalDateTime(timeZone).toString() + + // In the production code: + val currentTimeInProduction = formatCurrentTime(Clock.System, TimeZone.currentSystemDefault()) + // Testing this value is tricky because it changes all the time. + + // In the test code: + val testClock = object: Clock { + override fun now(): Instant = Instant.parse("2023-01-02T22:35:01Z") + } + // Then, one can write a completely deterministic test: + val currentTimeForTests = formatCurrentTime(testClock, TimeZone.of("Europe/Paris")) + check(currentTimeForTests == "2023-01-02T23:35:01") + } + @Test fun todayIn() { // Getting the current date in different time zones From f7878af58d752ef4485157c063c1e3514e43897b Mon Sep 17 00:00:00 2001 From: "Danil.Pavlov" Date: Fri, 12 Apr 2024 18:29:18 +0200 Subject: [PATCH 24/35] feat: new docs review --- core/common/src/Clock.kt | 19 ++++--- core/common/src/DateTimePeriod.kt | 6 +-- core/common/src/DateTimeUnit.kt | 23 ++++---- core/common/src/Instant.kt | 50 ++++++++--------- core/common/src/LocalDate.kt | 54 +++++++++---------- core/common/src/LocalDateTime.kt | 20 +++---- core/common/src/format/DateTimeComponents.kt | 6 +-- core/common/src/format/DateTimeFormat.kt | 6 +-- .../src/format/DateTimeFormatBuilder.kt | 2 +- core/common/src/format/LocalDateFormat.kt | 10 ++-- 10 files changed, 98 insertions(+), 98 deletions(-) diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 5dbf94fb0..6d3dee7c3 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -12,8 +12,8 @@ import kotlin.time.* * * See [Clock.System][Clock.System] for the clock instance that queries the operating system. * - * It is recommended not to use [Clock.System] directly in the implementation; instead, one could pass a - * [Clock] explicitly to the functions or classes that need it. + * It is not recommended to use [Clock.System] directly in the implementation. Instead, you can pass a + * [Clock] explicitly to the necessary functions or classes. * This way, tests can be written deterministically by providing custom [Clock] implementations * to the system under test. */ @@ -24,7 +24,7 @@ public interface Clock { * It is not guaranteed that calling [now] later will return a larger [Instant]. * In particular, for [Clock.System] it is completely expected that the opposite will happen, * and it must be taken into account. - * See the documentation of [Clock.System] for details. + * See the [System] documentation for details. * * Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling * leap seconds, [now] is not guaranteed to handle leap seconds in any specific way. @@ -38,15 +38,14 @@ public interface Clock { * these increases will not necessarily correspond to the elapsed time. * * For example, when using [Clock.System], the following could happen: - * - [now] returns `2023-01-02T22:35:01Z`; - * - The system queries the Internet and recognizes that its clock needs adjusting; + * - [now] returns `2023-01-02T22:35:01Z`. + * - The system queries the Internet and recognizes that its clock needs adjusting. * - [now] returns `2023-01-02T22:32:05Z`. * - * When predictable intervals between successive measurements are needed, consider using - * [TimeSource.Monotonic]. + * When you need predictable intervals between successive measurements, consider using [TimeSource.Monotonic]. * - * For improved testability, one could avoid using [Clock.System] directly in the implementation, - * instead passing a [Clock] explicitly. For example: + * For improved testability, you should avoid using [Clock.System] directly in the implementation + * and pass a [Clock] explicitly instead. For example: * * @sample kotlinx.datetime.test.samples.ClockSamples.system * @sample kotlinx.datetime.test.samples.ClockSamples.dependencyInjection @@ -74,7 +73,7 @@ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = /** * Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark. * - * **Pitfall**: using this function with [Clock.System] is error-prone, + * **Pitfall**: using this function with [Clock.System] is error-prone * because [Clock.System] is not well suited for measuring time intervals. * Please only use this conversion function on the [Clock] instances that are fully controlled programmatically. */ diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index a4fed01a5..85124b114 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -17,10 +17,10 @@ import kotlinx.serialization.Serializable /** * A difference between two [instants][Instant], decomposed into date and time components. * - * The date components are: [years] ([DateTimeUnit.YEAR]), [months] ([DateTimeUnit.MONTH]), [days] ([DateTimeUnit.DAY]). + * The date components are: [years] ([DateTimeUnit.YEAR]), [months] ([DateTimeUnit.MONTH]), and [days] ([DateTimeUnit.DAY]). * * The time components are: [hours] ([DateTimeUnit.HOUR]), [minutes] ([DateTimeUnit.MINUTE]), - * [seconds] ([DateTimeUnit.SECOND]), [nanoseconds] ([DateTimeUnit.NANOSECOND]). + * [seconds] ([DateTimeUnit.SECOND]), and [nanoseconds] ([DateTimeUnit.NANOSECOND]). * * The time components are not independent and are always normalized together. * Likewise, months are normalized together with years. @@ -428,7 +428,7 @@ public class DatePeriod internal constructor( /** * Constructs a new [DatePeriod]. * - * It is recommended to always explicitly name the arguments when constructing this manually, + * It is always recommended to name the arguments explicitly when constructing this manually, * like `DatePeriod(years = 1, months = 12, days = 16)`. * * The passed numbers are not stored as is but are normalized instead for human readability, so, for example, diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 591cf0955..97ab32f77 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -20,12 +20,12 @@ import kotlin.time.Duration.Companion.nanoseconds * * ### Interaction with other entities * - * Any [DateTimeUnit] can be used with [Instant.plus], [Instant.minus] to + * Any [DateTimeUnit] can be used with [Instant.plus] or [Instant.minus] to * find an instant that is some number of units away from the given instant. * Also, [Instant.until] can be used to find the number of the given units between two instants. * - * [DateTimeUnit.TimeBased] can be used in the [Instant] operations without specifying the time zone, because - * [DateTimeUnit.TimeBased] is defined in terms of passage of real time, and is independent of the time zone. + * [DateTimeUnit.TimeBased] can be used in the [Instant] operations without specifying the time zone because + * [DateTimeUnit.TimeBased] is defined in terms of the passage of real-time and is independent of the time zone. * Note that a calendar day is not considered identical to 24 hours, so using it does require specifying the time zone. * See [DateTimeUnit.DayBased] for an explanation. * @@ -33,10 +33,10 @@ import kotlin.time.Duration.Companion.nanoseconds * [LocalDate.until]. * * Arithmetic operations on [LocalDateTime] are not provided. - * Please see the [LocalDateTime] documentation for a discussion. + * Please see the [LocalDateTime] documentation for details. * - * [DateTimePeriod] is a combination of [DateTimeUnit] values of every kind, used to express things like - * "two days and three hours." + * [DateTimePeriod] is a combination of [DateTimeUnit] values of every kind, used to express periods like + * "two days and three hours". * [DatePeriod] is specifically a combination of [DateTimeUnit.DateBased] values. * [DateTimePeriod] is more flexible than [DateTimeUnit] because it can express a combination of values with different * kinds of units, but in exchange, the duration of time between two [Instant] or [LocalDate] values can be @@ -48,8 +48,8 @@ import kotlin.time.Duration.Companion.nanoseconds * [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.MICROSECOND * 10`. - * - By constructing an instance manually with [TimeBased], [DayBased], or [MonthBased]: for example, + * - By multiplying an existing unit on the right by an integer scalar, for example, `DateTimeUnit.MICROSECOND * 10`. + * - By constructing an instance manually with [TimeBased], [DayBased], or [MonthBased], for example, * `DateTimeUnit.TimeBased(nanoseconds = 10_000)`. * * Also, [DateTimeUnit] can be serialized and deserialized using `kotlinx.serialization`: @@ -164,8 +164,8 @@ public sealed class DateTimeUnit { /** * A [date-time unit][DateTimeUnit] 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. + * 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` @@ -204,7 +204,8 @@ public sealed class DateTimeUnit { /** * A [date-time unit][DateTimeUnit] equal to some number of months. * - * Since different months have different number of days, a `MonthBased`-unit cannot be expressed as a multiple of some [DayBased]-unit. + * Since different months have a different number of days, a `MonthBased` unit cannot be expressed + * as a multiple of some [DayBased]-unit. * * @see DateTimeUnit for a description of date-time units in general. * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.monthBasedUnit diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index ba92befc3..1036b311f 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -15,12 +15,12 @@ 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 + * 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 it is 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]. + * in time, as is "1970-01-01, 00:00:00 UTC+0", 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 @@ -46,7 +46,7 @@ import kotlin.time.* * * [Instant] is essentially the number of seconds and nanoseconds since a designated moment in time, * stored as something like `1709898983.123456789`. - * [Instant] contains no information about what day or time it is, as this depends on the time zone. + * [Instant] does not contain information about the day or time, as this depends on the time zone. * To work with this information for a specific time zone, obtain a [LocalDateTime] using [Instant.toLocalDateTime]: * * ``` @@ -98,16 +98,16 @@ import kotlin.time.* * Since [Instant] represents a point in time, it is always well-defined what the result of arithmetic operations on it * is, including the cases when a calendar is used. * This is not the case for [LocalDateTime], where the result of arithmetic operations depends on the time zone. - * See the documentation of [LocalDateTime] for more details. + * See the [LocalDateTime] documentation for more details. * * Adding and subtracting calendar-based units can be done using the [plus] and [minus] operators, * requiring a [TimeZone]: * * ``` - * // one day from now in Berlin + * // One day from now in Berlin * Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) * - * // a day and two hours short from two months later in Berlin + * // A day and two hours short from two months later in Berlin * Clock.System.now().plus(DateTimePeriod(months = 2, days = -1, hours = -2), TimeZone.of("Europe/Berlin")) * ``` * @@ -118,7 +118,7 @@ import kotlin.time.* * val start = Clock.System.now() * val concertStart = LocalDateTime(2023, 1, 1, 20, 0, 0).toInstant(TimeZone.of("Europe/Berlin")) * val timeUntilConcert = start.periodUntil(concertStart, TimeZone.of("Europe/Berlin")) - * // two months, three days, four hours, and five minutes until the concert + * // Two months, three days, four hours, and five minutes until the concert * ``` * * or [Instant.until] method, as well as [Instant.daysUntil], [Instant.monthsUntil], @@ -187,7 +187,7 @@ import kotlin.time.* * ``` * * Additionally, there are several `kotlinx-serialization` serializers for [Instant]: - * - [InstantIso8601Serializer] for the ISO 8601 extended format, + * - [InstantIso8601Serializer] for the ISO 8601 extended format. * - [InstantComponentSerializer] for an object with components. * * @see LocalDateTime for a user-visible representation of moments in time in an unspecified time zone. @@ -285,7 +285,7 @@ public expect class Instant : Comparable { /** * Compares `this` instant with the [other] instant. - * Returns zero if this instant represents 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., when it's 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. * @@ -294,9 +294,9 @@ public expect class Instant : Comparable { public override operator fun compareTo(other: Instant): Int /** - * Converts this instant to the ISO 8601 string representation; for example, `2023-01-02T23:40:57.120Z` + * Converts this instant to the ISO 8601 string representation, for example, `2023-01-02T23:40:57.120Z`. * - * The representation uses the UTC-SLS time scale, instead of UTC. + * 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. @@ -480,9 +480,9 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = * The components of [DateTimePeriod] are calculated so that adding it to `this` instant results in the [other] instant. * * All components of the [DateTimePeriod] returned are: - * - positive or zero if this instant is earlier than the other, - * - negative or zero if this instant is later than the other, - * - exactly zero if this instant is equal to the other. + * - Positive or zero if this instant is earlier than the other. + * - 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]. * Or (only on the JVM) if the number of months between the two dates exceeds an Int. @@ -495,9 +495,9 @@ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT * in the specified [timeZone]. * * The value returned is: - * - positive or zero if this instant is earlier than the other, - * - negative or zero if this instant is later than the other, - * - zero if this instant is equal to the other. + * - Positive or zero if this instant is earlier than the other. + * - Negative or zero if this instant is later than the other. + * - Zero if this instant is equal to the other. * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * @@ -510,9 +510,9 @@ public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti * Returns the whole number of the specified time [units][unit] between `this` and [other] instants. * * The value returned is: - * - positive or zero if this instant is earlier than the other, - * - negative or zero if this instant is later than the other, - * - zero if this instant is equal to the other. + * - Positive or zero if this instant is earlier than the other. + * - Negative or zero if this instant is later than the other. + * - Zero if this instant is equal to the other. * * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. * @@ -570,9 +570,9 @@ public fun Instant.yearsUntil(other: Instant, timeZone: TimeZone): Int = * The components of [DateTimePeriod] are calculated so that adding it back to the `other` instant results in this instant. * * All components of the [DateTimePeriod] returned are: - * - negative or zero if this instant is earlier than the other, - * - positive or zero if this instant is later than the other, - * - exactly zero if this instant is equal to the other. + * - Negative or zero if this instant is earlier than the other. + * - 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]. * Or (only on the JVM) if the number of months between the two dates exceeds an Int. diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 4965cfdc6..ab7454f26 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -23,9 +23,9 @@ import kotlinx.serialization.Serializable * ### Arithmetic operations * * Operations with [DateTimeUnit.DateBased] and [DatePeriod] are provided for [LocalDate]: - * - [LocalDate.plus] and [LocalDate.minus] allow expressing concepts like "two months later," + * - [LocalDate.plus] and [LocalDate.minus] allow expressing concepts like "two months later". * - [LocalDate.until] and its shortcuts [LocalDate.daysUntil], [LocalDate.monthsUntil], and [LocalDate.yearsUntil] - * can be used to find the number of days, months, or years between two dates, + * can be used to find the number of days, months, or years between two dates. * - [LocalDate.periodUntil] (and [LocalDate.minus] that accepts a [LocalDate]) * can be used to find the [DatePeriod] between two dates. * @@ -40,7 +40,7 @@ import kotlinx.serialization.Serializable * * ### Construction, serialization, and deserialization * - * [LocalDate] can be constructed directly from its components, using the constructor. + * [LocalDate] can be constructed directly from its components using the constructor. * See sample 1. * * [fromEpochDays] can be used to obtain a [LocalDate] from the number of days since the epoch day `1970-01-01`; @@ -55,7 +55,7 @@ import kotlinx.serialization.Serializable * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: - * - [LocalDateIso8601Serializer] for the ISO 8601 extended format, + * - [LocalDateIso8601Serializer] for the ISO 8601 extended format. * - [LocalDateComponentSerializer] for an object with components. * * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunctionMonthNumber @@ -158,7 +158,7 @@ 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 + * @throws IllegalArgumentException if any parameter is out of range or if [dayOfMonth] is invalid for the * given [monthNumber] and [year]. * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunctionMonthNumber */ @@ -173,7 +173,7 @@ 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 + * @throws IllegalArgumentException if any parameter is out of range or if [dayOfMonth] is invalid for the * given [month] and [year]. * @sample kotlinx.datetime.test.samples.LocalDateSamples.constructorFunction */ @@ -269,7 +269,7 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) /** * 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 + * 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. * * **Pitfall**: since [LocalDateTime] is not tied to a particular time zone, the resulting [LocalDateTime] may not @@ -291,7 +291,7 @@ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: * **Pitfall**: since [LocalDateTime] is not tied to a particular time zone, the resulting [LocalDateTime] may not * exist in the implicit time zone. * For example, `LocalDate(2021, 3, 28).atTime(LocalTime(2, 16, 20))` will successfully create a [LocalDateTime], - * even though in Berlin, times between 2:00 and 3:00 do not exist on March 28, 2021 due to the transition to DST. + * even though in Berlin, times between 2:00 and 3:00 do not exist on March 28, 2021, due to the transition to DST. * * @sample kotlinx.datetime.test.samples.LocalDateSamples.atTime */ @@ -299,7 +299,7 @@ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this /** - * Returns a date that is the result of adding components of [DatePeriod] to this date. The components are + * Returns a date that results from adding components of [DatePeriod] to this date. The components are * added in the order from the largest units to the smallest: first years and months, then days. * * @see LocalDate.periodUntil @@ -310,7 +310,7 @@ public fun LocalDate.atTime(time: LocalTime): LocalDateTime = LocalDateTime(this public expect operator fun LocalDate.plus(period: DatePeriod): LocalDate /** - * Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are + * Returns a date that results from subtracting components of [DatePeriod] from this date. The components are * subtracted in the order from the largest units to the smallest: first years and months, then days. * * @see LocalDate.periodUntil @@ -322,7 +322,7 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) { plus(with(period) { DatePeriod(-years, -months, -days) }) } else { - // TODO: calendar operations are non-associative, check if subtracting years and months separately is correct + // TODO: calendar operations are non-associative; check if subtracting years and months separately is correct minus(period.years, DateTimeUnit.YEAR).minus(period.months, DateTimeUnit.MONTH) .minus(period.days, DateTimeUnit.DAY) } @@ -333,9 +333,9 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate = * The components of [DatePeriod] are calculated so that adding it to `this` date results in the [other] date. * * All components of the [DatePeriod] returned are: - * - positive or zero if this date is earlier than the other, - * - negative or zero if this date is later than the other, - * - exactly zero if this date is equal to the other. + * - Positive or zero if this date is earlier than the other. + * - Negative or zero if this date is later than the other. + * - Exactly zero if this date is equal to the other. * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * @@ -350,9 +350,9 @@ public expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod * The components of [DatePeriod] are calculated so that adding it back to the `other` date results in this date. * * All components of the [DatePeriod] returned are: - * - negative or zero if this date is earlier than the other, - * - positive or zero if this date is later than the other, - * - exactly zero if this date is equal to the other. + * - Negative or zero if this date is earlier than the other. + * - Positive or zero if this date is later than the other. + * - Exactly zero if this date is equal to the other. * * @throws DateTimeArithmeticException if the number of months between the two dates exceeds an Int (JVM only). * @@ -365,9 +365,9 @@ public operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.period * Returns the whole number of the specified date [units][unit] between `this` and [other] dates. * * The value returned is: - * - positive or zero if this date is earlier than the other, - * - negative or zero if this date is later than the other, - * - zero if this date is equal to the other. + * - Positive or zero if this date is earlier than the other. + * - Negative or zero if this date is later than the other. + * - Zero if this date is equal to the other. * * The value is rounded toward zero. * @@ -417,7 +417,7 @@ public expect fun LocalDate.monthsUntil(other: LocalDate): Int public expect fun LocalDate.yearsUntil(other: LocalDate): Int /** - * Returns a [LocalDate] that is the result of adding one [unit] to this date. + * Returns a [LocalDate] that results from adding one [unit] to this date. * * The value is rounded toward zero. * @@ -429,7 +429,7 @@ public expect fun LocalDate.yearsUntil(other: LocalDate): Int public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate /** - * Returns a [LocalDate] that is the result of subtracting one [unit] from this date. + * Returns a [LocalDate] that results from subtracting one [unit] from this date. * * The value is rounded toward zero. * @@ -441,7 +441,7 @@ public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, unit) /** - * Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date. + * Returns a [LocalDate] that results from adding the [value] number of the specified [unit] to this date. * * If the [value] is positive, the returned date is later than this date. * If the [value] is negative, the returned date is earlier than this date. @@ -454,7 +454,7 @@ public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, u public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate /** - * Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date. + * Returns a [LocalDate] that results from subtracting the [value] number of the specified [unit] from this date. * * If the [value] is positive, the returned date is earlier than this date. * If the [value] is negative, the returned date is later than this date. @@ -467,7 +467,7 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate /** - * Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date. + * Returns a [LocalDate] that results from adding the [value] number of the specified [unit] to this date. * * If the [value] is positive, the returned date is later than this date. * If the [value] is negative, the returned date is earlier than this date. @@ -480,7 +480,7 @@ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): Loc public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate /** - * Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date. + * Returns a [LocalDate] that results from subtracting the [value] number of the specified [unit] from this date. * * If the [value] is positive, the returned date is earlier than this date. * If the [value] is negative, the returned date is later than this date. @@ -492,5 +492,5 @@ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): Loc */ public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) -// workaround for https://youtrack.jetbrains.com/issue/KT-65484 +// A workaround for https://youtrack.jetbrains.com/issue/KT-65484 internal fun getIsoDateFormat() = LocalDate.Formats.ISO diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index da0f42435..a9712c975 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -14,8 +14,8 @@ 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 someone could observe in their time zone. - * For example, `2020-08-30T18:43` is not a *moment in time*, since someone in Berlin and someone in Tokyo would witness + * Instead, you can think of its instances as clock readings, which can be observed in a particular time zone. + * For example, `2020-08-30T18:43` is not a *moment in time* since someone in Berlin and Tokyo would witness * this on their clocks at different times, but it is a [LocalDateTime]. * * The main purpose of this class is to provide human-readable representations of [Instant] values, to transfer them @@ -25,18 +25,18 @@ import kotlinx.serialization.Serializable * * ### Arithmetic operations * - * The arithmetic on [LocalDateTime] values is not provided, since without accounting for the time zone transitions it - * may give misleading results. + * The arithmetic on [LocalDateTime] values is not provided since it may give misleading results + * without accounting for time zone transitions. * * For example, in Berlin, naively adding one day to `2021-03-27T02:16:20` without accounting for the time zone would * result in `2021-03-28T02:16:20`. - * However, the resulting local date-time cannot be observed in that time zone, + * However, the resulting local date-time cannot be observed in that time zone * because the clocks moved forward from `02:00` to `03:00` on that day. - * This is known as a "time gap", or a "spring forward" transition. + * This is known as a "time gap" or a "spring forward" transition. * * Similarly, the local date-time `2021-10-31T02:16:20` is ambiguous, * because the clocks moved back from `03:00` to `02:00`. - * This is known as a "time overlap", or a "fall back" transition. + * This is known as a "time overlap" or a "fall back" transition. * * For these reasons, using [LocalDateTime] as an input to arithmetic operations is discouraged. * @@ -94,7 +94,7 @@ import kotlinx.serialization.Serializable * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: - * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format, + * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format. * - [LocalDateTimeComponentSerializer] for an object with components. * * @see LocalDate for only the date part of the date/time value. @@ -350,7 +350,7 @@ public expect class LocalDateTime : Comparable { * val ldt1 = Clock.System.now().toLocalDateTime(zone) // 2021-10-31T02:16:20 * // 45 minutes pass; clocks move back from 03:00 to 02:00 in the meantime * val ldt2 = Clock.System.now().toLocalDateTime(zone) // 2021-10-31T02:01:20 - * ldt2 > ldt1 // returns `false` + * ldt2 > ldt1 // Returns `false` * ``` * * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.compareToSample @@ -396,5 +396,5 @@ public fun LocalDateTime.format(format: DateTimeFormat): String = @Deprecated("Removed to support more idiomatic code. See https://github.com/Kotlin/kotlinx-datetime/issues/339", ReplaceWith("LocalDateTime.parse(this)"), DeprecationLevel.WARNING) public fun String.toLocalDateTime(): LocalDateTime = LocalDateTime.parse(this) -// workaround for https://youtrack.jetbrains.com/issue/KT-65484 +// A workaround for https://youtrack.jetbrains.com/issue/KT-65484 internal fun getIsoDateTimeFormat() = LocalDateTime.Formats.ISO diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index be46c55d7..a3fb80945 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -87,9 +87,9 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * Guaranteed to parse all strings that [Instant.toString] produces. * - * Typically, to use this format, one can simply call [Instant.toString] and [Instant.parse], - * but accessing it directly allows one to obtain the UTC offset, which is not returned from [Instant.parse], - * or specifying the UTC offset to be formatted. + * Typically, to use this format, you can simply call [Instant.toString] and [Instant.parse]. + * However, by accessing this format directly, you can obtain the UTC offset, which is not returned from [Instant.parse], + * or specify the UTC offset to be formatted. * * ``` * val components = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse("2020-08-30T18:43:00.123456789+03:00") diff --git a/core/common/src/format/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index 5fc755e9a..933360535 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -19,7 +19,7 @@ import kotlinx.datetime.internal.format.parser.* */ public sealed interface DateTimeFormat { /** - * Formats the given [value] into a string, using this format. + * Formats the given [value] into a string using this format. * * @throws IllegalArgumentException if the value does not contain all the information required by the format. * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.format @@ -80,14 +80,14 @@ public enum class Padding { NONE, /** - * Pad with zeros during formatting. During parsing, the padding is required, or parsing fails. + * Pad with zeros during formatting. During parsing, padding is required; otherwise, parsing fails. * * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.zero */ ZERO, /** - * Pad with spaces during formatting. During parsing, the padding is required, or parsing fails. + * Pad with spaces during formatting. During parsing, padding is required; otherwise, parsing fails. * * @sample kotlinx.datetime.test.samples.format.DateTimeFormatSamples.PaddingSamples.spaces */ diff --git a/core/common/src/format/DateTimeFormatBuilder.kt b/core/common/src/format/DateTimeFormatBuilder.kt index cca7943e2..3c92bfbfd 100644 --- a/core/common/src/format/DateTimeFormatBuilder.kt +++ b/core/common/src/format/DateTimeFormatBuilder.kt @@ -362,7 +362,7 @@ public fun T.alternativeParsing( * * When formatting, the section is formatted if the value of any field in the block is not equal to the default value. * Only [optional] calls where all the fields have default values are permitted. - * See [alternativeParsing] for a way to parse some fields optionally without introducing special formatting behavior. + * See [alternativeParsing] to parse some fields optionally without introducing a particular formatting behavior. * * Example: * ``` diff --git a/core/common/src/format/LocalDateFormat.kt b/core/common/src/format/LocalDateFormat.kt index 4d63ab967..23be10e7e 100644 --- a/core/common/src/format/LocalDateFormat.kt +++ b/core/common/src/format/LocalDateFormat.kt @@ -17,8 +17,8 @@ import kotlinx.datetime.internal.format.parser.Copyable * * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithDate.monthName]. * - * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED], and custom instances can be created - * using the constructor. + * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED]. + * You can also create custom instances using the constructor. * * An [IllegalArgumentException] will be thrown if some month name is empty or there are duplicate names. * @@ -100,12 +100,12 @@ private fun MonthNames.toKotlinCode(): String = when (this.names) { } /** - * A description of how day-of-week names are formatted. + * A description of how the names of weekdays are formatted. * * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithDate.dayOfWeek]. * - * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED], and custom instances can be created - * using the constructor. + * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED]. + * You can also create custom instances using the constructor. * * An [IllegalArgumentException] will be thrown if some day-of-week name is empty or there are duplicate names. * From 63644e6837df50320bacaf7b57d2c76dcbcfc0ac Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 30 Apr 2024 10:36:45 +0200 Subject: [PATCH 25/35] Mention the relevant parts of ISO 8601 when describing formats --- core/common/src/DateTimePeriod.kt | 6 ++++++ core/common/src/LocalDate.kt | 6 ++++++ core/common/src/LocalDateTime.kt | 3 +++ core/common/src/LocalTime.kt | 8 ++++++++ core/common/src/UtcOffset.kt | 4 ++++ 5 files changed, 27 insertions(+) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 85124b114..dd2282c38 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -146,6 +146,8 @@ public sealed class DateTimePeriod { * - `P1DT-3H-2M-4.123456789S`: one day, minus three hours, minus two minutes, * minus four seconds, minus 123456789 nanoseconds; * + * See ISO-8601-1:2019, 5.5.2.2a) + * * @see DateTimePeriod.parse for the detailed description of the format. * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.toStringSample */ @@ -224,6 +226,10 @@ public sealed class DateTimePeriod { * An explicit `+` or `-` sign can be prepended to any number. * `-` means that the number is negative, and `+` has no effect. * + * See ISO-8601-1:2019, 5.5.2.2a) and 5.5.2.2b). + * We combine the two formats into one by allowing the number of weeks to go after the number of months + * and before the number of days. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are * exceeded. * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parsing diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index ab7454f26..fa31883ae 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -129,6 +129,9 @@ public expect class LocalDate : Comparable { * - `0000-08-30` * - `-0001-08-30` * + * See ISO-8601-1:2019, 5.2.2.1b), using the "expanded calendar year" extension from 5.2.2.3a), generalized + * to any number of digits in the year for years that fit in an [Int]. + * * @sample kotlinx.datetime.test.samples.LocalDateSamples.Formats.iso */ public val ISO: DateTimeFormat @@ -142,6 +145,9 @@ public expect class LocalDate : Comparable { * - `00000830` * - `-00010830` * + * See ISO-8601-1:2019, 5.2.2.1a), using the "expanded calendar year" extension from 5.2.2.3a), generalized + * to any number of digits in the year for years that fit in an [Int]. + * * @sample kotlinx.datetime.test.samples.LocalDateSamples.Formats.isoBasic */ public val ISO_BASIC: DateTimeFormat diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index a9712c975..267b4aa54 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -183,6 +183,9 @@ public expect class LocalDateTime : Comparable { * * Guaranteed to parse all strings that [LocalDateTime.toString] produces. * + * See ISO-8601-1:2019, 5.4.2.1b), the version without the offset, together with + * [LocalDate.Formats.ISO] and [LocalTime.Formats.ISO]. + * * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.Formats.iso */ public val ISO: DateTimeFormat diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 276c68223..ee338386c 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -199,6 +199,14 @@ public expect class LocalTime : Comparable { * * Guaranteed to parse all strings that [LocalTime.toString] produces. * + * See ISO-8601-1:2019. + * Any of the extended formats in 5.3.1.2b), 5.3.1.4a), and 5.3.1.3a) can be used, depending on whether + * seconds and fractional seconds are non-zero. + * The length of the fractional part is flexible between one and nine digits. + * The only allowed separator between seconds and fractional seconds is the dot `.`. + * We *forbid* using the time designator `T` to allow for a predictable composition of formats: + * see the note at the end of rule 5.3.5. + * * @sample kotlinx.datetime.test.samples.LocalTimeSamples.Formats.iso */ public val ISO: DateTimeFormat diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index bbaf179dd..2f9e7755c 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -129,6 +129,8 @@ public expect class UtcOffset { * - `-17:16` * - `+10:36:30` * + * See ISO-8601-1:2019, 4.3.13c), extended to support seconds-of-minute when they are non-zero. + * * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.iso */ public val ISO: DateTimeFormat @@ -146,6 +148,8 @@ public expect class UtcOffset { * - `-1716` * - `+103630` * + * See ISO-8601-1:2019, 4.3.13a) and b), extended to support seconds-of-minute when they are non-zero. + * * @see UtcOffset.Formats.FOUR_DIGITS * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.isoBasic */ From e6b758e537bf12c4f840b299d177f2fdc4a1a6aa Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 30 Apr 2024 10:42:56 +0200 Subject: [PATCH 26/35] Incorporate some more proofreading results See https://github.com/Kotlin/kotlinx-datetime/pull/386 --- core/common/src/Instant.kt | 4 ++-- core/common/src/LocalDate.kt | 4 ++-- core/common/src/LocalDateTime.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 1036b311f..ef0a95791 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -285,7 +285,7 @@ public expect class Instant : Comparable { /** * Compares `this` instant with the [other] instant. - * Returns zero if this instant represents the same moment as the other (i.e., when it's equal to other), + * Returns zero if this instant represents the same moment as the other (meaning they are equal to one another), * a negative number if this instant is earlier than the other, * and a positive number if this instant is later than the other. * @@ -449,7 +449,7 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst /** * Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components - * are subtracted in the order from the largest units to the smallest, i.e. from years to nanoseconds. + * are subtracted in the order from the largest units to the smallest, i.e., from years to nanoseconds. * * - If the [DateTimePeriod] only contains time-based components, please consider subtracting a [Duration] instead, * as in `Clock.System.now() - 5.hours`. diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index fa31883ae..21ae39218 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -13,7 +13,7 @@ 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, + * As such, these objects may denote different time intervals 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 @@ -239,7 +239,7 @@ public expect class LocalDate : Comparable { /** * Compares `this` date with the [other] date. - * Returns zero if this date represents the same day as the other (i.e., equal to other), + * Returns zero if this date represents the same day as the other (meaning they are equal to one other), * a negative number if this date is earlier than the other, * and a positive number if this date is later than the other. * diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 267b4aa54..c0543a87b 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -13,7 +13,7 @@ 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. + * This class does not describe specific *moments in time*. For that, use [Instant] values instead. * Instead, you can think of its instances as clock readings, which can be observed in a particular time zone. * For example, `2020-08-30T18:43` is not a *moment in time* since someone in Berlin and Tokyo would witness * this on their clocks at different times, but it is a [LocalDateTime]. @@ -83,7 +83,7 @@ import kotlinx.serialization.Serializable * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor. * See sample 1. * - * Some additional constructors that accept the date's and time's fields directly are provided for convenience. + * Some additional constructors that directly accept the values from date and time fields are provided for convenience. * See sample 2. * * [parse] and [toString] methods can be used to obtain a [LocalDateTime] from and convert it to a string in the From 517ae7a9ebc2c230ae2d72ded583603580d2ee03 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 1 May 2024 17:24:03 +0200 Subject: [PATCH 27/35] Address the review --- core/common/src/DateTimePeriod.kt | 2 +- core/common/src/Instant.kt | 6 ++++-- core/common/src/LocalDate.kt | 5 +++-- core/common/src/LocalDateTime.kt | 4 ++-- core/common/src/LocalTime.kt | 12 ++++++++---- core/common/src/TimeZone.kt | 9 ++++++--- core/common/src/UtcOffset.kt | 8 +++++--- core/common/src/format/LocalDateFormat.kt | 1 - 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index dd2282c38..fa4a96412 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -422,7 +422,7 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * on [LocalDates][LocalDate], but they also can be passed anywhere a [DateTimePeriod] is expected. * * On the JVM, there are `DatePeriod.toJavaPeriod()` and `java.time.Period.toKotlinDatePeriod()` - * extension functions. + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. * * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting */ diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index ef0a95791..982119f33 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -133,8 +133,10 @@ import kotlin.time.* * * ### Platform specifics * - * On the JVM, there are `Instant.toJavaInstant()` and `java.time.Instant.toKotlinInstant()` extension functions. - * On the Darwin platforms, there are `Instant.toNSDate()` and `NSDate.toKotlinInstant()` extension functions. + * On the JVM, there are `Instant.toJavaInstant()` and `java.time.Instant.toKotlinInstant()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * Similarly, on the Darwin platforms, there are `Instant.toNSDate()` and `NSDate.toKotlinInstant()` + * extension functions. * * ### Construction, serialization, and deserialization * diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 21ae39218..36a15b6df 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -35,8 +35,9 @@ import kotlinx.serialization.Serializable * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. * * On the JVM, - * there are `LocalDate.toJavaLocalDate()` and `java.time.LocalDate.toKotlinLocalDate()` extension functions. - * On the Darwin platforms, there is a `LocalDate.toNSDateComponents()` extension function. + * there are `LocalDate.toJavaLocalDate()` and `java.time.LocalDate.toKotlinLocalDate()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * Similarly, on the Darwin platforms, there is a `LocalDate.toNSDateComponents()` extension function. * * ### Construction, serialization, and deserialization * diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index c0543a87b..dad1a2c28 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -66,8 +66,8 @@ import kotlinx.serialization.Serializable * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE]. * * On the JVM, there are `LocalDateTime.toJavaLocalDateTime()` and `java.time.LocalDateTime.toKotlinLocalDateTime()` - * extension functions. - * On the Darwin platforms, there is a `LocalDateTime.toNSDateComponents()` extension function. + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * Similarly, on the Darwin platforms, there is a `LocalDateTime.toNSDateComponents()` extension function. * * ### Construction, serialization, and deserialization * diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index ee338386c..79c61793d 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -48,7 +48,8 @@ import kotlinx.serialization.Serializable * ### Platform specifics * * On the JVM, - * there are `LocalTime.toJavaLocalTime()` and `java.time.LocalTime.toKotlinLocalTime()` extension functions. + * there are `LocalTime.toJavaLocalTime()` and `java.time.LocalTime.toKotlinLocalTime()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. * * ### Construction, serialization, and deserialization * @@ -103,7 +104,8 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [secondOfDay] is outside the `0 until 86400` range, * with 86400 being the number of seconds in a calendar day. * - * It is incorrect to pass the number of seconds since the start of the day to this function. + * It is incorrect to pass to this function + * the number of seconds that have physically elapsed since the start of the day. * The reason is that, due to the daylight-saving-time transitions, the number of seconds since the start * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. * Use [Instant] to perform reliable time arithmetic. @@ -123,7 +125,8 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [millisecondOfDay] is outside the `0 until 86400 * 1_000` range, * with 86400 being the number of seconds in a calendar day. * - * It is incorrect to pass the number of milliseconds since the start of the day to this function. + * It is incorrect to pass to this function + * the number of milliseconds that have physically elapsed since the start of the day. * The reason is that, due to the daylight-saving-time transitions, the number of milliseconds since the start * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. * Use [Instant] to perform reliable time arithmetic. @@ -142,7 +145,8 @@ public expect class LocalTime : Comparable { * @throws IllegalArgumentException if [nanosecondOfDay] is outside the `0 until 86400 * 1_000_000_000` range, * with 86400 being the number of seconds in a calendar day. * - * It is incorrect to pass the number of nanoseconds since the start of the day to this function. + * It is incorrect to pass to this function + * the number of nanoseconds that have physically elapsed since the start of the day. * The reason is that, due to the daylight-saving-time transitions, the number of nanoseconds since the start * of the day is not a constant value: clocks could be shifted by an hour or more on some dates. * Use [Instant] to perform reliable time arithmetic. diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 1ce51ee4f..69b0de71c 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -25,8 +25,10 @@ import kotlinx.serialization.Serializable * For interaction with `kotlinx-serialization`, [TimeZoneSerializer] is provided that serializes the time zone as its * identifier. * - * On the JVM, there are `TimeZone.toJavaZoneId()` and `java.time.ZoneId.toKotlinTimeZone()` extension functions. - * On the Darwin platforms, there are `TimeZone.toNSTimeZone()` and `NSTimeZone.toKotlinTimeZone()` extension functions. + * On the JVM, there are `TimeZone.toJavaZoneId()` and `java.time.ZoneId.toKotlinTimeZone()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * Similarly, on the Darwin platforms, there are `TimeZone.toNSTimeZone()` and `NSTimeZone.toKotlinTimeZone()` extension + * functions. * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.usage */ @@ -142,7 +144,8 @@ public expect open class TimeZone { * changes in legislation or other reasons. * * On the JVM, there are `FixedOffsetTimeZone.toJavaZoneOffset()` and - * `java.time.ZoneOffset.toKotlinFixedOffsetTimeZone()` extension functions. + * `java.time.ZoneOffset.toKotlinFixedOffsetTimeZone()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. * Note also the functions available for [TimeZone] in general. * * @sample kotlinx.datetime.test.samples.TimeZoneSamples.FixedOffsetTimeZoneSamples.casting diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 2f9e7755c..5c5bd7fdb 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -30,7 +30,7 @@ import kotlinx.serialization.Serializable * ### Platform specifics * * On the JVM, there are `UtcOffset.toJavaZoneOffset()` and `java.time.ZoneOffset.toKotlinUtcOffset()` - * extension functions. + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. * * ### Construction, serialization, and deserialization * @@ -218,8 +218,10 @@ public fun UtcOffset(): UtcOffset = UtcOffset.ZERO /** * Returns the fixed-offset time zone with the given UTC offset. * - * **Pitfall**: if the offset is not fixed, the returned time zone will not reflect the changes in the offset. - * Use [TimeZone.of] with a IANA timezone name to obtain a time zone that can handle changes in the offset. + * **Pitfall**: UTC offsets are static values and do not change with time. + * If the logic requires that the offset changes with time, for example, to account for daylight-saving-time + * transitions, use [TimeZone.of] with a IANA time zone name to obtain a time zone that can handle + * changes in the offset. * * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.asFixedOffsetTimeZone */ diff --git a/core/common/src/format/LocalDateFormat.kt b/core/common/src/format/LocalDateFormat.kt index 23be10e7e..c94a24040 100644 --- a/core/common/src/format/LocalDateFormat.kt +++ b/core/common/src/format/LocalDateFormat.kt @@ -111,7 +111,6 @@ private fun MonthNames.toKotlinCode(): String = when (this.names) { * * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.usage * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.constructionFromList ->>>>>>> 585441e (WIP: add samples for some formatting APIs) */ public class DayOfWeekNames( /** From f45e4930fa03b0be51abc073ceb1b2f4739cdfab Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 3 May 2024 17:10:57 +0200 Subject: [PATCH 28/35] Address the review --- core/common/src/Instant.kt | 18 ++--- .../test/samples/DateTimePeriodSamples.kt | 77 ++++++++++--------- core/common/test/samples/InstantSamples.kt | 4 +- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 982119f33..11e3d9be6 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -213,7 +213,7 @@ public expect class Instant : Comparable { /** * The number of nanoseconds by which this instant is later than [epochSeconds] from the epoch instant. * - * The value is always positive and lies in the range `0..999_999_999`. + * The value is always non-negative and lies in the range `0..999_999_999`. * * @see fromEpochSeconds * @sample kotlinx.datetime.test.samples.InstantSamples.nanosecondsOfSecond @@ -240,10 +240,10 @@ public expect class Instant : Comparable { * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. * - * **Pitfall**: do not use [Duration] values obtained via [Duration.Companion.days], as this is misleading: - * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers - * a day to be 24 hours. - * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. + * **Pitfall**: [Duration.Companion.days] are multiples of 24 hours and are not calendar-based. + * Consider using the [plus] overload that accepts a multiple of a [DateTimeUnit] instead for calendar-based + * operations instead of using [Duration]. + * For an explanation of why some days are not 24 hours, see [DateTimeUnit.DayBased]. * * @sample kotlinx.datetime.test.samples.InstantSamples.plusDuration */ @@ -257,10 +257,10 @@ public expect class Instant : Comparable { * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. * - * **Pitfall**: do not use [Duration] values obtained via [Duration.Companion.days], as this is misleading: - * in `kotlinx-datetime`, adding a day is a calendar-based operation, whereas [Duration] always considers - * a day to be 24 hours. - * For an explanation of why this is error-prone, see [DateTimeUnit.DayBased]. + * **Pitfall**: [Duration.Companion.days] are multiples of 24 hours and are not calendar-based. + * Consider using the [minus] overload that accepts a multiple of a [DateTimeUnit] instead for calendar-based + * operations instead of using [Duration]. + * For an explanation of why some days are not 24 hours, see [DateTimeUnit.DayBased]. * * @sample kotlinx.datetime.test.samples.InstantSamples.minusDuration */ diff --git a/core/common/test/samples/DateTimePeriodSamples.kt b/core/common/test/samples/DateTimePeriodSamples.kt index 0d1636097..bd308bc48 100644 --- a/core/common/test/samples/DateTimePeriodSamples.kt +++ b/core/common/test/samples/DateTimePeriodSamples.kt @@ -29,9 +29,9 @@ class DateTimePeriodSamples { @Test fun simpleParsingAndFormatting() { // Parsing and formatting a DateTimePeriod - val string = "-P2M-3DT-4H" + val string = "P-2M-3DT-4H60M" val period = DateTimePeriod.parse(string) - check(period.toString() == "P-2M3DT4H") + check(period.toString() == "-P2M3DT3H") } @Test @@ -64,7 +64,7 @@ class DateTimePeriodSamples { @Test fun parsing() { // Parsing a string representation of a DateTimePeriod - DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S").apply { + with(DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S")) { check(years == 1) check(months == 2) check(days == 3) @@ -73,13 +73,13 @@ class DateTimePeriodSamples { check(seconds == 6) check(nanoseconds == 7) } - DateTimePeriod.parse("P14M-16DT5H").apply { + with(DateTimePeriod.parse("P14M-16DT5H")) { check(years == 1) check(months == 2) check(days == -16) check(hours == 5) } - DateTimePeriod.parse("-P2M16DT5H").apply { + with(DateTimePeriod.parse("-P2M16DT5H")) { check(years == 0) check(months == -2) check(days == -16) @@ -96,6 +96,7 @@ class DateTimePeriodSamples { check(dateTimePeriod.days == -60) // days are separate from months and are not normalized check(dateTimePeriod.hours == 14) // the negative minutes overflowed to hours check(dateTimePeriod.minutes == 59) // (-61 minutes) + (2 hours) * (60 minutes / hour) + val datePeriod = DateTimePeriod(months = 15, days = 3, hours = 2, minutes = -120) check(datePeriod is DatePeriod) // the time components are zero } @@ -106,42 +107,42 @@ class DateTimePeriodSamples { check(130.minutes.toDateTimePeriod() == DateTimePeriod(minutes = 130)) check(2.days.toDateTimePeriod() == DateTimePeriod(days = 0, hours = 48)) } +} - class DatePeriodSamples { +class DatePeriodSamples { - @Test - fun simpleParsingAndFormatting() { - // Parsing and formatting a DatePeriod - val datePeriod1 = DatePeriod(years = 1, days = 3) - val string = datePeriod1.toString() - check(string == "P1Y3D") - val datePeriod2 = DatePeriod.parse(string) - check(datePeriod1 == datePeriod2) - } + @Test + fun simpleParsingAndFormatting() { + // Parsing and formatting a DatePeriod + val datePeriod1 = DatePeriod(years = 1, days = 3) + val string = datePeriod1.toString() + check(string == "P1Y3D") + val datePeriod2 = DatePeriod.parse(string) + check(datePeriod1 == datePeriod2) + } - @Test - fun construction() { - // Constructing a DatePeriod using its constructor - val datePeriod = DatePeriod(years = 1, months = 16, days = 60) - check(datePeriod.years == 2) // 1 year + (16 months / 12) - check(datePeriod.months == 4) // 16 months % 12 - check(datePeriod.days == 60) - // the time components are always zero: - check(datePeriod.hours == 0) - check(datePeriod.minutes == 0) - check(datePeriod.seconds == 0) - check(datePeriod.nanoseconds == 0) - } + @Test + fun construction() { + // Constructing a DatePeriod using its constructor + val datePeriod = DatePeriod(years = 1, months = 16, days = 60) + check(datePeriod.years == 2) // 1 year + (16 months / 12) + check(datePeriod.months == 4) // 16 months % 12 + check(datePeriod.days == 60) + // the time components are always zero: + check(datePeriod.hours == 0) + check(datePeriod.minutes == 0) + check(datePeriod.seconds == 0) + check(datePeriod.nanoseconds == 0) + } - @Test - fun parsing() { - // Parsing a string representation of a DatePeriod - // ISO duration strings are supported: - val datePeriod = DatePeriod.parse("P1Y16M60D") - check(datePeriod == DatePeriod(years = 2, months = 4, days = 60)) - // it's okay to have time components as long as they amount to zero in total: - val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M") - check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3)) - } + @Test + fun parsing() { + // Parsing a string representation of a DatePeriod + // ISO duration strings are supported: + val datePeriod = DatePeriod.parse("P1Y16M60D") + check(datePeriod == DatePeriod(years = 2, months = 4, days = 60)) + // it's okay to have time components as long as they amount to zero in total: + val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M") + check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3)) } } diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index 465fc6338..ff34ddd8b 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -75,7 +75,9 @@ class InstantSamples { repeat(100) { val instant1 = randomInstant() val instant2 = randomInstant() - check((instant1 < instant2) == (instant1.toEpochMilliseconds() < instant2.toEpochMilliseconds())) + // in the UTC time zone, earlier instants are represented as earlier datetimes + check((instant1 < instant2) == + (instant1.toLocalDateTime(TimeZone.UTC) < instant2.toLocalDateTime(TimeZone.UTC))) } } From 8a0e356b4bcbe9b04df4ca110e60d327b1e57520 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 8 May 2024 14:13:28 +0200 Subject: [PATCH 29/35] ~fixup --- core/common/src/DateTimePeriod.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index fa4a96412..0ce452ece 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -424,7 +424,7 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * On the JVM, there are `DatePeriod.toJavaPeriod()` and `java.time.Period.toKotlinDatePeriod()` * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. * - * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.DatePeriodSamples.simpleParsingAndFormatting */ @Serializable(with = DatePeriodIso8601Serializer::class) public class DatePeriod internal constructor( @@ -445,7 +445,7 @@ public class DatePeriod internal constructor( * For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`. * * @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int]. - * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.construction + * @sample kotlinx.datetime.test.samples.DatePeriodSamples.construction */ public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days) // avoiding excessive computations @@ -473,7 +473,7 @@ public class DatePeriod internal constructor( * or any time components are not zero. * * @see DateTimePeriod.parse - * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.parsing + * @sample kotlinx.datetime.test.samples.DatePeriodSamples.parsing */ public fun parse(text: String): DatePeriod = when (val period = DateTimePeriod.parse(text)) { From f70e422d1d8b50c13e0c73ce65825b61694a836a Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 11:25:05 +0200 Subject: [PATCH 30/35] Remove separate samples for Long overloads (plus a small fixup) --- core/common/src/Instant.kt | 8 ++--- core/common/src/LocalDate.kt | 8 ++--- core/common/test/samples/InstantSamples.kt | 38 -------------------- core/common/test/samples/LocalDateSamples.kt | 25 ++----------- 4 files changed, 11 insertions(+), 68 deletions(-) diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 11e3d9be6..6168b7b8f 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -700,7 +700,7 @@ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * It is also not needed when adding date-based units to a [LocalDate]. * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. - * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnitLong + * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -715,7 +715,7 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo * It is also not needed when subtracting date-based units from a [LocalDate]. * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. - * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnitLong + * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnit */ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = if (value != Long.MIN_VALUE) { @@ -732,7 +732,7 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. * - * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnitLong + * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant @@ -744,7 +744,7 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta * * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. * - * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnitLong + * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit */ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = if (value != Long.MIN_VALUE) { diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 36a15b6df..0146d6f95 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -456,7 +456,7 @@ public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, u * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. - * @sample kotlinx.datetime.test.samples.LocalDateSamples.plusInt + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plus */ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -469,7 +469,7 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. - * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusInt + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate @@ -482,7 +482,7 @@ public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): Loc * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. - * @sample kotlinx.datetime.test.samples.LocalDateSamples.plusLong + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plus */ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate @@ -495,7 +495,7 @@ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): Loc * The value is rounded toward zero. * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. - * @sample kotlinx.datetime.test.samples.LocalDateSamples.minusLong + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index ff34ddd8b..888bd48f6 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -284,44 +284,6 @@ class InstantSamples { check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) } - @Test - @Ignore // only the JVM has the range wide enough - fun plusDateTimeUnitLong() { - // Finding a moment that's later than the starting point by the given large length of calendar time - val zone = TimeZone.of("Europe/Berlin") - val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) - val tenTrillionDaysLater = now.plus(10_000_000_000L, DateTimeUnit.DAY, zone) - check(tenTrillionDaysLater.toLocalDateTime(zone).date == LocalDate(27_381_094, Month.MAY, 12)) - } - - @Test - @Ignore // only the JVM has the range wide enough - fun minusDateTimeUnitLong() { - // Finding a moment that's earlier than the starting point by the given large length of calendar time - val zone = TimeZone.of("Europe/Berlin") - val now = LocalDate(2024, Month.APRIL, 16).atTime(13, 30).toInstant(zone) - val tenTrillionDaysAgo = now.minus(10_000_000_000L, DateTimeUnit.DAY, zone) - check(tenTrillionDaysAgo.toLocalDateTime(zone).date == LocalDate(-27_377_046, Month.MARCH, 22)) - } - - @Test - fun plusTimeBasedUnitLong() { - // Finding a moment that's later than the starting point by the given amount of real time - val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) - val quadrillion = 1_000_000_000_000L - val quadrillionSecondsLater = startInstant.plus(quadrillion, DateTimeUnit.SECOND) - check(quadrillionSecondsLater.epochSeconds == quadrillion) - } - - @Test - fun minusTimeBasedUnitLong() { - // Finding a moment that's earlier than the starting point by the given amount of real time - val startInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) - val quadrillion = 1_000_000_000_000L - val quadrillionSecondsEarlier = startInstant.minus(quadrillion, DateTimeUnit.SECOND) - check(quadrillionSecondsEarlier.epochSeconds == -quadrillion) - } - /** copy of [untilAsDateTimeUnit] */ @Test fun minusAsDateTimeUnit() { diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index 32a29cbac..194af1f8e 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -88,8 +88,7 @@ class LocalDateSamples { @Test fun dayOfMonth() { // Getting the day of the month - repeat(30) { - val dayOfMonth = it + 1 + for (dayOfMonth in 1..30) { check(LocalDate(2024, Month.APRIL, dayOfMonth).dayOfMonth == dayOfMonth) } } @@ -245,7 +244,7 @@ class LocalDateSamples { } @Test - fun plusInt() { + fun plus() { // Adding a number of days or months to a date val today = LocalDate(2024, Month.APRIL, 16) val tenDaysLater = today.plus(10, DateTimeUnit.DAY) @@ -255,7 +254,7 @@ class LocalDateSamples { } @Test - fun minusInt() { + fun minus() { // Subtracting a number of days or months from a date val today = LocalDate(2024, Month.APRIL, 16) val tenDaysAgo = today.minus(10, DateTimeUnit.DAY) @@ -264,24 +263,6 @@ class LocalDateSamples { check(twoMonthsAgo == LocalDate(2024, Month.FEBRUARY, 16)) } - @Test - @Ignore // only the JVM has the range wide enough - fun plusLong() { - // Adding a large number of days to a date - val today = LocalDate(2024, Month.APRIL, 16) - val tenTrillionDaysLater = today.plus(10_000_000_000L, DateTimeUnit.DAY) - check(tenTrillionDaysLater == LocalDate(27_381_094, Month.MAY, 12)) - } - - @Test - @Ignore // only the JVM has the range wide enough - fun minusLong() { - // Subtracting a large number of days from a date - val today = LocalDate(2024, Month.APRIL, 16) - val tenTrillionDaysAgo = today.minus(10_000_000_000L, DateTimeUnit.DAY) - check(tenTrillionDaysAgo == LocalDate(-27_377_046, Month.MARCH, 22)) - } - class Formats { @Test fun iso() { From 6c1423a4e52aa400e729b1d9af7147a6f60f68b7 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 11:28:13 +0200 Subject: [PATCH 31/35] Use yyyy instead of uuuu in the Unicode pattern sample --- core/common/test/samples/format/DateTimeFormatSamples.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/test/samples/format/DateTimeFormatSamples.kt b/core/common/test/samples/format/DateTimeFormatSamples.kt index f1596986f..aabd28bbb 100644 --- a/core/common/test/samples/format/DateTimeFormatSamples.kt +++ b/core/common/test/samples/format/DateTimeFormatSamples.kt @@ -63,17 +63,17 @@ class DateTimeFormatSamples { // Printing a given date-time format as a Kotlin code snippet that creates the same format val customFormat = LocalDate.Format { @OptIn(FormatStringsInDatetimeFormats::class) - byUnicodePattern("MM/dd uuuu") + byUnicodePattern("MM/dd yyyy") } val customFormatAsKotlinCode = DateTimeFormat.formatAsKotlinBuilderDsl(customFormat) check( - customFormatAsKotlinCode == """ + customFormatAsKotlinCode.contains(""" monthNumber() char('/') dayOfMonth() char(' ') year() - """.trimIndent() + """.trimIndent()) ) } From fa59e05f17a32ece8cd78c71d9d4d690c7f305f8 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 11:37:09 +0200 Subject: [PATCH 32/35] Use Instant.fromEpochSeconds in samples more --- core/common/test/samples/InstantSamples.kt | 36 ++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index 888bd48f6..8fec354b7 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -47,17 +47,19 @@ class InstantSamples { @Test fun plusDuration() { // Finding a moment that's later than the starting point by the given amount of real time - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val fiveHoursLater = instant + 5.hours - check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) + check(fiveHoursLater.epochSeconds == 12 * 60 * 60L) + check(fiveHoursLater.nanosecondsOfSecond == 123_456_789) } @Test fun minusDuration() { // Finding a moment that's earlier than the starting point by the given amount of real time - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val fiveHoursEarlier = instant - 5.hours - check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) + check(fiveHoursEarlier.epochSeconds == 2 * 60 * 60L) + check(fiveHoursEarlier.nanosecondsOfSecond == 123_456_789) } @Test @@ -84,7 +86,7 @@ class InstantSamples { @Test fun toStringSample() { // Converting an Instant to a string - check(Instant.fromEpochMilliseconds(0).toString() == "1970-01-01T00:00:00Z") + check(Instant.fromEpochSeconds(0).toString() == "1970-01-01T00:00:00Z") } @Test @@ -113,8 +115,8 @@ class InstantSamples { @Test fun parsing() { // Parsing an Instant from a string using predefined and custom formats - check(Instant.parse("1970-01-01T00:00:00Z") == Instant.fromEpochMilliseconds(0)) - check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochMilliseconds(0)) + check(Instant.parse("1970-01-01T00:00:00Z") == Instant.fromEpochSeconds(0)) + check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochSeconds(0)) } @Test @@ -189,8 +191,8 @@ class InstantSamples { @Test fun untilAsTimeBasedUnit() { // Finding the difference between two instants in terms of the given measurement unit - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) - val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(0) + val otherInstant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val hoursBetweenInstants = instant.until(otherInstant, DateTimeUnit.HOUR) check(hoursBetweenInstants == 7L) } @@ -271,17 +273,19 @@ class InstantSamples { @Test fun plusTimeBasedUnit() { // Finding a moment that's later than the starting point by the given amount of real time - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val fiveHoursLater = instant.plus(5, DateTimeUnit.HOUR) - check(fiveHoursLater.toEpochMilliseconds() == 12 * 60 * 60 * 1000L) + check(fiveHoursLater.epochSeconds == 12 * 60 * 60L) + check(fiveHoursLater.nanosecondsOfSecond == 123_456_789) } @Test fun minusTimeBasedUnit() { // Finding a moment that's earlier than the starting point by the given amount of real time - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val fiveHoursEarlier = instant.minus(5, DateTimeUnit.HOUR) - check(fiveHoursEarlier.toEpochMilliseconds() == 2 * 60 * 60 * 1000L) + check(fiveHoursEarlier.epochSeconds == 2 * 60 * 60L) + check(fiveHoursEarlier.nanosecondsOfSecond == 123_456_789) } /** copy of [untilAsDateTimeUnit] */ @@ -302,8 +306,8 @@ class InstantSamples { @Test fun minusAsTimeBasedUnit() { // Finding a moment that's earlier than the starting point by a given amount of real time - val instant = Instant.fromEpochMilliseconds(epochMilliseconds = 0) - val otherInstant = Instant.fromEpochMilliseconds(epochMilliseconds = 7 * 60 * 60 * 1000) + val instant = Instant.fromEpochSeconds(0) + val otherInstant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) val hoursBetweenInstants = otherInstant.minus(instant, DateTimeUnit.HOUR) check(hoursBetweenInstants == 7L) } @@ -311,7 +315,7 @@ class InstantSamples { @Test fun formatting() { // Formatting an Instant to a string using predefined and custom formats - val epochStart = Instant.fromEpochMilliseconds(0) + val epochStart = Instant.fromEpochSeconds(0) check(epochStart.toString() == "1970-01-01T00:00:00Z") check(epochStart.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) == "1970-01-01T00:00:00Z") val customFormat = DateTimeComponents.Format { From a529d44dde4353ce77b421553d31f5ff9e1b296d Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 13:12:04 +0200 Subject: [PATCH 33/35] Fix a broken sample reference --- core/common/src/format/Unicode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/format/Unicode.kt b/core/common/src/format/Unicode.kt index 79d49c9e2..c61f1dd0a 100644 --- a/core/common/src/format/Unicode.kt +++ b/core/common/src/format/Unicode.kt @@ -100,7 +100,7 @@ public annotation class FormatStringsInDatetimeFormats * @throws IllegalArgumentException if the pattern is invalid or contains unsupported directives. * @throws IllegalArgumentException if the builder is incompatible with the specified directives. * @throws UnsupportedOperationException if the kotlinx-datetime library does not support the specified directives. - * @sample kotlinx.datetime.test.samples.format.UnicodeSample.byUnicodePattern + * @sample kotlinx.datetime.test.samples.format.UnicodeSamples.byUnicodePattern */ @FormatStringsInDatetimeFormats public fun DateTimeFormatBuilder.byUnicodePattern(pattern: String) { From a0a13355f48187c5fd2881a150303760cf7cfafe Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 13:20:16 +0200 Subject: [PATCH 34/35] Fix broken table rendering --- core/common/src/format/Unicode.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/src/format/Unicode.kt b/core/common/src/format/Unicode.kt index c61f1dd0a..2a27b4346 100644 --- a/core/common/src/format/Unicode.kt +++ b/core/common/src/format/Unicode.kt @@ -53,6 +53,7 @@ public annotation class FormatStringsInDatetimeFormats * The list of supported directives is as follows: * * | **Directive** | **Meaning** | + * | ------------------- | --------------------------------------------------------------------------------------- | * | `'string'` | literal `string`, without quotes | * | `'''` | literal char `'` | * | `[fmt]` | equivalent to `fmt` during formatting, but during parsing also accepts the empty string | @@ -75,6 +76,7 @@ public annotation class FormatStringsInDatetimeFormats * and seconds are zero-padded to two digits. Also, hours are unconditionally present. * * | **Directive** | **Minutes** | **Seconds** | **Separator** | **Representation of zero** | + * | ---------------------- | ----------- | ----------- | ------------- | -------------------------- | * | `X` | unless zero | never | none | `Z` | * | `XX` | always | never | none | `Z` | * | `XXX` | always | never | colon | `Z` | From 8851274d1a3e998615bd7d235132b50e94e83ad1 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 10 May 2024 14:42:18 +0200 Subject: [PATCH 35/35] Properly display samples in Dokka --- core/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8dfbb094f..cbf6c80ed 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -397,7 +397,9 @@ val downloadWindowsZonesMapping by tasks.registering { tasks.withType().configureEach { pluginsMapConfiguration.set(mapOf("org.jetbrains.dokka.base.DokkaBase" to """{ "templatesDir" : "${projectDir.toString().replace('\\', '/')}/dokka-templates" }""")) + failOnWarning.set(true) dokkaSourceSets.configureEach { + kotlin.sourceSets.commonTest.get().kotlin.srcDirs.forEach { samples.from(it) } // reportUndocumented.set(true) // much noisy output about `hashCode` and serializer encoders, decoders etc skipDeprecated.set(true) // Enum members and undocumented toString()