Skip to content

Commit 2b88030

Browse files
committed
Implement the first pack of fixes after the review
1 parent a971c2f commit 2b88030

File tree

4 files changed

+108
-12
lines changed

4 files changed

+108
-12
lines changed

core/common/src/Clock.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import kotlin.time.*
1111
* A source of [Instant] values.
1212
*
1313
* See [Clock.System][Clock.System] for the clock instance that queries the operating system.
14+
*
15+
* It is recommended not to use [Clock.System] directly in the implementation; instead, one could pass a
16+
* [Clock] explicitly to the functions or classes that need it.
17+
* This way, tests can be written deterministically by providing custom [Clock] implementations
18+
* to the system under test.
1419
*/
1520
public interface Clock {
1621
/**
@@ -23,7 +28,7 @@ public interface Clock {
2328
public fun now(): Instant
2429

2530
/**
26-
* The [Clock] instance that queries the operating system as its source of time knowledge.
31+
* The [Clock] instance that queries the platform-specific system clock as its source of time knowledge.
2732
*
2833
* Successive calls to [now] will not necessarily return increasing [Instant] values, and when they do,
2934
* these increases will not necessarily correspond to the elapsed time.
@@ -35,6 +40,9 @@ public interface Clock {
3540
*
3641
* When predictable intervals between successive measurements are needed, consider using
3742
* [TimeSource.Monotonic].
43+
*
44+
* For improved testability, one could avoid using [Clock.System] directly in the implementation,
45+
* instead passing a [Clock] explicitly.
3846
*/
3947
public object System : Clock {
4048
override fun now(): Instant = @Suppress("DEPRECATION_ERROR") Instant.now()
@@ -47,6 +55,15 @@ public interface Clock {
4755

4856
/**
4957
* Returns the current date at the given [time zone][timeZone], according to [this Clock][this].
58+
*
59+
* The time zone is important because the current date is not the same in all time zones at the same time.
60+
* ```
61+
* val clock = object : Clock {
62+
* override fun now(): Instant = Instant.parse("2020-01-01T12:00:00Z")
63+
* }
64+
* val dateInUTC = clock.todayIn(TimeZone.UTC) // 2020-01-01
65+
* val dateInNewYork = clock.todayIn(TimeZone.of("America/New_York")) // 2019-12-31
66+
* ```
5067
*/
5168
public fun Clock.todayIn(timeZone: TimeZone): LocalDate =
5269
now().toLocalDateTime(timeZone).date

core/common/src/DateTimePeriod.kt

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,22 @@ import kotlinx.serialization.Serializable
1717
/**
1818
* A difference between two [instants][Instant], decomposed into date and time components.
1919
*
20-
* The date components are: [years], [months], [days].
20+
* The date components are: [years] ([DateTimeUnit.YEAR]), [months] ([DateTimeUnit.MONTH]), [days] ([DateTimeUnit.DAY]).
2121
*
22-
* The time components are: [hours], [minutes], [seconds], [nanoseconds].
22+
* The time components are: [hours] ([DateTimeUnit.HOUR]), [minutes] ([DateTimeUnit.MINUTE]),
23+
* [seconds] ([DateTimeUnit.SECOND]), [nanoseconds] ([DateTimeUnit.NANOSECOND]).
24+
*
25+
* The time components are not independent and always overflow into one another.
26+
* Likewise, months overflow into years.
27+
* For example, there is no difference between `DateTimePeriod(months = 24, hours = 2, minutes = 63)` and
28+
* `DateTimePeriod(years = 2, hours = 3, minutes = 3)`.
29+
*
30+
* All components can also be negative: for example, `DateTimePeriod(months = -5, days = 6, hours = -3)`.
31+
* Whereas `months = 5` means "5 months after," `months = -5` means "5 months earlier."
32+
*
33+
* Since, semantically, a [DateTimePeriod] is a combination of [DateTimeUnit] values, in cases when the period is a
34+
* fixed time interval (like "yearly" or "quarterly"), please consider using [DateTimeUnit] directly instead:
35+
* for example, instead of `DateTimePeriod(months = 6)`, one could use `DateTimeUnit.MONTH * 6`.
2336
*
2437
* ### Interaction with other entities
2538
*
@@ -29,6 +42,10 @@ import kotlinx.serialization.Serializable
2942
* [DatePeriod] is a subtype of [DateTimePeriod] that only stores the date components and has all time components equal
3043
* to zero.
3144
*
45+
* [DateTimePeriod] can be thought of as a combination of a [Duration] and a [DatePeriod], as it contains both the
46+
* time components of [Duration] and the date components of [DatePeriod].
47+
* [Duration.toDateTimePeriod] can be used to convert a [Duration] to the corresponding [DateTimePeriod].
48+
*
3249
* ### Construction, serialization, and deserialization
3350
*
3451
* When a [DateTimePeriod] is constructed in any way, a [DatePeriod] value, which is a subtype of [DateTimePeriod],
@@ -98,7 +115,17 @@ public sealed class DateTimePeriod {
98115
/**
99116
* Converts this period to the ISO 8601 string representation for durations, for example, `P2M1DT3H`.
100117
*
101-
* @see DateTimePeriod.parse
118+
* Note that the ISO 8601 duration is not the same as [Duration],
119+
* but instead includes the date components, like [DateTimePeriod] does.
120+
*
121+
* Examples of the output:
122+
* - `P2Y4M-1D`: two years, four months, minus one day;
123+
* - `-P2Y4M1D`: minus two years, minus four months, minus one day;
124+
* - `P1DT3H2M4.123456789S`: one day, three hours, two minutes, four seconds, 123456789 nanoseconds;
125+
* - `P1DT-3H-2M-4.123456789S`: one day, minus three hours, minus two minutes,
126+
* minus four seconds, minus 123456789 nanoseconds;
127+
*
128+
* @see DateTimePeriod.parse for the detailed description of the format.
102129
*/
103130
override fun toString(): String = buildString {
104131
val sign = if (allNonpositive()) { append('-'); -1 } else 1
@@ -144,16 +171,38 @@ public sealed class DateTimePeriod {
144171
public companion object {
145172
/**
146173
* Parses a ISO 8601 duration string as a [DateTimePeriod].
174+
*
147175
* If the time components are absent or equal to zero, returns a [DatePeriod].
148176
*
149-
* Additionally, we support the `W` signifier to represent weeks.
177+
* Note that the ISO 8601 duration is not the same as [Duration],
178+
* but instead includes the date components, like [DateTimePeriod] does.
150179
*
151180
* Examples of durations in the ISO 8601 format:
152181
* - `P1Y40D` is one year and 40 days
153182
* - `-P1DT1H` is minus (one day and one hour)
154183
* - `P1DT-1H` is one day minus one hour
155184
* - `-PT0.000000001S` is minus one nanosecond
156185
*
186+
* The format is defined as follows:
187+
* - First, optionally, a `-` or `+`.
188+
* If `-` is present, the whole period after the `-` is negated: `-P-2M1D` is the same as `P2M-1D`.
189+
* - Then, the letter `P`.
190+
* - Optionally, the number of years, followed by `Y`.
191+
* - Optionally, the number of months, followed by `M`.
192+
* - Optionally, the number of weeks, followed by `W`.
193+
* This is not a part of the ISO 8601 format but an extension.
194+
* - Optionally, the number of days, followed by `D`.
195+
* - The string can end here if there are no more time components.
196+
* If there are time components, the letter `T` is required.
197+
* - Optionally, the number of hours, followed by `H`.
198+
* - Optionally, the number of minutes, followed by `M`.
199+
* - Optionally, the number of seconds, followed by `S`.
200+
* Seconds can optionally have a fractional part with up to nine digits.
201+
* The fractional part is separated with a `.`.
202+
*
203+
* All numbers can be negative, in which case, `-` is prepended to them.
204+
* Otherwise, a number can have `+` prepended to it, which does not have an effect.
205+
*
157206
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are
158207
* exceeded.
159208
*/
@@ -348,10 +397,14 @@ public class DatePeriod internal constructor(
348397
* Constructs a new [DatePeriod].
349398
*
350399
* It is recommended to always explicitly name the arguments when constructing this manually,
351-
* like `DatePeriod(years = 1, months = 12)`.
400+
* like `DatePeriod(years = 1, months = 12, days = 16)`.
352401
*
353402
* The passed numbers are not stored as is but are normalized instead for human readability, so, for example,
354-
* `DateTimePeriod(months = 24)` becomes `DateTimePeriod(years = 2)`.
403+
* `DateTimePeriod(months = 24, days = 41)` becomes `DateTimePeriod(years = 2, days = 41)`.
404+
*
405+
* If only a single component is set and is always non-zero and is semantically a fixed time interval
406+
* (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit.DateBased] instead.
407+
* For example, instead of `DatePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
355408
*
356409
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
357410
*/
@@ -435,10 +488,14 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos
435488
* Constructs a new [DateTimePeriod]. If all the time components are zero, returns a [DatePeriod].
436489
*
437490
* It is recommended to always explicitly name the arguments when constructing this manually,
438-
* like `DateTimePeriod(years = 1, months = 12)`.
491+
* like `DateTimePeriod(years = 1, months = 12, days = 16)`.
439492
*
440493
* The passed numbers are not stored as is but are normalized instead for human readability, so, for example,
441-
* `DateTimePeriod(months = 24)` becomes `DateTimePeriod(years = 2)`.
494+
* `DateTimePeriod(months = 24, days = 41)` becomes `DateTimePeriod(years = 2, days = 41)`.
495+
*
496+
* If only a single component is set and is always non-zero and is semantically a fixed time interval
497+
* (like "yearly" or "quarterly"), please consider using a multiple of [DateTimeUnit] instead.
498+
* For example, instead of `DateTimePeriod(months = 6)`, one can use `DateTimeUnit.MONTH * 6`.
442499
*
443500
* @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
444501
* @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds]
@@ -465,6 +522,10 @@ public fun DateTimePeriod(
465522
* The reason is that even a [Duration] obtained via [Duration.Companion.days] just means a multiple of 24 hours,
466523
* whereas in `kotlinx-datetime`, a day is a calendar day, which can be different from 24 hours.
467524
* See [DateTimeUnit.DayBased] for details.
525+
*
526+
* ```
527+
* 2.days.toDateTimePeriod() // 0 days, 48 hours
528+
* ```
468529
*/
469530
// TODO: maybe it's more consistent to throw here on overflow?
470531
public fun Duration.toDateTimePeriod(): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds)

core/common/src/Instant.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import kotlin.time.*
3838
* The [Clock.System] implementation uses the platform-specific system clock to obtain the current moment.
3939
* Note that this clock is not guaranteed to be monotonic, and it may be adjusted by the user or the system at any time,
4040
* so it should not be used for measuring time intervals.
41-
* For that, consider [TimeSource.Monotonic].
41+
* For that, consider using [TimeSource.Monotonic] and [TimeMark] instead of [Clock.System] and [Instant].
4242
*
4343
* ### Obtaining human-readable representations
4444
*
@@ -104,7 +104,11 @@ import kotlin.time.*
104104
* requiring a [TimeZone]:
105105
*
106106
* ```
107-
* Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin")) // one day from now in Berlin
107+
* // one day from now in Berlin
108+
* Clock.System.now().plus(1, DateTimeUnit.DAY, TimeZone.of("Europe/Berlin"))
109+
*
110+
* // a day and two hours short from two months later in Berlin
111+
* Clock.System.now().plus(DateTimePeriod(months = 2, days = -1, hours = -2), TimeZone.of("Europe/Berlin"))
108112
* ```
109113
*
110114
* The difference between [Instant] values in terms of calendar-based units can be obtained using the [periodUntil]
@@ -366,6 +370,13 @@ public fun String.toInstant(): Instant = Instant.parse(this)
366370
* Returns an instant that is the result of adding components of [DateTimePeriod] to this instant. The components are
367371
* added in the order from the largest units to the smallest, i.e. from years to nanoseconds.
368372
*
373+
* - If the [DateTimePeriod] only contains time-based components, please consider adding a [Duration] instead,
374+
* as in `Clock.System.now() + 5.hours`.
375+
* Then, it will not be necessary to pass the [timeZone].
376+
* - If the [DateTimePeriod] only has a single non-zero component (only the months or only the days),
377+
* please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], like in
378+
* `Clock.System.now().plus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`.
379+
*
369380
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in
370381
* [LocalDateTime].
371382
*/
@@ -375,6 +386,13 @@ public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst
375386
* Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components
376387
* are subtracted in the order from the largest units to the smallest, i.e. from years to nanoseconds.
377388
*
389+
* - If the [DateTimePeriod] only contains time-based components, please consider subtracting a [Duration] instead,
390+
* as in `Clock.System.now() - 5.hours`.
391+
* Then, it is not necessary to pass the [timeZone].
392+
* - If the [DateTimePeriod] only has a single non-zero component (only the months or only the days),
393+
* please consider using a multiple of [DateTimeUnit.DAY] or [DateTimeUnit.MONTH], as in
394+
* `Clock.System.now().minus(5, DateTimeUnit.DAY, TimeZone.currentSystemDefault())`.
395+
*
378396
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in
379397
* [LocalDateTime].
380398
*/

core/common/test/ClockTimeSourceTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,4 @@ class ClockTimeSourceTest {
8383
assertFailsWith<IllegalArgumentException> { markFuture - Duration.INFINITE }
8484
assertFailsWith<IllegalArgumentException> { markPast + Duration.INFINITE }
8585
}
86-
}
86+
}

0 commit comments

Comments
 (0)