Skip to content

Commit daec006

Browse files
committed
Samples for Date(Time)Period and Clock
1 parent 31efc51 commit daec006

File tree

4 files changed

+205
-28
lines changed

4 files changed

+205
-28
lines changed

core/common/src/Clock.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public interface Clock {
2222
* Returns the [Instant] corresponding to the current time, according to this clock.
2323
*
2424
* It is not guaranteed that calling [now] later will return a larger [Instant].
25-
* In particular, for [System], violations of this are completely expected and must be taken into account.
25+
* In particular, for [System] it is completely expected that the opposite will happen,
26+
* and it must be taken into account.
2627
* See the documentation of [System] for details.
2728
*
2829
* Even though [Instant] is defined to be on the UTC-SLS time scale, which enforces a specific way of handling
@@ -46,6 +47,8 @@ public interface Clock {
4647
*
4748
* For improved testability, one could avoid using [Clock.System] directly in the implementation,
4849
* instead passing a [Clock] explicitly.
50+
*
51+
* @sample kotlinx.datetime.test.samples.ClockSamples.system
4952
*/
5053
public object System : Clock {
5154
override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now()
@@ -59,14 +62,9 @@ public interface Clock {
5962
/**
6063
* Returns the current date at the given [time zone][timeZone], according to [this Clock][this].
6164
*
62-
* The time zone is important because the current date is not the same in all time zones at the same time.
63-
* ```
64-
* val clock = object : Clock {
65-
* override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z")
66-
* }
67-
* val dateInUTC = clock.todayIn(TimeZone.UTC) // 2020-01-01
68-
* val dateInNewYork = clock.todayIn(TimeZone.of("America/New_York")) // 2019-12-31
69-
* ```
65+
* The time zone is important because the current date is not the same in all time zones at the same instant.
66+
*
67+
* @sample kotlinx.datetime.test.samples.ClockSamples.todayIn
7068
*/
7169
public fun Clock.todayIn(timeZone: TimeZone): LocalDate =
7270
now().toLocalDateTime(timeZone).date

core/common/src/DateTimePeriod.kt

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,20 @@ import kotlinx.serialization.Serializable
5252
* will be returned if all time components happen to be zero.
5353
*
5454
* A `DateTimePeriod` can be constructed using the constructor function with the same name.
55-
*
56-
* ```
57-
* val dateTimePeriod = DateTimePeriod(months = 24, days = -3)
58-
* val datePeriod = dateTimePeriod as DatePeriod // the same as DatePeriod(years = 2, days = -3)
59-
* ```
55+
* See sample 1.
6056
*
6157
* [parse] and [toString] methods can be used to obtain a [DateTimePeriod] from and convert it to a string in the
6258
* ISO 8601 extended format.
63-
*
64-
* ```
65-
* val dateTimePeriod = DateTimePeriod.parse("P1Y2M6DT13H1S") // 1 year, 2 months, 6 days, 13 hours, 1 second
66-
* val string = dateTimePeriod.toString() // "P1Y2M6DT13H1S"
67-
* ```
59+
* See sample 2.
6860
*
6961
* `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]).
7062
*
7163
* Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]:
7264
* - [DateTimePeriodIso8601Serializer] for the ISO 8601 format;
7365
* - [DateTimePeriodComponentSerializer] for an object with components.
66+
*
67+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.construction
68+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.simpleParsingAndFormatting
7469
*/
7570
@Serializable(with = DateTimePeriodIso8601Serializer::class)
7671
// TODO: could be error-prone without explicitly named params
@@ -82,40 +77,54 @@ public sealed class DateTimePeriod {
8277
*
8378
* Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details.
8479
* Also, this field does not overflow into months, so values larger than 31 can be present.
80+
*
81+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
8582
*/
8683
public abstract val days: Int
8784
internal abstract val totalNanoseconds: Long
8885

8986
/**
9087
* The number of whole years. Can be negative.
88+
*
89+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.valueNormalization
9190
*/
9291
public val years: Int get() = totalMonths / 12
9392

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

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

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

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

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

@@ -136,6 +145,7 @@ public sealed class DateTimePeriod {
136145
* minus four seconds, minus 123456789 nanoseconds;
137146
*
138147
* @see DateTimePeriod.parse for the detailed description of the format.
148+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.toStringSample
139149
*/
140150
override fun toString(): String = buildString {
141151
val sign = if (allNonpositive()) { append('-'); -1 } else 1
@@ -215,6 +225,7 @@ public sealed class DateTimePeriod {
215225
*
216226
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are
217227
* exceeded.
228+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parsing
218229
*/
219230
public fun parse(text: String): DateTimePeriod {
220231
fun parseException(message: String, position: Int): Nothing =
@@ -400,14 +411,10 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this
400411
* are not zero, and [DatePeriodIso8601Serializer] and [DatePeriodComponentSerializer], mirroring those of
401412
* [DateTimePeriod].
402413
*
403-
* ```
404-
* val datePeriod1 = DatePeriod(years = 1, days = 3)
405-
* val string = datePeriod1.toString() // "P1Y3D"
406-
* val datePeriod2 = DatePeriod.parse(string) // 1 year and 3 days
407-
* ```
408-
*
409414
* `DatePeriod` values are used in operations on [LocalDates][LocalDate] and are returned from operations
410415
* on [LocalDates][LocalDate], but they also can be passed anywhere a [DateTimePeriod] is expected.
416+
*
417+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.simpleParsingAndFormatting
411418
*/
412419
@Serializable(with = DatePeriodIso8601Serializer::class)
413420
public class DatePeriod internal constructor(
@@ -428,6 +435,7 @@ public class DatePeriod internal constructor(
428435
* For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
429436
*
430437
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
438+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.construction
431439
*/
432440
public constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days)
433441
// avoiding excessive computations
@@ -455,6 +463,7 @@ public class DatePeriod internal constructor(
455463
* or any time components are not zero.
456464
*
457465
* @see DateTimePeriod.parse
466+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.DatePeriodSamples.parsing
458467
*/
459468
public fun parse(text: String): DatePeriod =
460469
when (val period = DateTimePeriod.parse(text)) {
@@ -521,6 +530,7 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos
521530
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
522531
* @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds]
523532
* overflows a [Long].
533+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.constructorFunction
524534
*/
525535
public fun DateTimePeriod(
526536
years: Int = 0,
@@ -544,16 +554,17 @@ public fun DateTimePeriod(
544554
* whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours.
545555
* See [DateTimeUnit.DayBased] for details.
546556
*
547-
* ```
548-
* 2.days.toDateTimePeriod() // 0 days, 48 hours
549-
* ```
557+
* @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.durationToDateTimePeriod
550558
*/
551559
// TODO: maybe it's more consistent to throw here on overflow?
552560
public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds)
553561

554562
/**
555563
* Adds two [DateTimePeriod] instances.
556564
*
565+
* **Pitfall**: given three instants, adding together the periods between the first and the second and between the
566+
* second and the third *does not* necessarily equal the period between the first and the third.
567+
*
557568
* @throws DateTimeArithmeticException if arithmetic overflow happens.
558569
*/
559570
public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = buildDateTimePeriod(
@@ -565,6 +576,9 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod =
565576
/**
566577
* Adds two [DatePeriod] instances.
567578
*
579+
* **Pitfall**: given three dates, adding together the periods between the first and the second and between the
580+
* second and the third *does not* necessarily equal the period between the first and the third.
581+
*
568582
* @throws DateTimeArithmeticException if arithmetic overflow happens.
569583
*/
570584
public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod(
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.test.samples
7+
8+
import kotlinx.datetime.*
9+
import kotlin.test.*
10+
11+
class ClockSamples {
12+
@Test
13+
fun system() {
14+
val zone = TimeZone.of("Europe/Berlin")
15+
val currentInstant = Clock.System.now()
16+
val currentLocalDateTime = currentInstant.toLocalDateTime(zone)
17+
currentLocalDateTime.toString() // show the current date and time, according to the OS
18+
}
19+
20+
@Test
21+
fun todayIn() {
22+
val clock = object : Clock {
23+
override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z")
24+
}
25+
check(clock.todayIn(TimeZone.UTC) == LocalDate(2020, 1, 1))
26+
check(clock.todayIn(TimeZone.of("America/New_York")) == LocalDate(2019, 12, 31))
27+
}
28+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.test.samples
7+
8+
import kotlinx.datetime.*
9+
import kotlin.test.*
10+
import kotlin.time.Duration.Companion.days
11+
import kotlin.time.Duration.Companion.minutes
12+
13+
class DateTimePeriodSamples {
14+
15+
@Test
16+
fun construction() {
17+
val period = DateTimePeriod(years = 5, months = 21, days = 36, seconds = 3601)
18+
check(period.years == 6) // 5 years + (21 months / 12)
19+
check(period.months == 9) // 21 months % 12
20+
check(period.days == 36)
21+
check(period.hours == 1) // 3601 seconds / 3600
22+
check(period.minutes == 0)
23+
check(period.seconds == 1)
24+
check(period.nanoseconds == 0)
25+
check(DateTimePeriod(months = -24) as DatePeriod == DatePeriod(years = -2))
26+
}
27+
28+
@Test
29+
fun simpleParsingAndFormatting() {
30+
val string = "-P2M-3DT-4H"
31+
val period = DateTimePeriod.parse(string)
32+
check(period.toString() == "P-2M3DT4H")
33+
}
34+
35+
@Test
36+
fun valueNormalization() {
37+
val period = DateTimePeriod(
38+
years = -12, months = 122, days = -1440,
39+
hours = 400, minutes = -80, seconds = 123, nanoseconds = -123456789
40+
)
41+
// years and months have the same sign and are normalized together:
42+
check(period.years == -1) // -12 years + (122 months % 12) + 1 year
43+
check(period.months == -10) // (122 months % 12) - 1 year
44+
// days are separate from months and are not normalized:
45+
check(period.days == -1440)
46+
// hours, minutes, seconds, and nanoseconds are normalized together and have the same sign:
47+
check(period.hours == 398) // 400 hours - 2 hours' worth of minutes
48+
check(period.minutes == 42) // -80 minutes + 2 hours' worth of minutes + 120 seconds
49+
check(period.seconds == 2) // 123 seconds - 2 minutes' worth of seconds - 1 second
50+
check(period.nanoseconds == 876543211) // -123456789 nanoseconds + 1 second
51+
}
52+
53+
@Test
54+
fun toStringSample() {
55+
check(DateTimePeriod(years = 1, months = 2, days = 3, hours = 4, minutes = 5, seconds = 6, nanoseconds = 7).toString() == "P1Y2M3DT4H5M6.000000007S")
56+
check(DateTimePeriod(months = 14, days = -16, hours = 5).toString() == "P1Y2M-16DT5H")
57+
check(DateTimePeriod(months = -2, days = -16, hours = -5).toString() == "-P2M16DT5H")
58+
}
59+
60+
@Test
61+
fun parsing() {
62+
DateTimePeriod.parse("P1Y2M3DT4H5M6.000000007S").apply {
63+
check(years == 1)
64+
check(months == 2)
65+
check(days == 3)
66+
check(hours == 4)
67+
check(minutes == 5)
68+
check(seconds == 6)
69+
check(nanoseconds == 7)
70+
}
71+
DateTimePeriod.parse("P14M-16DT5H").apply {
72+
check(years == 1)
73+
check(months == 2)
74+
check(days == -16)
75+
check(hours == 5)
76+
}
77+
DateTimePeriod.parse("-P2M16DT5H").apply {
78+
check(years == 0)
79+
check(months == -2)
80+
check(days == -16)
81+
check(hours == -5)
82+
}
83+
}
84+
85+
@Test
86+
fun constructorFunction() {
87+
val dateTimePeriod = DateTimePeriod(months = 16, days = -60, hours = 16, minutes = -61)
88+
check(dateTimePeriod.years == 1) // months overflowed to years
89+
check(dateTimePeriod.months == 4) // 16 months % 12
90+
check(dateTimePeriod.days == -60) // days are separate from months and are not normalized
91+
check(dateTimePeriod.hours == 14) // the negative minutes overflowed to hours
92+
check(dateTimePeriod.minutes == 59) // (-61 minutes) + (2 hours) * (60 minutes / hour)
93+
val datePeriod = DateTimePeriod(months = 15, days = 3, hours = 2, minutes = -120)
94+
check(datePeriod is DatePeriod) // the time components are zero
95+
}
96+
97+
@Test
98+
fun durationToDateTimePeriod() {
99+
check(130.minutes.toDateTimePeriod() == DateTimePeriod(minutes = 130))
100+
check(2.days.toDateTimePeriod() == DateTimePeriod(days = 0, hours = 48))
101+
}
102+
103+
class DatePeriodSamples {
104+
105+
@Test
106+
fun simpleParsingAndFormatting() {
107+
val datePeriod1 = DatePeriod(years = 1, days = 3)
108+
val string = datePeriod1.toString()
109+
check(string == "P1Y3D")
110+
val datePeriod2 = DatePeriod.parse(string)
111+
check(datePeriod1 == datePeriod2)
112+
}
113+
114+
@Test
115+
fun construction() {
116+
val datePeriod = DatePeriod(years = 1, months = 16, days = 60)
117+
check(datePeriod.years == 2) // 1 year + (16 months / 12)
118+
check(datePeriod.months == 4) // 16 months % 12
119+
check(datePeriod.days == 60)
120+
// the time components are always zero:
121+
check(datePeriod.hours == 0)
122+
check(datePeriod.minutes == 0)
123+
check(datePeriod.seconds == 0)
124+
check(datePeriod.nanoseconds == 0)
125+
}
126+
127+
@Test
128+
fun parsing() {
129+
// ISO duration strings are supported:
130+
val datePeriod = DatePeriod.parse("P1Y16M60D")
131+
check(datePeriod == DatePeriod(years = 2, months = 4, days = 60))
132+
// it's okay to have time components as long as they amount to zero in total:
133+
val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M")
134+
check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3))
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)