@@ -11,23 +11,70 @@ import kotlin.math.*
11
11
import kotlin.time.Duration
12
12
import kotlinx.serialization.Serializable
13
13
14
+ /* *
15
+ * A difference between two [instants][Instant], decomposed into date and time components.
16
+ *
17
+ * The date components are: [years], [months], [days].
18
+ *
19
+ * The time components are: [hours], [minutes], [seconds], [nanoseconds].
20
+ *
21
+ * A `DateTimePeriod` can be constructed using the same-named constructor function,
22
+ * [parsed][DateTimePeriod.parse] from a string, or returned as the result of instant arithmetic operations (see [Instant.periodUntil]).
23
+ * All these functions can return a [DatePeriod] value, which is a subtype of `DateTimePeriod`,
24
+ * a special case that only stores date components, if all time components of the result happen to be zero.
25
+ */
14
26
@Serializable(with = DateTimePeriodIso8601Serializer ::class )
15
27
// TODO: could be error-prone without explicitly named params
16
28
public sealed class DateTimePeriod {
17
29
internal abstract val totalMonths: Int
30
+
31
+ /* *
32
+ * The number of calendar days.
33
+ *
34
+ * Note that a calendar day is not identical to 24 hours, see [DateTimeUnit.DayBased] for details.
35
+ */
18
36
public abstract val days: Int
19
37
internal abstract val totalNanoseconds: Long
20
38
39
+ /* *
40
+ * The number of whole years.
41
+ */
21
42
public val years: Int get() = totalMonths / 12
43
+
44
+ /* *
45
+ * The number of months in this period that don't form a whole year, so this value is always in `(-11..11)`.
46
+ */
22
47
public val months: Int get() = totalMonths % 12
48
+
49
+ /* *
50
+ * The number of whole hours in this period.
51
+ */
23
52
public open val hours: Int get() = (totalNanoseconds / 3_600_000_000_000 ).toInt()
53
+
54
+ /* *
55
+ * The number of whole minutes in this period that don't form a whole hour, so this value is always in `(-59..59)`.
56
+ */
24
57
public open val minutes: Int get() = ((totalNanoseconds % 3_600_000_000_000 ) / 60_000_000_000 ).toInt()
58
+
59
+ /* *
60
+ * The number of whole seconds in this period that don't form a whole minute, so this value is always in `(-59..59)`.
61
+ */
25
62
public open val seconds: Int get() = ((totalNanoseconds % 60_000_000_000 ) / NANOS_PER_ONE ).toInt()
63
+
64
+ /* *
65
+ * The number of whole nanoseconds in this period that don't form a whole second, so this value is always in
66
+ * `(-999_999_999..999_999_999)`.
67
+ */
26
68
public open val nanoseconds: Int get() = (totalNanoseconds % NANOS_PER_ONE ).toInt()
27
69
28
70
private fun allNonpositive () =
29
71
totalMonths <= 0 && days <= 0 && totalNanoseconds <= 0 && (totalMonths or days != 0 || totalNanoseconds != 0L )
30
72
73
+ /* *
74
+ * Converts this period to the ISO-8601 string representation for durations.
75
+ *
76
+ * @see DateTimePeriod.parse
77
+ */
31
78
override fun toString (): String = buildString {
32
79
val sign = if (allNonpositive()) { append(' -' ); - 1 } else 1
33
80
append(' P' )
@@ -70,6 +117,21 @@ public sealed class DateTimePeriod {
70
117
}
71
118
72
119
public companion object {
120
+ /* *
121
+ * Parses a ISO-8601 duration string as a [DateTimePeriod].
122
+ * If the time components are absent or equal to zero, returns a [DatePeriod].
123
+ *
124
+ * Additionally, we support the `W` signifier to represent weeks.
125
+ *
126
+ * Examples of durations in the ISO-8601 format:
127
+ * - `P1Y40D` is one year and 40 days
128
+ * - `-P1DT1H` is minus (one day and one hour)
129
+ * - `P1DT-1H` is one day minus one hour
130
+ * - `-PT0.000000001S` is minus one nanosecond
131
+ *
132
+ * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are
133
+ * exceeded.
134
+ */
73
135
public fun parse (text : String ): DateTimePeriod {
74
136
fun parseException (message : String , position : Int ): Nothing =
75
137
throw DateTimeFormatException (" Parse error at char $position : $message " )
@@ -234,22 +296,58 @@ public sealed class DateTimePeriod {
234
296
}
235
297
}
236
298
299
+ /* *
300
+ * Parses the ISO-8601 duration representation as a [DateTimePeriod].
301
+ *
302
+ * See [DateTimePeriod.parse] for examples.
303
+ *
304
+ * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are exceeded.
305
+ *
306
+ * @see DateTimePeriod.parse
307
+ */
237
308
public fun String.toDateTimePeriod (): DateTimePeriod = DateTimePeriod .parse(this )
238
309
310
+ /* *
311
+ * A special case of [DateTimePeriod] that only stores date components and has all time components equal to zero.
312
+ *
313
+ * A `DatePeriod` is automatically returned from all constructor functions for [DateTimePeriod] if it turns out that
314
+ * the time components are zero.
315
+ *
316
+ * `DatePeriod` values are used in operations on [LocalDates][LocalDate] and are returned from operations on [LocalDates][LocalDate],
317
+ * but they also can be passed anywhere a [DateTimePeriod] is expected.
318
+ */
239
319
@Serializable(with = DatePeriodIso8601Serializer ::class )
240
320
public class DatePeriod internal constructor(
241
321
internal override val totalMonths : Int ,
242
322
override val days : Int ,
243
323
) : DateTimePeriod() {
244
324
public constructor (years: Int = 0 , months: Int = 0 , days: Int = 0 ): this (totalMonths(years, months), days)
245
325
// avoiding excessive computations
326
+ /* * The number of whole hours in this period. Always equal to zero. */
246
327
override val hours: Int get() = 0
328
+
329
+ /* * The number of whole minutes in this period. Always equal to zero. */
247
330
override val minutes: Int get() = 0
331
+
332
+ /* * The number of whole seconds in this period. Always equal to zero. */
248
333
override val seconds: Int get() = 0
334
+
335
+ /* * The number of nanoseconds in this period. Always equal to zero. */
249
336
override val nanoseconds: Int get() = 0
250
337
internal override val totalNanoseconds: Long get() = 0
251
338
252
339
public companion object {
340
+ /* *
341
+ * Parses the ISO-8601 duration representation as a [DatePeriod].
342
+ *
343
+ * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not
344
+ * zero.
345
+ *
346
+ * @throws IllegalArgumentException if the text cannot be parsed, the boundaries of [DatePeriod] are exceeded,
347
+ * or any time components are not zero.
348
+ *
349
+ * @see DateTimePeriod.parse
350
+ */
253
351
public fun parse (text : String ): DatePeriod =
254
352
when (val period = DateTimePeriod .parse(text)) {
255
353
is DatePeriod -> period
@@ -258,6 +356,17 @@ public class DatePeriod internal constructor(
258
356
}
259
357
}
260
358
359
+ /* *
360
+ * Parses the ISO-8601 duration representation as a [DatePeriod].
361
+ *
362
+ * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not
363
+ * zero.
364
+ *
365
+ * @throws IllegalArgumentException if the text cannot be parsed, the boundaries of [DatePeriod] are exceeded,
366
+ * or any time components are not zero.
367
+ *
368
+ * @see DateTimePeriod.parse
369
+ */
261
370
public fun String.toDatePeriod (): DatePeriod = DatePeriod .parse(this )
262
371
263
372
private class DateTimePeriodImpl (
@@ -295,6 +404,19 @@ internal fun buildDateTimePeriod(totalMonths: Int = 0, days: Int = 0, totalNanos
295
404
else
296
405
DatePeriod (totalMonths, days)
297
406
407
+ /* *
408
+ * Constructs a new [DateTimePeriod]. If all the time components are zero, returns a [DatePeriod].
409
+ *
410
+ * It is recommended to always explicitly name the arguments when constructing this manually,
411
+ * like `DateTimePeriod(years = 1, months = 12)`.
412
+ *
413
+ * The passed numbers are not stored as is but are normalized instead for human readability, so, for example,
414
+ * `DateTimePeriod(months = 24)` becomes `DateTimePeriod(years = 2)`.
415
+ *
416
+ * @throws IllegalArgumentException if the total number of months in [years] and [months] overflows an [Int].
417
+ * @throws IllegalArgumentException if the total number of months in [hours], [minutes], [seconds] and [nanoseconds]
418
+ * overflows a [Long].
419
+ */
298
420
public fun DateTimePeriod (
299
421
years : Int = 0,
300
422
months : Int = 0,
@@ -306,14 +428,31 @@ public fun DateTimePeriod(
306
428
): DateTimePeriod = buildDateTimePeriod(totalMonths(years, months), days,
307
429
totalNanoseconds(hours, minutes, seconds, nanoseconds))
308
430
431
+ /* *
432
+ * Constructs a [DateTimePeriod] from a [Duration].
433
+ *
434
+ * If the duration value is too big to be represented as a [Long] number of nanoseconds,
435
+ * the result will be [Long.MAX_VALUE] nanoseconds.
436
+ */
437
+ // TODO: maybe it's more consistent to throw here on overflow?
309
438
public fun Duration.toDateTimePeriod (): DateTimePeriod = buildDateTimePeriod(totalNanoseconds = inWholeNanoseconds)
310
439
440
+ /* *
441
+ * Adds two [DateTimePeriod] instances.
442
+ *
443
+ * @throws DateTimeArithmeticException if arithmetic overflow happens.
444
+ */
311
445
public operator fun DateTimePeriod.plus (other : DateTimePeriod ): DateTimePeriod = buildDateTimePeriod(
312
446
safeAdd(totalMonths, other.totalMonths),
313
447
safeAdd(days, other.days),
314
448
safeAdd(totalNanoseconds, other.totalNanoseconds),
315
449
)
316
450
451
+ /* *
452
+ * Adds two [DatePeriod] instances.
453
+ *
454
+ * @throws DateTimeArithmeticException if arithmetic overflow happens.
455
+ */
317
456
public operator fun DatePeriod.plus (other : DatePeriod ): DatePeriod = DatePeriod (
318
457
safeAdd(totalMonths, other.totalMonths),
319
458
safeAdd(days, other.days),
0 commit comments