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/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() diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 7849d6700..6d3dee7c3 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -11,20 +11,50 @@ import kotlin.time.* * A source of [Instant] values. * * See [Clock.System][Clock.System] for the clock instance that queries the operating system. + * + * 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. */ 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 [Clock.System] it is completely expected that the opposite will happen, + * and it must be taken into account. + * 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. */ 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 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. + * + * 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 you need predictable intervals between successive measurements, consider using [TimeSource.Monotonic]. + * + * 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 */ public object System : Clock { override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now() } + /** A companion object used purely for namespacing. */ public companion object { } @@ -32,12 +62,20 @@ 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 instant. + * + * @sample kotlinx.datetime.test.samples.ClockSamples.todayIn */ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = now().toLocalDateTime(timeZone).date /** * Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark. + * + * **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..0ce452ece 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 @@ -15,14 +17,57 @@ 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]), and [days] ([DateTimeUnit.DAY]). * - * The time components are: [hours], [minutes], [seconds], [nanoseconds]. + * The time components are: [hours] ([DateTimeUnit.HOUR]), [minutes] ([DateTimeUnit.MINUTE]), + * [seconds] ([DateTimeUnit.SECOND]), and [nanoseconds] ([DateTimeUnit.NANOSECOND]). * - * 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. + * 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)`. + * + * 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." + * + * 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 + * + * [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. + * + * [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], + * will be returned if all time components happen to be zero. + * + * A `DateTimePeriod` can be constructed using the constructor function with the same name. + * 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. + * 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 @@ -30,41 +75,58 @@ 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 get normalized together with 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. + * 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 /** - * The number of whole hours in this period. + * The number of whole hours in this period. Can be negative. + * + * 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 */ 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() @@ -72,9 +134,22 @@ 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`. + * + * Note that the ISO 8601 duration is not the same as [Duration], + * but instead includes the date components, like [DateTimePeriod] does. * - * @see DateTimePeriod.parse + * 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 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 */ override fun toString(): String = buildString { val sign = if (allNonpositive()) { append('-'); -1 } else 1 @@ -119,19 +194,45 @@ 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. + * 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: + * Examples of durations in the ISO 8601 format: * - `P1Y40D` is one year and 40 days * - `-P1DT1H` is minus (one day and one hour) * - `P1DT-1H` is one day minus one hour * - `-PT0.000000001S` is minus one nanosecond * + * 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`. + * - 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 `.`. + * + * 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 */ public fun parse(text: String): DateTimePeriod { fun parseException(message: String, position: Int): Nothing = @@ -304,19 +405,48 @@ 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. * - * `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. + * ``` + * 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. + * + * 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.DatePeriodSamples.simpleParsingAndFormatting */ @Serializable(with = DatePeriodIso8601Serializer::class) public class DatePeriod internal constructor( internal override val totalMonths: Int, override val days: Int, ) : DateTimePeriod() { + /** + * Constructs a new [DatePeriod]. + * + * 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, + * `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]. + * @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 /** The number of whole hours in this period. Always equal to zero. */ @@ -334,7 +464,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. @@ -343,6 +473,7 @@ public class DatePeriod internal constructor( * or any time components are not zero. * * @see DateTimePeriod.parse + * @sample kotlinx.datetime.test.samples.DatePeriodSamples.parsing */ public fun parse(text: String): DatePeriod = when (val period = DateTimePeriod.parse(text)) { @@ -397,14 +528,19 @@ 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] * overflows a [Long]. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.constructorFunction */ public fun DateTimePeriod( years: Int = 0, @@ -422,6 +558,13 @@ 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. + * + * @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) @@ -429,6 +572,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( @@ -440,6 +586,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/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index a219cc7ae..97ab32f77 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -12,34 +12,79 @@ 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, 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 + * + * 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 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. + * + * [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 details. + * + * [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 + * 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], * [DateTimeUnit.MONTH], and others. * * Two ways are provided to create custom [DateTimeUnit] instances: - * - By multiplying an existing unit on the right by an integer scalar: for example, `DateTimeUnit.NANOSECOND * 10`. - * - By constructing an instance manually with [TimeBased], [DayBased], or [MonthBased]: for example, - * `TimeBased(nanoseconds = 10)`. + * - 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)`. * - * 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. + * + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.construction */ @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. + * @sample kotlinx.datetime.test.samples.DateTimeUnitSamples.multiplication + */ 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 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() { @@ -81,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 @@ -94,11 +141,15 @@ 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 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() { @@ -111,19 +162,25 @@ 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. + * 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 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() { @@ -145,14 +202,20 @@ 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. + * 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 */ @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 496d61bef..32c18f367 100644 --- a/core/common/src/DayOfWeek.kt +++ b/core/common/src/DayOfWeek.kt @@ -7,6 +7,11 @@ 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. + * + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.usage */ public expect enum class DayOfWeek { MONDAY, @@ -19,12 +24,17 @@ 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. + * + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.isoDayNumber */ 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. + * + * @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/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..6168b7b8f 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -8,29 +8,191 @@ 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.* /** * 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 * 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 that, consider using [TimeSource.Monotonic] and [TimeMark] instead of [Clock.System] and [Instant]. + * + * ### Obtaining human-readable representations + * + * #### Date and time + * + * [Instant] is essentially the number of seconds and nanoseconds since a designated moment in time, + * stored as something like `1709898983.123456789`. + * [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]: + * + * ``` + * 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]. + * + * #### 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 [Duration]s to and subtract them from an [Instant]: + * + * ``` + * 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]: + * + * ``` + * Clock.System.now().plus(4, DateTimeUnit.HOUR) // 4 hours from now + * ``` + * + * Also, there is a [minus] operator that returns the [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 [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 + * 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] + * 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 + * ``` + * + * ### Platform specifics + * + * 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 + * + * [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. + * + * ``` + * 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. + * + * ``` + * 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. + * + * @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,27 +205,30 @@ 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 + * @sample kotlinx.datetime.test.samples.InstantSamples.epochSeconds */ public val epochSeconds: Long /** * 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 Instant.fromEpochSeconds + * @see fromEpochSeconds + * @sample kotlinx.datetime.test.samples.InstantSamples.nanosecondsOfSecond */ public val nanosecondsOfSecond: Int /** * 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. * - * @see Instant.fromEpochMilliseconds + * @see fromEpochMilliseconds + * @sample kotlinx.datetime.test.samples.InstantSamples.toEpochMilliseconds */ public fun toEpochMilliseconds(): Long @@ -74,6 +239,13 @@ 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**: [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 */ public operator fun plus(duration: Duration): Instant @@ -84,6 +256,13 @@ 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**: [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 */ public operator fun minus(duration: Duration): Instant @@ -96,29 +275,39 @@ 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]. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusInstant */ 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 (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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.compareToSample */ 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. + * 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. + * @sample kotlinx.datetime.test.samples.InstantSamples.toStringSample */ public override fun toString(): String @@ -131,8 +320,12 @@ 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 + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochMilliseconds */ public fun fromEpochMilliseconds(epochMilliseconds: Long): Instant @@ -141,6 +334,13 @@ 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 + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochSeconds */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long = 0): Instant @@ -149,6 +349,13 @@ 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 + * @sample kotlinx.datetime.test.samples.InstantSamples.fromEpochSecondsIntNanos */ public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant @@ -165,11 +372,13 @@ 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. * * @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, @@ -180,7 +389,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 +399,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 @@ -197,11 +410,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 @@ -213,19 +434,35 @@ 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`. + * 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]. + * @sample kotlinx.datetime.test.samples.InstantSamples.plusPeriod */ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant /** * 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`. + * 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]. + * @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 @@ -245,12 +482,13 @@ 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. + * @sample kotlinx.datetime.test.samples.InstantSamples.periodUntil */ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod @@ -259,13 +497,14 @@ 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. * * @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 @@ -273,11 +512,13 @@ 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.untilAsTimeBasedUnit */ public fun Instant.until(other: Instant, unit: DateTimeUnit.TimeBased): Long = try { @@ -296,6 +537,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() @@ -307,6 +549,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() @@ -318,6 +561,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() @@ -328,13 +572,14 @@ 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. * @see Instant.periodUntil + * @sample kotlinx.datetime.test.samples.InstantSamples.minusInstantInZone */ public fun Instant.minus(other: Instant, timeZone: TimeZone): DateTimePeriod = other.periodUntil(this, timeZone) @@ -392,7 +637,11 @@ 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][LocalDate.plus]. + * * @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 @@ -403,7 +652,14 @@ 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]. + * + * 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 @@ -414,6 +670,8 @@ public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZo * If the [value] 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit */ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = plus(value.toLong(), unit) @@ -425,6 +683,8 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * If the [value] 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit */ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = minus(value.toLong(), unit) @@ -436,7 +696,11 @@ 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]. + * * @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: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant @@ -447,7 +711,11 @@ 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]. + * * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnit */ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = if (value != Long.MIN_VALUE) { @@ -463,6 +731,8 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I * If the [value] 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant @@ -473,6 +743,8 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta * If the [value] 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. + * + * @sample kotlinx.datetime.test.samples.InstantSamples.minusTimeBasedUnit */ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = if (value != Long.MIN_VALUE) { @@ -491,7 +763,8 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = * 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. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusAsDateTimeUnit */ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = other.until(this, unit, timeZone) @@ -504,7 +777,8 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone) * * 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. + * @sample kotlinx.datetime.test.samples.InstantSamples.minusAsTimeBasedUnit */ public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long = other.until(this, unit) @@ -518,6 +792,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/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0a411a8e1..0146d6f95 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -6,19 +6,63 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.datetime.serializers.* 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 * 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. + * + * ### 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 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 + * + * [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`; + * [toEpochDays] is the inverse operation. + * 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. + * See sample 3. + * + * [parse] and [LocalDate.format] both support custom formats created with [Format] or defined in [Formats]. + * 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 { @@ -29,11 +73,13 @@ 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. * * @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 @@ -41,32 +87,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 @@ -94,6 +129,11 @@ public expect class LocalDate : Comparable { * - `+12020-08-30` * - `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 @@ -105,6 +145,11 @@ public expect class LocalDate : Comparable { * - `+120200830` * - `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 } @@ -115,13 +160,14 @@ 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 * - * @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 */ public constructor(year: Int, monthNumber: Int, dayOfMonth: Int) @@ -129,32 +175,57 @@ 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 * - * @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 */ 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-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 /** @@ -163,23 +234,27 @@ 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 /** * 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 (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. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.compareToSample */ 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. * @see LocalDate.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.toStringSample */ public override fun toString(): String } @@ -187,6 +262,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) @@ -199,8 +276,15 @@ 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 + * 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 = LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond) @@ -210,32 +294,42 @@ 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. + * + * @sample kotlinx.datetime.test.samples.LocalDateSamples.atTime */ 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. + * 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 * @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 /** - * 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. + * 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 * @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) { 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) } @@ -246,13 +340,14 @@ 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). * - * @see LocalDate.minus + * @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 @@ -262,13 +357,14 @@ 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). * - * @see LocalDate.periodUntil + * @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) @@ -276,48 +372,61 @@ 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. + * * 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 /** * 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 + * @sample kotlinx.datetime.test.samples.LocalDateSamples.daysUntil */ 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 + * @sample kotlinx.datetime.test.samples.LocalDateSamples.monthsUntil */ 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 + * @sample kotlinx.datetime.test.samples.LocalDateSamples.yearsUntil */ 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. * * The returned date is later than this date. * @@ -327,7 +436,9 @@ 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. * * The returned date is earlier than this date. * @@ -337,44 +448,56 @@ 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. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plus */ 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. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ 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. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.plus */ 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. * + * The value is rounded toward zero. + * * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.minus */ 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 6fa4e768f..dad1a2c28 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -5,24 +5,105 @@ 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. - * 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 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]. * - * The main purpose of this class is to provide human-readable representations of [Instant] values, or to transfer them - * as data. + * 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. * - * 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 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 + * 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 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. + * + * ``` + * 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 + * ``` + * + * ### 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 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 + * + * **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 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. + * + * [LocalDateTime] can be constructed directly from its components, [LocalDate] and [LocalTime], using the constructor. + * See sample 1. + * + * 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 + * ISO 8601 extended format (for example, `2023-01-02T22:35:01`). + * See sample 3. + * + * [parse] and [LocalDateTime.format] both support custom formats created with [Format] or defined in [Formats]. + * See sample 4. + * + * 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. + * @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 { @@ -35,9 +116,14 @@ 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. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoDateTimeFormat()): LocalDateTime @@ -64,6 +150,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 @@ -94,6 +182,11 @@ public expect class LocalDateTime : Comparable { * Fractional parts of the second are included if non-zero. * * 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 } @@ -104,7 +197,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 +206,10 @@ 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]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunctionWithMonthNumber */ public constructor( year: Int, @@ -130,7 +225,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 +234,10 @@ 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]. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.constructorFunction */ public constructor( year: Int, @@ -154,43 +251,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-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 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 /** @@ -198,8 +345,19 @@ 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` + * ``` + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.compareToSample */ - // TODO: add a note about pitfalls of comparing localdatetimes falling in the Autumn transition public override operator fun compareTo(other: LocalDateTime): Int /** @@ -220,6 +378,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 } @@ -227,6 +386,10 @@ 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. + * + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.formatting */ public fun LocalDateTime.format(format: DateTimeFormat): String = format.format(this) @@ -236,5 +399,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/LocalTime.kt b/core/common/src/LocalTime.kt index 900bc61a1..79c61793d 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -8,22 +8,73 @@ 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. + * + * ### Platform specifics + * + * On the JVM, + * 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 + * + * [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. + * 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. See sample 3. + * + * [parse] and [LocalTime.format] both support custom formats created with [Format] or defined in [Formats]. + * 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 { @@ -34,11 +85,15 @@ 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. * * @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 @@ -49,9 +104,16 @@ 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 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. + * * @see LocalTime.toSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.fromNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToSecondOfDay */ public fun fromSecondOfDay(secondOfDay: Int): LocalTime @@ -63,9 +125,16 @@ 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 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. + * * @see LocalTime.fromSecondOfDay * @see LocalTime.toMillisecondOfDay * @see LocalTime.fromNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToMillisecondOfDay */ public fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime @@ -76,9 +145,16 @@ 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 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. + * * @see LocalTime.fromSecondOfDay * @see LocalTime.fromMillisecondOfDay * @see LocalTime.toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToNanosecondOfDay */ public fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime @@ -99,6 +175,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 @@ -125,6 +202,16 @@ public expect class LocalTime : Comparable { * Fractional parts of the second are included if non-zero. * * 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 } @@ -139,28 +226,84 @@ 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 time component of this time value. */ + /** + * 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 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 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 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 - /** 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 + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toSecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToSecondOfDay + */ 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 + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toMillisecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToMillisecondOfDay + */ 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 + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.toNanosecondOfDay + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.fromAndToNanosecondOfDay + */ public fun toNanosecondOfDay(): Long /** @@ -172,11 +315,13 @@ 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 /** - * 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 @@ -192,6 +337,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 } @@ -199,6 +345,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) @@ -210,18 +358,33 @@ 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. + * + * @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) /** * 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. + * + * @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) /** * 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. + * + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.atDate */ 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..ae3b8177a 100644 --- a/core/common/src/Month.kt +++ b/core/common/src/Month.kt @@ -7,6 +7,12 @@ 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. + * + * @sample kotlinx.datetime.test.samples.MonthSamples.usage */ public expect enum class Month { /** January, month #01, with 31 days. */ @@ -44,17 +50,20 @@ 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 } /** * 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 /** * 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 c0af7d863..69b0de71c 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -14,6 +14,23 @@ 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 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. + * + * 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 */ @Serializable(with = TimeZoneSerializer::class) public expect open class TimeZone { @@ -21,21 +38,41 @@ 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 - // TODO: Declare and document toString/equals/hashCode + /** + * 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 public companion object { /** * 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. + * + * The [id] of this time zone is `"UTC"`. + * + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.utc */ public val UTC: FixedOffsetTimeZone @@ -50,13 +87,19 @@ 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. + * @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 } @@ -70,35 +113,56 @@ public expect open class TimeZone { * @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. * - * 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. + * 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.toInstantWithTwoReceivers */ public fun LocalDateTime.toInstant(): Instant } /** * 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. + * + * 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 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 */ @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 @@ -106,14 +170,19 @@ 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.offsetAt */ public expect fun TimeZone.offsetAt(instant: Instant): UtcOffset @@ -126,22 +195,33 @@ 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 /** * 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.instantToLocalDateTimeInOffset */ 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.offsetIn */ public fun Instant.offsetIn(timeZone: TimeZone): UtcOffset = timeZone.offsetAt(this) @@ -149,16 +229,17 @@ 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 + * @sample kotlinx.datetime.test.samples.TimeZoneSamples.localDateTimeToInstantInZone */ public expect fun LocalDateTime.toInstant(timeZone: TimeZone): Instant @@ -166,6 +247,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 @@ -173,11 +255,13 @@ 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 * `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 630ee1553..5c5bd7fdb 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -18,6 +18,40 @@ 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. + * + * ### Platform specifics + * + * On the JVM, there are `UtcOffset.toJavaZoneOffset()` and `java.time.ZoneOffset.toKotlinUtcOffset()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * + * ### Construction, serialization, and deserialization + * + * 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. + * + * [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`). + * See sample 2. + * + * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. + * 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 { @@ -28,7 +62,12 @@ 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]. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.equalsSample + */ + public override fun equals(other: Any?): Boolean public companion object { /** @@ -42,26 +81,17 @@ 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. + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.parsing */ - 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. * - * 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. @@ -69,6 +99,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 @@ -97,6 +128,10 @@ public expect class UtcOffset { * - `-02:00`, minus two hours; * - `-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 @@ -113,7 +148,10 @@ 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 */ public val ISO_BASIC: DateTimeFormat @@ -129,16 +167,19 @@ public expect class UtcOffset { * - `+1036` * * @see UtcOffset.Formats.ISO_BASIC + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.Formats.fourDigits */ public val FOUR_DIGITS: DateTimeFormat } /** - * 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. * @see UtcOffset.format for formatting using a custom format. + * + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.toStringSample */ public override fun toString(): String } @@ -146,6 +187,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) @@ -153,16 +196,19 @@ 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. * @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 @@ -171,8 +217,15 @@ public fun UtcOffset(): UtcOffset = UtcOffset.ZERO /** * Returns the fixed-offset time zone with the given UTC 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 */ 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/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 1266c031c..a3fb80945 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 { @@ -92,7 +67,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. @@ -111,6 +86,19 @@ 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, 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") + * 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) + * ``` + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.Formats.iso */ public val ISO_DATE_TIME_OFFSET: DateTimeFormat = Format { date(ISO_DATE) @@ -144,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({ @@ -183,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) @@ -193,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) @@ -205,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) @@ -217,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) @@ -233,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( @@ -250,24 +251,37 @@ 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. + * + * 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) } @@ -278,10 +292,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) { @@ -293,6 +311,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) @@ -300,30 +319,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 @@ -334,28 +358,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 /** @@ -368,6 +401,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() @@ -382,6 +416,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() @@ -396,6 +431,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() @@ -418,6 +454,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()) @@ -431,6 +468,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() @@ -473,6 +511,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() }) @@ -485,6 +524,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/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index f7117cccb..933360535 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -11,15 +11,26 @@ 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 { /** - * 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 */ 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 +38,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 +46,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 +57,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 +68,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. + * 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. + * 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. + * Pad with spaces during formatting. During parsing, padding is required; otherwise, 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..3c92bfbfd 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) @@ -32,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) @@ -49,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) @@ -56,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) @@ -73,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) } @@ -105,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) @@ -121,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) @@ -133,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) @@ -140,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) @@ -149,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) @@ -168,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) @@ -188,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) @@ -196,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) } @@ -211,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) } @@ -229,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) @@ -238,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) @@ -247,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) } @@ -271,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) } @@ -332,6 +340,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 +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] to parse some fields optionally without introducing a particular formatting behavior. * * Example: * ``` @@ -368,6 +379,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 +395,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..c94a24040 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]. + * 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. + * + * @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,42 @@ 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 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]. + * 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. + * + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.usage + * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.DayOfWeekNamesSamples.constructionFromList */ 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 +134,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 +151,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 +162,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 +171,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..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` | @@ -100,6 +102,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.UnicodeSamples.byUnicodePattern */ @FormatStringsInDatetimeFormats public fun DateTimeFormatBuilder.byUnicodePattern(pattern: 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 ad41ba1b4..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 ISO-8601 representation. + * A serializer for [UtcOffset] that uses the extended ISO 8601 representation. * * JSON example: `"+02:00"` * 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 +} diff --git a/core/common/test/samples/ClockSamples.kt b/core/common/test/samples/ClockSamples.kt new file mode 100644 index 000000000..83a3a1563 --- /dev/null +++ b/core/common/test/samples/ClockSamples.kt @@ -0,0 +1,48 @@ +/* + * 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() { + // Getting the current date and time + 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 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 + val clock = object : Clock { + 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/DateTimePeriodSamples.kt b/core/common/test/samples/DateTimePeriodSamples.kt new file mode 100644 index 000000000..bd308bc48 --- /dev/null +++ b/core/common/test/samples/DateTimePeriodSamples.kt @@ -0,0 +1,148 @@ +/* + * 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() { + // 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 + 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() { + // Parsing and formatting a DateTimePeriod + val string = "P-2M-3DT-4H60M" + val period = DateTimePeriod.parse(string) + check(period.toString() == "-P2M3DT3H") + } + + @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 + ) + // 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() { + // 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") + } + + @Test + fun parsing() { + // Parsing a string representation of a DateTimePeriod + with(DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S")) { + check(years == 1) + check(months == 2) + check(days == 3) + check(hours == 4) + check(minutes == 5) + check(seconds == 6) + check(nanoseconds == 7) + } + with(DateTimePeriod.parse("P14M-16DT5H")) { + check(years == 1) + check(months == 2) + check(days == -16) + check(hours == 5) + } + with(DateTimePeriod.parse("-P2M16DT5H")) { + check(years == 0) + check(months == -2) + check(days == -16) + check(hours == -5) + } + } + + @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 + 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() { + // 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)) + } +} + +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 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)) + } +} diff --git a/core/common/test/samples/DateTimeUnitSamples.kt b/core/common/test/samples/DateTimeUnitSamples.kt new file mode 100644 index 000000000..bea41aa58 --- /dev/null +++ b/core/common/test/samples/DateTimeUnitSamples.kt @@ -0,0 +1,57 @@ +/* + * 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() { + // 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)) + check(DateTimeUnit.CENTURY == DateTimeUnit.MonthBased(months = 12 * 100)) + } + + @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) + check(halfDay == DateTimeUnit.HOUR * 12) + check(halfDay == DateTimeUnit.MINUTE * 720) + check(halfDay == DateTimeUnit.SECOND * 43_200) + } + + @Test + fun dayBasedUnit() { + // Constructing various day-based measurement units + val iteration = DateTimeUnit.DayBased(days = 14) + check(iteration.days == 14) + check(iteration == DateTimeUnit.DAY * 14) + check(iteration == DateTimeUnit.WEEK * 2) + } + + @Test + fun monthBasedUnit() { + // Constructing various month-based measurement units + 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..c8393ae98 --- /dev/null +++ b/core/common/test/samples/DayOfWeekSamples.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.* + +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) + 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() { + // Getting the ISO day-of-week number + check(DayOfWeek.MONDAY.isoDayNumber == 1) + check(DayOfWeek.TUESDAY.isoDayNumber == 2) + // ... + check(DayOfWeek.SUNDAY.isoDayNumber == 7) + } + + @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) + // ... + check(DayOfWeek(isoDayNumber = 7) == DayOfWeek.SUNDAY) + try { + DayOfWeek(0) + fail("Expected IllegalArgumentException") + } catch (e: IllegalArgumentException) { + // Expected + } + } +} diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt new file mode 100644 index 000000000..8fec354b7 --- /dev/null +++ b/core/common/test/samples/InstantSamples.kt @@ -0,0 +1,328 @@ +/* + * 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() { + // 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) + 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() { + // 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) + 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() { + // 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) + .toEpochMilliseconds() == 1_000_000_000_123L) + } + + @Test + fun plusDuration() { + // Finding a moment that's later than the starting point by the given amount of real time + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) + val fiveHoursLater = instant + 5.hours + 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.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) + val fiveHoursEarlier = instant - 5.hours + check(fiveHoursEarlier.epochSeconds == 2 * 60 * 60L) + check(fiveHoursEarlier.nanosecondsOfSecond == 123_456_789) + } + + @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()) + ) + repeat(100) { + val instant1 = randomInstant() + val instant2 = randomInstant() + // in the UTC time zone, earlier instants are represented as earlier datetimes + check((instant1 < instant2) == + (instant1.toLocalDateTime(TimeZone.UTC) < instant2.toLocalDateTime(TimeZone.UTC))) + } + } + + @Test + fun toStringSample() { + // Converting an Instant to a string + check(Instant.fromEpochSeconds(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")) + } + + @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")) + } + + @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.fromEpochSeconds(0)) + check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochSeconds(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) + check(Instant.DISTANT_PAST.isDistantPast) + } + + @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) + check(Instant.DISTANT_FUTURE.isDistantFuture) + } + + @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")) + 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() { + // 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")) + 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() { + // 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 + 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() { + // 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 + 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() { + // Finding the difference between two instants in terms of the given measurement unit + 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) + } + + @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 + 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() { + // 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 + 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() { + // 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 + 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() { + // 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 + 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() { + // 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")) + val twoYearsLaterInSydney = startInstant.plus(2, DateTimeUnit.YEAR, TimeZone.of("Australia/Sydney")) + check(twoYearsLaterInSydney == Instant.parse("2026-04-05T23:51:45.586Z")) + } + + @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")) + val twoYearsEarlierInCairo = startInstant.minus(2, DateTimeUnit.YEAR, TimeZone.of("Africa/Cairo")) + check(twoYearsEarlierInCairo == Instant.parse("2022-03-28T02:04:56.256Z")) + } + + @Test + fun plusTimeBasedUnit() { + // Finding a moment that's later than the starting point by the given amount of real time + val instant = Instant.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) + val fiveHoursLater = instant.plus(5, DateTimeUnit.HOUR) + 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.fromEpochSeconds(7 * 60 * 60, nanosecondAdjustment = 123_456_789) + val fiveHoursEarlier = instant.minus(5, DateTimeUnit.HOUR) + check(fiveHoursEarlier.epochSeconds == 2 * 60 * 60L) + check(fiveHoursEarlier.nanosecondsOfSecond == 123_456_789) + } + + /** 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 + 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() { + // Finding a moment that's earlier than the starting point by a given amount of real time + 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) + } + + @Test + fun formatting() { + // Formatting an Instant to a string using predefined and custom formats + 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 { + 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 new file mode 100644 index 000000000..194af1f8e --- /dev/null +++ b/core/common/test/samples/LocalDateSamples.kt @@ -0,0 +1,285 @@ +/* + * 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() { + // 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() + } + check(LocalDate.parse("Apr 16, 2024", customFormat) == LocalDate(2024, Month.APRIL, 16)) + } + + @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) + check(randomDate.toEpochDays() == randomEpochDay) + } + + @Test + fun customFormat() { + // Parsing and formatting LocalDate values using a custom format + 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() { + // Constructing a LocalDate value using its constructor + 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() { + // Constructing a LocalDate value using its constructor + val date = LocalDate(2024, Month.APRIL, 16) + check(date.year == 2024) + check(date.month == Month.APRIL) + check(date.dayOfMonth == 16) + } + + @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) + } + + @Test + fun month() { + // Getting the month + for (month in Month.entries) { + check(LocalDate(2024, month, 16).month == month) + } + } + + @Test + fun dayOfMonth() { + // Getting the day of the month + for (dayOfMonth in 1..30) { + check(LocalDate(2024, Month.APRIL, dayOfMonth).dayOfMonth == dayOfMonth) + } + } + + @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) + } + + @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) + } + + @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) + } + + @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)) + check(LocalDate(-1000, 4, 16) < LocalDate(0, 4, 17)) + } + + @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") + } + + @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() + } + check(LocalDate(2024, 4, 16).format(customFormat) == "Apr 16, 2024") + } + + @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)) + } + + @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) + check(dateTime == LocalDateTime(2024, Month.APRIL, 16, 13, 30)) + } + + @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: + // 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() { + // 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: + // 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() { + // 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) + check(period == DatePeriod(years = 1, months = 2, days = 30)) + } + + @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 + check(period == DatePeriod(years = 1, months = 2, days = 30)) + } + + @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) + check(differenceInMonths == 14) + // one year, two months, and 30 days, rounded toward zero. + } + + @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) + check(daysUntilConcert == 163) + } + + @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) + check(ageInMonths == 4) + } + + @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) + check(age == 8) + } + + @Test + 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) + check(tenDaysLater == LocalDate(2024, Month.APRIL, 26)) + val twoMonthsLater = today.plus(2, DateTimeUnit.MONTH) + check(twoMonthsLater == LocalDate(2024, Month.JUNE, 16)) + } + + @Test + 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) + check(tenDaysAgo == LocalDate(2024, Month.APRIL, 6)) + val twoMonthsAgo = today.minus(2, DateTimeUnit.MONTH) + check(twoMonthsAgo == LocalDate(2024, Month.FEBRUARY, 16)) + } + + 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) + check(formatted == "2024-04-16") + } + + @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) + check(formatted == "20240416") + } + } +} diff --git a/core/common/test/samples/LocalDateTimeSamples.kt b/core/common/test/samples/LocalDateTimeSamples.kt new file mode 100644 index 000000000..7b8c7ae90 --- /dev/null +++ b/core/common/test/samples/LocalDateTimeSamples.kt @@ -0,0 +1,227 @@ +/* + * 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() { + // 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, + hour = 2, minute = 16, second = 20, nanosecond = 0 + ) + check(dateTime1 == dateTime2) + } + + @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() + check(formatted == "2024-02-15T08:30:15.123456700") + } + + @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") == + 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() { + // Parsing and formatting LocalDateTime values using a custom format + 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(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, + 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() { + // Constructing a LocalDateTime value using its constructor + 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() { + // 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) + check(dateTime.date == date) + check(dateTime.time == time) + check(dateTime == date.atTime(time)) + check(dateTime == time.atDate(date)) + } + + @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) + 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() { + // 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) + 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() { + // 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) + check(dateTime.date == date) + check(dateTime.time == time) + } + + @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)) + 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() { + // 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") + } + + @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) + 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() { + // 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) + .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") + } + } +} diff --git a/core/common/test/samples/LocalTimeSamples.kt b/core/common/test/samples/LocalTimeSamples.kt new file mode 100644 index 000000000..df75f6ce2 --- /dev/null +++ b/core/common/test/samples/LocalTimeSamples.kt @@ -0,0 +1,297 @@ +/* + * 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() { + // 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) + 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() { + // 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() + 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() { + // 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() + check(formatted == "08:30:15.123456700") + } + + @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)) + 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() { + // 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) + check(time.toSecondOfDay() == randomNumberOfSeconds) + check(time.nanosecond == 0) // sub-second part is zero + } + + @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) + check(time.toMillisecondOfDay() == randomNumberOfMilliseconds) + check(time.nanosecond % 1_000_000 == 0) // sub-millisecond part is zero + } + + @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), + 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() { + // Parsing and formatting LocalTime values using a custom format + 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() { + // Constructing a LocalTime using its constructor + 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() { + // 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) + } + + @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) + 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() { + // 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) + 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() { + // 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) + 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() { + // 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)) + check(LocalTime(hour = 8, minute = 30) < LocalTime(hour = 8, minute = 30, second = 0, nanosecond = 1)) + } + + @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") + } + + @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() + 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() { + // 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) + } + check(firstMorningOfEveryMonth.all { it.time == morning && it.dayOfMonth == 1 }) + } + + /** + * @see atDateComponentWiseMonthNumber + */ + @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) + } + check(firstMorningOfEveryMonth.all { it.time == morning && it.dayOfMonth == 1 }) + } + + @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) + 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() { + // 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) + 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/MonthSamples.kt b/core/common/test/samples/MonthSamples.kt new file mode 100644 index 000000000..70128845a --- /dev/null +++ b/core/common/test/samples/MonthSamples.kt @@ -0,0 +1,57 @@ +/* + * 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() { + // 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) + 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() { + // Getting the number of a month + check(Month.JANUARY.number == 1) + check(Month.FEBRUARY.number == 2) + // ... + check(Month.DECEMBER.number == 12) + } + + @Test + fun constructorFunction() { + // Constructing a Month using the constructor function + 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..71ef13fb6 --- /dev/null +++ b/core/common/test/samples/TimeZoneSamples.kt @@ -0,0 +1,251 @@ +/* + * 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() { + // 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) + 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() { + // 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) + } + + @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 + 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() { + // 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) + 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" + } + // Outputs a text like `[2024-06-02 08:30:02.515+0200] Starting the application` + logEntry("Starting the application") + } + + @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")) + 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() { + // 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" + check(zone.id == zoneId || zone is FixedOffsetTimeZone) + } + } + + /** + * @see instantToLocalDateTime + */ + @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) { + instant.toLocalDateTime() + } + check(localDateTime == LocalDate(2023, 6, 2).atTime(8, 30)) + } + + /** + * @see localDateTimeToInstantInZone + */ + @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) { + localDateTime.toInstant() + } + check(instant == Instant.parse("2023-06-02T16:30:00Z")) + } + + /** + * @see offsetIn + */ + @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) + check(offset == UtcOffset(hours = -4)) + } + + @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) + check(localDateTime == LocalDate(2023, 6, 2).atTime(8, 30)) + } + + @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) + check(localDateTime == LocalDate(2023, 6, 2).atTime(14, 0)) + } + + /** + * @see offsetAt + */ + @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) + check(offset == UtcOffset(hours = -4)) + } + + /** + * @see toInstantWithTwoReceivers + */ + @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) + check(instant == Instant.parse("2023-06-02T16:30:00Z")) + } + + @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) + check(instant == Instant.parse("2023-06-02T11:00:00Z")) + } + + @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) + 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() { + // 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", + "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() { + // 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") + check(zone.offset == offset) + } + + @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 new file mode 100644 index 000000000..ec651c71d --- /dev/null +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -0,0 +1,140 @@ +/* + * 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() { + // 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() + optional { char(':'); offsetSecondsOfMinute() } + } + } + 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) + val offset3 = UtcOffset(hours = 1) + check(offset1 != offset3) + } + + @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() } + check(UtcOffset.parse("+130", customFormat).totalSeconds == 5400) + } + + @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") + } + + @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") + } + + @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 { + 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() { + // 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)) + } + } + + 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) + check(formatted == "+103622") + } + + @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) + check(formatted == "+10:36:22") + } + + @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) + val formattedOffsetWithSeconds = UtcOffset.Formats.FOUR_DIGITS.format(offsetWithSeconds) + check(formattedOffsetWithSeconds == "+1036") + } + } +} diff --git a/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt new file mode 100644 index 000000000..b91aeb448 --- /dev/null +++ b/core/common/test/samples/format/DateTimeComponentsFormatSamples.kt @@ -0,0 +1,48 @@ +/* + * 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() { + // Defining a custom format that includes a time zone ID + 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() { + // Using a predefined DateTimeComponents format in a larger format + 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/DateTimeComponentsSamples.kt b/core/common/test/samples/format/DateTimeComponentsSamples.kt new file mode 100644 index 000000000..b4c0148e8 --- /dev/null +++ b/core/common/test/samples/format/DateTimeComponentsSamples.kt @@ -0,0 +1,420 @@ +/* + * 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() { + // 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)) + check(components.toInstantUsingOffset() == Instant.parse("2020-03-16T20:59:59.999999999Z")) + check(components.toUtcOffset() == UtcOffset(3, 0)) + } + + @Test + fun parsingInvalidInput() { + // Parsing an invalid input and handling the error + 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() { + // 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), + UtcOffset(hours = 3) + ) + } + check(formatted == "Mon, 16 Mar 2020 23:59:59 +0300") + } + + @Test + fun customFormat() { + // Formatting and parsing a complex entity with a custom format + 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() { + // 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) + 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() { + // 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 { + setDateTimeOffset(instant, offset) + } + check(formatted == "Sun, 28 Mar 2021 02:16:20 +0300") + } + + @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 { + setDateTimeOffset(localDateTime, offset) + } + check(formatted == "Sun, 28 Mar 2021 02:16:20 +0300") + } + + @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(' ') + 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() { + // Formatting and parsing a date in complex scenarios + 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 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) + check(parsed.month == Month.JULY) + parsed.month = Month.JUNE + check(parsed.monthNumber == 6) + check(parsed.month == Month.JUNE) + } + + @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") + } + 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() { + // Formatting and parsing a time in complex scenarios + 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() { + // 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)) + 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() { + // Formatting and parsing a time zone ID as part of a complex format + 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() { + // 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() + check(offset == UtcOffset(3, 0)) + } + + @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() + check(localDate == LocalDate(1994, 11, 6)) + } + + @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() + check(localTime == LocalTime(8, 49, 37)) + } + + @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() + check(localDateTime == LocalDateTime(1994, 11, 6, 8, 49, 37)) + } + + @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() + 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() { + // 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) + 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() { + // 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) + 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() { + // 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)) + check(parsed.toLocalTime() == LocalTime(11, 5, 30)) + check(parsed.toUtcOffset() == UtcOffset(-3, 0)) + } + + @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 { + setDateTimeOffset(today, offset) + } + check(formatted == "Mon, 15 Apr 2024 14:01:01 +0200") + } + + @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)) + 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/DateTimeFormatBuilderSamples.kt b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt new file mode 100644 index 000000000..c660c78c8 --- /dev/null +++ b/core/common/test/samples/format/DateTimeFormatBuilderSamples.kt @@ -0,0 +1,103 @@ +/* + * 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() { + // Defining a custom format that includes verbatim strings + val format = LocalDate.Format { + monthNumber() + char('/') + dayOfMonth() + chars(", ") + year() + } + check(LocalDate(2020, 1, 13).format(format) == "01/13, 2020") + } + + @Test + fun alternativeParsing() { + // Defining a custom format that allows parsing one of several alternatives + 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() { + // Defining a custom format that includes parts that will be omitted if they are zero + 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() { + // Defining a custom format that includes a verbatim character + 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..aabd28bbb --- /dev/null +++ b/core/common/test/samples/format/DateTimeFormatSamples.kt @@ -0,0 +1,150 @@ +/* + * 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() { + // 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)) + check(sb.toString() == "Today is 2024-04-05") + } + + @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") + 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() { + // 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) + 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() { + // 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 yyyy") + } + val customFormatAsKotlinCode = DateTimeFormat.formatAsKotlinBuilderDsl(customFormat) + check( + customFormatAsKotlinCode.contains(""" + monthNumber() + char('/') + dayOfMonth() + char(' ') + year() + """.trimIndent()) + ) + } + + class PaddingSamples { + @Test + fun usage() { + // Defining a custom format that uses various padding rules + 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() { + // 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('/') + 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() { + // Defining a custom format that removes padding requirements + 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() { + // Defining a custom format that uses spaces for padding + 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..5a6d476ce --- /dev/null +++ b/core/common/test/samples/format/LocalDateFormatSamples.kt @@ -0,0 +1,235 @@ +/* + * 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 { + + @Test + fun year() { + // Using the year number in a custom format + 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() { + // Using two-digit years in a custom format + 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() { + // Using month number with various paddings in a custom format + 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() { + // Using strings for month names in a custom format + 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() { + // Using day-of-month with various paddings in a custom format + 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() { + // 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() + } + 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() { + // 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') } + hour(); char(':'); minute() + } + check(format.format(LocalDateTime(2021, 1, 13, 14, 30)) == "2021-01-13T14:30") + } + + 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(' ') + dayOfMonth() + chars(", ") + year() + } + check(format.format(LocalDate(2021, 1, 13)) == "Jan 13, 2021") + } + + @Test + fun constructionFromStrings() { + // 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" + ) + check(myMonthNames == MonthNames.ENGLISH_ABBREVIATED) // could just use the built-in one... + } + + @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" + ) + // constructing by passing a list of 12 strings + val myMonthNamesFromList = MonthNames(germanMonthNames) + check(myMonthNamesFromList.names == germanMonthNames) + } + + @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" + )) + } + + @Test + fun englishFull() { + // Using the built-in English month names in a custom format + 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() { + // Using the built-in English abbreviated month names in a custom format + 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() { + // Using strings for day-of-week names in a custom format + 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 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" + ) + check(myMonthNames == DayOfWeekNames.ENGLISH_ABBREVIATED) // could just use the built-in one... + } + + @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" + ) + // constructing by passing a list of 7 strings + val myDayOfWeekNames = DayOfWeekNames(germanDayOfWeekNames) + check(myDayOfWeekNames.names == germanDayOfWeekNames) + } + + @Test + fun names() { + // Obtaining the list of day of week names + check(DayOfWeekNames.ENGLISH_ABBREVIATED.names == listOf( + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" + )) + } + + @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(", ") + dayOfWeek(DayOfWeekNames.ENGLISH_FULL) + } + check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wednesday") + } + + @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(", ") + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + } + check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wed") + } + } +} diff --git a/core/common/test/samples/format/LocalDateTimeFormatSamples.kt b/core/common/test/samples/format/LocalDateTimeFormatSamples.kt new file mode 100644 index 000000000..ee9cb01f0 --- /dev/null +++ b/core/common/test/samples/format/LocalDateTimeFormatSamples.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.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +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) + } + 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..8211601c2 --- /dev/null +++ b/core/common/test/samples/format/LocalTimeFormatSamples.kt @@ -0,0 +1,58 @@ +/* + * 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() { + // Defining a custom format for the local time + // 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() { + // 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") + } + val formatted = format.format(LocalTime(9, 34, 58, 120_000_000)) + check(formatted == "09:34:58 AM") + } + + @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) + } + val formatted = format.format(LocalTime(9, 34, 58, 120_000_000)) + check(formatted == "09:34:58.120") + } + + @Test + fun time() { + // Using a predefined format for the local 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/UnicodeSamples.kt b/core/common/test/samples/format/UnicodeSamples.kt new file mode 100644 index 000000000..26d9a16f0 --- /dev/null +++ b/core/common/test/samples/format/UnicodeSamples.kt @@ -0,0 +1,31 @@ +/* + * 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() { + // 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") + } + check(customFormat.format(LocalDate(2021, 1, 13)) == "01/13 2021") + check( + DateTimeFormat.formatAsKotlinBuilderDsl(customFormat) == """ + monthNumber() + char('/') + dayOfMonth() + char(' ') + year() + """.trimIndent() + ) + } +} diff --git a/core/common/test/samples/format/UtcOffsetFormatSamples.kt b/core/common/test/samples/format/UtcOffsetFormatSamples.kt new file mode 100644 index 000000000..f6f51ba76 --- /dev/null +++ b/core/common/test/samples/format/UtcOffsetFormatSamples.kt @@ -0,0 +1,45 @@ +/* + * 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() { + // Defining a custom format for the UTC offset + 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() { + // Using a predefined format for the UTC 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") + } +} 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 { 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()) } }