Skip to content

Commit

Permalink
Samples for Date(Time)Period and Clock
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Apr 16, 2024
1 parent 31efc51 commit daec006
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 28 deletions.
16 changes: 7 additions & 9 deletions core/common/src/Clock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public interface Clock {
* Returns the [Instant] corresponding to the current time, according to this clock.
*
* It is not guaranteed that calling [now] later will return a larger [Instant].
* In particular, for [System], violations of this are completely expected and must be taken into account.
* In particular, for [System] it is completely expected that the opposite will happen,
* and it must be taken into account.
* See the documentation of [System] for details.
*
* Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling
Expand All @@ -46,6 +47,8 @@ public interface Clock {
*
* For improved testability, one could avoid using [Clock.System] directly in the implementation,
* instead passing a [Clock] explicitly.
*
* @sample kotlinx.datetime.test.samples.ClockSamples.system
*/
public object System : Clock {
override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now()
Expand All @@ -59,14 +62,9 @@ public interface Clock {
/**
* Returns the current date at the given [time zone][timeZone], according to [this Clock][this].
*
* The time zone is important because the current date is not the same in all time zones at the same time.
* ```
* val clock = object : Clock {
* override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z")
* }
* val dateInUTC = clock.todayIn(TimeZone.UTC) // 2020-01-01
* val dateInNewYork = clock.todayIn(TimeZone.of("America/New_York")) // 2019-12-31
* ```
* The time zone is important because the current date is not the same in all time zones at the same instant.
*
* @sample kotlinx.datetime.test.samples.ClockSamples.todayIn
*/
public fun Clock.todayIn(timeZone: TimeZone): LocalDate =
now().toLocalDateTime(timeZone).date
Expand Down
52 changes: 33 additions & 19 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,20 @@ import kotlinx.serialization.Serializable
* will be returned if all time components happen to be zero.
*
* A `DateTimePeriod` can be constructed using the constructor function with the same name.
*
* ```
* val dateTimePeriod = DateTimePeriod(months = 24, days = -3)
* val datePeriod = dateTimePeriod as DatePeriod // the same as DatePeriod(years = 2, days = -3)
* ```
* See sample 1.
*
* [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the
* ISO 8601 extended format.
*
* ```
* val dateTimePeriod = DateTimePeriod.parse("P1Y2M6DT13H1S") // 1 year, 2 months, 6 days, 13 hours, 1 second
* val string = dateTimePeriod.toString() // "P1Y2M6DT13H1S"
* ```
* See sample 2.
*
* `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]).
*
* Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]:
* - [DateTimePeriodIso8601Serializer] for the ISO 8601 format;
* - [DateTimePeriodComponentSerializer] for an object with components.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.construction
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.simpleParsingAndFormatting
*/
@Serializable(with = DateTimePeriodIso8601Serializer::class)
// TODO: could be error-prone without explicitly named params
Expand All @@ -82,40 +77,54 @@ public sealed class DateTimePeriod {
*
* Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details.
* Also, this field does not overflow into months, so values larger than 31 can be present.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public abstract val days: Int
internal abstract val totalNanoseconds: Long

/**
* The number of whole years. Can be negative.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public val years: Int get() = totalMonths / 12

/**
* The number of months in this period that don't form a whole year, so this value is always in `(-11..11)`.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public val months: Int get() = totalMonths % 12

/**
* The number of whole hours in this period. Can be negative.
*
* This field does not overflow into days, so values larger than 23 or smaller than -23 can be present.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public open val hours: Int get() = (totalNanoseconds / 3_600_000_000_000).toInt()

/**
* The number of whole minutes in this period that don't form a whole hour, so this value is always in `(-59..59)`.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public open val minutes: Int get() = ((totalNanoseconds % 3_600_000_000_000) / 60_000_000_000).toInt()

/**
* The number of whole seconds in this period that don't form a whole minute, so this value is always in `(-59..59)`.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public open val seconds: Int get() = ((totalNanoseconds % 60_000_000_000) / NANOS_PER_ONE).toInt()

/**
* The number of whole nanoseconds in this period that don't form a whole second, so this value is always in
* `(-999_999_999..999_999_999)`.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
*/
public open val nanoseconds: Int get() = (totalNanoseconds % NANOS_PER_ONE).toInt()

Expand All @@ -136,6 +145,7 @@ public sealed class DateTimePeriod {
* minus four seconds, minus 123456789 nanoseconds;
*
* @see DateTimePeriod.parse for the detailed description of the format.
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.toStringSample
*/
override fun toString(): String = buildString {
val sign = if (allNonpositive()) { append('-'); -1 } else 1
Expand Down Expand Up @@ -215,6 +225,7 @@ public sealed class DateTimePeriod {
*
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are
* exceeded.
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parsing
*/
public fun parse(text: String): DateTimePeriod {
fun parseException(message: String, position: Int): Nothing =
Expand Down Expand Up @@ -400,14 +411,10 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this
* are not zero, and [DatePeriodIso8601Serializer] and [DatePeriodComponentSerializer], mirroring those of
* [DateTimePeriod].
*
* ```
* val datePeriod1 = DatePeriod(years = 1, days = 3)
* val string = datePeriod1.toString() // "P1Y3D"
* val datePeriod2 = DatePeriod.parse(string) // 1 year and 3 days
* ```
*
* `DatePeriod` values are used in operations on [LocalDates][LocalDate] and are returned from operations
* on [LocalDates][LocalDate], but they also can be passed anywhere a [DateTimePeriod] is expected.
*
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting
*/
@Serializable(with = DatePeriodIso8601Serializer::class)
public class DatePeriod internal constructor(
Expand All @@ -428,6 +435,7 @@ public class DatePeriod internal constructor(
* For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
*
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.construction
*/
public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days)
// avoiding excessive computations
Expand Down Expand Up @@ -455,6 +463,7 @@ public class DatePeriod internal constructor(
* or any time components are not zero.
*
* @see DateTimePeriod.parse
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.parsing
*/
public fun parse(text: String): DatePeriod =
when (val period = DateTimePeriod.parse(text)) {
Expand Down Expand Up @@ -521,6 +530,7 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
* @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds]
* overflows a [Long].
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.constructorFunction
*/
public fun DateTimePeriod(
years: Int = 0,
Expand All @@ -544,16 +554,17 @@ public fun DateTimePeriod(
* whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours.
* See [DateTimeUnit.DayBased] for details.
*
* ```
* 2.days.toDateTimePeriod() // 0 days, 48 hours
* ```
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.durationToDateTimePeriod
*/
// TODO: maybe it's more consistent to throw here on overflow?
public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds)

/**
* 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(
Expand All @@ -565,6 +576,9 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod =
/**
* Adds two [DatePeriod] instances.
*
* **Pitfall**: given three dates, adding together the periods between the first and the second and between the
* second and the third *does not* necessarily equal the period between the first and the third.
*
* @throws DateTimeArithmeticException if arithmetic overflow happens.
*/
public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod(
Expand Down
28 changes: 28 additions & 0 deletions core/common/test/samples/ClockSamples.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.test.samples

import kotlinx.datetime.*
import kotlin.test.*

class ClockSamples {
@Test
fun system() {
val zone = TimeZone.of("Europe/Berlin")
val currentInstant = Clock.System.now()
val currentLocalDateTime = currentInstant.toLocalDateTime(zone)
currentLocalDateTime.toString() // show the current date and time, according to the OS
}

@Test
fun todayIn() {
val clock = object : Clock {
override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z")
}
check(clock.todayIn(TimeZone.UTC) == LocalDate(2020, 1, 1))
check(clock.todayIn(TimeZone.of("America/New_York")) == LocalDate(2019, 12, 31))
}
}
137 changes: 137 additions & 0 deletions core/common/test/samples/DateTimePeriodSamples.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.test.samples

import kotlinx.datetime.*
import kotlin.test.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes

class DateTimePeriodSamples {

@Test
fun construction() {
val period = DateTimePeriod(years = 5, months = 21, days = 36, seconds = 3601)
check(period.years == 6) // 5 years + (21 months / 12)
check(period.months == 9) // 21 months % 12
check(period.days == 36)
check(period.hours == 1) // 3601 seconds / 3600
check(period.minutes == 0)
check(period.seconds == 1)
check(period.nanoseconds == 0)
check(DateTimePeriod(months = -24) as DatePeriod == DatePeriod(years = -2))
}

@Test
fun simpleParsingAndFormatting() {
val string = "-P2M-3DT-4H"
val period = DateTimePeriod.parse(string)
check(period.toString() == "P-2M3DT4H")
}

@Test
fun valueNormalization() {
val period = DateTimePeriod(
years = -12, months = 122, days = -1440,
hours = 400, minutes = -80, seconds = 123, nanoseconds = -123456789
)
// years and months have the same sign and are normalized together:
check(period.years == -1) // -12 years + (122 months % 12) + 1 year
check(period.months == -10) // (122 months % 12) - 1 year
// days are separate from months and are not normalized:
check(period.days == -1440)
// hours, minutes, seconds, and nanoseconds are normalized together and have the same sign:
check(period.hours == 398) // 400 hours - 2 hours' worth of minutes
check(period.minutes == 42) // -80 minutes + 2 hours' worth of minutes + 120 seconds
check(period.seconds == 2) // 123 seconds - 2 minutes' worth of seconds - 1 second
check(period.nanoseconds == 876543211) // -123456789 nanoseconds + 1 second
}

@Test
fun toStringSample() {
check(DateTimePeriod(years = 1, months = 2, days = 3, hours = 4, minutes = 5, seconds = 6, nanoseconds = 7).toString() == "P1Y2M3DT4H5M6.000000007S")
check(DateTimePeriod(months = 14, days = -16, hours = 5).toString() == "P1Y2M-16DT5H")
check(DateTimePeriod(months = -2, days = -16, hours = -5).toString() == "-P2M16DT5H")
}

@Test
fun parsing() {
DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S").apply {
check(years == 1)
check(months == 2)
check(days == 3)
check(hours == 4)
check(minutes == 5)
check(seconds == 6)
check(nanoseconds == 7)
}
DateTimePeriod.parse("P14M-16DT5H").apply {
check(years == 1)
check(months == 2)
check(days == -16)
check(hours == 5)
}
DateTimePeriod.parse("-P2M16DT5H").apply {
check(years == 0)
check(months == -2)
check(days == -16)
check(hours == -5)
}
}

@Test
fun constructorFunction() {
val dateTimePeriod = DateTimePeriod(months = 16, days = -60, hours = 16, minutes = -61)
check(dateTimePeriod.years == 1) // months overflowed to years
check(dateTimePeriod.months == 4) // 16 months % 12
check(dateTimePeriod.days == -60) // days are separate from months and are not normalized
check(dateTimePeriod.hours == 14) // the negative minutes overflowed to hours
check(dateTimePeriod.minutes == 59) // (-61 minutes) + (2 hours) * (60 minutes / hour)
val datePeriod = DateTimePeriod(months = 15, days = 3, hours = 2, minutes = -120)
check(datePeriod is DatePeriod) // the time components are zero
}

@Test
fun durationToDateTimePeriod() {
check(130.minutes.toDateTimePeriod() == DateTimePeriod(minutes = 130))
check(2.days.toDateTimePeriod() == DateTimePeriod(days = 0, hours = 48))
}

class DatePeriodSamples {

@Test
fun simpleParsingAndFormatting() {
val datePeriod1 = DatePeriod(years = 1, days = 3)
val string = datePeriod1.toString()
check(string == "P1Y3D")
val datePeriod2 = DatePeriod.parse(string)
check(datePeriod1 == datePeriod2)
}

@Test
fun construction() {
val datePeriod = DatePeriod(years = 1, months = 16, days = 60)
check(datePeriod.years == 2) // 1 year + (16 months / 12)
check(datePeriod.months == 4) // 16 months % 12
check(datePeriod.days == 60)
// the time components are always zero:
check(datePeriod.hours == 0)
check(datePeriod.minutes == 0)
check(datePeriod.seconds == 0)
check(datePeriod.nanoseconds == 0)
}

@Test
fun parsing() {
// ISO duration strings are supported:
val datePeriod = DatePeriod.parse("P1Y16M60D")
check(datePeriod == DatePeriod(years = 2, months = 4, days = 60))
// it's okay to have time components as long as they amount to zero in total:
val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M")
check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3))
}
}
}

0 comments on commit daec006

Please sign in to comment.