Skip to content

Commit dc5c965

Browse files
authored
Add LocalTime (#187)
1 parent 5a920ea commit dc5c965

17 files changed

+597
-53
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The library provides the basic set of types for working with date and time:
3636
- `Clock` to obtain the current instant;
3737
- `LocalDateTime` to represent date and time components without a reference to the particular time zone;
3838
- `LocalDate` to represent the components of date only;
39+
- `LocalTime` to represent the components of time only;
3940
- `TimeZone` and `FixedOffsetTimeZone` provide time zone information to convert between `Instant` and `LocalDateTime`;
4041
- `Month` and `DayOfWeek` enums;
4142
- `DateTimePeriod` to represent a difference between two instants decomposed into date and time units;
@@ -61,6 +62,8 @@ Here is some basic advice on how to choose which of the date-carrying types to u
6162
Also, use `LocalDateTime` to decode an `Instant` to its local date-time components for display and UIs.
6263

6364
- Use `LocalDate` to represent a date of the event that does not have a specific time associated with it (like a birth date).
65+
66+
- Use `LocalTime` to represent a time of the event that does not have a specific date associated with it.
6467

6568
## Operations
6669

@@ -143,14 +146,31 @@ Note, that today's date really depends on the time zone in which you're observin
143146
val knownDate = LocalDate(2020, 2, 21)
144147
```
145148

149+
### Getting local time components
150+
151+
`LocalTime` type represents local time without date. You can obtain it from `Instant`
152+
by converting it to `LocalDateTime` and taking its `time` property.
153+
154+
```kotlin
155+
val now: Instant = Clock.System.now()
156+
val thisTime: LocalTime = now.toLocalDateTime(TimeZone.currentSystemDefault()).time
157+
```
158+
159+
`LocalTime` can be constructed from four components, hour, minute, second and nanosecond:
160+
```kotlin
161+
val knownTime = LocalTime(hour = 23, minute = 59, second = 12)
162+
val timeWithNanos = LocalTime(hour = 23, minute = 59, second = 12, nanosecond = 999)
163+
val hourMinute = LocalTime(hour = 12, minute = 13)
164+
```
165+
146166
### Converting instant to and from unix time
147167

148168
An `Instant` can be converted to a number of milliseconds since the Unix/POSIX epoch with the `toEpochMilliseconds()` function.
149169
To convert back, use `Instant.fromEpochMilliseconds(Long)` companion object function.
150170

151171
### Converting instant and local date/time to and from string
152172

153-
Currently, `Instant`, `LocalDateTime`, and `LocalDate` only support ISO-8601 format.
173+
Currently, `Instant`, `LocalDateTime`, `LocalDate` and `LocalTime` only support ISO-8601 format.
154174
The `toString()` function is used to convert the value to a string in that format, and
155175
the `parse` function in companion object is used to parse a string representation back.
156176

@@ -168,10 +188,14 @@ where it feels more convenient:
168188

169189
`LocalDate` uses format with just year, month, and date components, e.g. `2010-06-01`.
170190

191+
`LocalTime` uses format with just hour, minute, second and (if non-zero) nanosecond components, e.g. `12:01:03`.
192+
171193
```kotlin
172194
"2010-06-01T22:19:44.475Z".toInstant()
173195
"2010-06-01T22:19:44".toLocalDateTime()
174196
"2010-06-01".toLocalDate()
197+
"12:01:03".toLocalTime()
198+
"12:0:03.999".toLocalTime()
175199
```
176200

177201
### Instant arithmetic

core/common/src/LocalDate.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this)
113113
public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalDateTime =
114114
LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond)
115115

116+
/**
117+
* Combines this date's components with the specified [LocalTime] components into a [LocalDateTime] value.
118+
*
119+
* For finding an instant that corresponds to the start of a date in a particular time zone consider using
120+
* [LocalDate.atStartOfDayIn] function because a day does not always start at the fixed time 0:00:00.
121+
*/
122+
public fun LocalDate.atTime(time: LocalTime): LocalDateTime = atTime(time.hour, time.minute, time.second, time.nanosecond)
123+
116124

117125
/**
118126
* Returns a date that is the result of adding components of [DatePeriod] to this date. The components are

core/common/src/LocalDateTime.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ public expect class LocalDateTime : Comparable<LocalDateTime> {
107107
/** Returns the date part of this date/time value. */
108108
public val date: LocalDate
109109

110+
/** Returns the time part of this date/time value. */
111+
public val time: LocalTime
112+
110113
/**
111114
* Compares `this` date/time value with the [other] date/time value.
112115
* Returns zero if this value is equal to the other,

core/common/src/LocalTime.kt

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2019-2022 JetBrains s.r.o.
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
7+
8+
import kotlinx.datetime.serializers.LocalTimeIso8601Serializer
9+
import kotlinx.serialization.Serializable
10+
11+
/**
12+
* The time part of [LocalDateTime].
13+
*
14+
* This class represents time-of-day without a referencing a specific date.
15+
* To reconstruct a full [LocalDateTime], representing civil date and time, [LocalTime] needs to be
16+
* combined with [LocalDate] via [LocalDate.atTime] or [LocalTime.atDate].
17+
*
18+
* Also, [LocalTime] does not reference a particular time zone.
19+
* Therefore, even on the same date, [LocalTime] denotes different moments of time.
20+
* For example, `18:43` happens at different moments in Berlin and in Tokyo.
21+
*
22+
* The arithmetic on [LocalTime] values is not provided, since without accounting for the time zone
23+
* transitions it may give misleading results.
24+
*/
25+
@Serializable(LocalTimeIso8601Serializer::class)
26+
public expect class LocalTime : Comparable<LocalTime> {
27+
public companion object {
28+
29+
/**
30+
* Parses a string that represents a time value in ISO-8601 and returns the parsed [LocalTime] value.
31+
*
32+
* Examples of time in ISO-8601 format:
33+
* - `18:43`
34+
* - `18:43:00`
35+
* - `18:43:00.500`
36+
* - `18:43:00.123456789`
37+
*
38+
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalTime] are
39+
* exceeded.
40+
*/
41+
public fun parse(isoString: String): LocalTime
42+
43+
internal val MIN: LocalTime
44+
internal val MAX: LocalTime
45+
}
46+
47+
/**
48+
* Constructs a [LocalTime] instance from the given time components.
49+
*
50+
* The supported ranges of components:
51+
* - [hour] `0..23`
52+
* - [minute] `0..59`
53+
* - [second] `0..59`
54+
* - [nanosecond] `0..999_999_999`
55+
*
56+
* @throws IllegalArgumentException if any parameter is out of range.
57+
*/
58+
public constructor(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0)
59+
60+
/** Returns the hour-of-day time component of this time value. */
61+
public val hour: Int
62+
/** Returns the minute-of-hour time component of this time value. */
63+
public val minute: Int
64+
/** Returns the second-of-minute time component of this time value. */
65+
public val second: Int
66+
/** Returns the nanosecond-of-second time component of this time value. */
67+
public val nanosecond: Int
68+
69+
/**
70+
* Compares `this` time value with the [other] time value.
71+
* Returns zero if this value is equal to the other, a negative number if this value occurs earlier
72+
* in the course of a typical day than the other, and a positive number if this value occurs
73+
* later in the course of a typical day than the other.
74+
*
75+
* Note that, on days when there is a time overlap (for example, due to the daylight saving time
76+
* transitions in autumn), a "lesser" wall-clock reading can, in fact, happen later than the
77+
* "greater" one.
78+
*/
79+
public override operator fun compareTo(other: LocalTime): Int
80+
81+
/**
82+
* Converts this time value to the ISO-8601 string representation.
83+
*
84+
* @see LocalDateTime.parse
85+
*/
86+
public override fun toString(): String
87+
}
88+
89+
/**
90+
* Converts this string representing a time value in ISO-8601 format to a [LocalTime] value.
91+
*
92+
* See [LocalTime.parse] for examples of time string representations.
93+
*
94+
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalTime] are exceeded.
95+
*/
96+
public fun String.toLocalTime(): LocalTime = LocalTime.parse(this)
97+
98+
/**
99+
* Combines this time's components with the specified date components into a [LocalDateTime] value.
100+
*/
101+
public fun LocalTime.atDate(year: Int, monthNumber: Int, dayOfMonth: Int = 0): LocalDateTime =
102+
LocalDateTime(year, monthNumber, dayOfMonth, hour, minute, second, nanosecond)
103+
104+
/**
105+
* Combines this time's components with the specified date components into a [LocalDateTime] value.
106+
*/
107+
public fun LocalTime.atDate(year: Int, month: Month, dayOfMonth: Int = 0): LocalDateTime =
108+
LocalDateTime(year, month, dayOfMonth, hour, minute, second, nanosecond)
109+
110+
/**
111+
* Combines this time's components with the specified [LocalDate] components into a [LocalDateTime] value.
112+
*/
113+
public fun LocalTime.atDate(date: LocalDate): LocalDateTime = atDate(date.year, date.monthNumber, date.dayOfMonth)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2019-2022 JetBrains s.r.o.
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.serializers
7+
8+
import kotlinx.datetime.*
9+
import kotlinx.serialization.*
10+
import kotlinx.serialization.descriptors.*
11+
import kotlinx.serialization.encoding.*
12+
13+
/**
14+
* A serializer for [LocalTime] that uses the ISO-8601 representation.
15+
*
16+
* JSON example: `"12:01:03.999"`
17+
*
18+
* @see LocalDate.parse
19+
* @see LocalDate.toString
20+
*/
21+
public object LocalTimeIso8601Serializer : KSerializer<LocalTime> {
22+
23+
override val descriptor: SerialDescriptor =
24+
PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING)
25+
26+
override fun deserialize(decoder: Decoder): LocalTime =
27+
LocalTime.parse(decoder.decodeString())
28+
29+
override fun serialize(encoder: Encoder, value: LocalTime) {
30+
encoder.encodeString(value.toString())
31+
}
32+
}
33+
34+
/**
35+
* A serializer for [LocalTime] that represents a value as its components.
36+
*
37+
* JSON example: `{"hour":12,"minute":1,"second":3,"nanosecond":999}`
38+
*/
39+
public object LocalTimeComponentSerializer : KSerializer<LocalTime> {
40+
41+
override val descriptor: SerialDescriptor =
42+
buildClassSerialDescriptor("LocalTime") {
43+
element<Short>("hour")
44+
element<Short>("minute")
45+
element<Short>("second", isOptional = true)
46+
element<Int>("nanosecond", isOptional = true)
47+
}
48+
49+
@Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException`
50+
override fun deserialize(decoder: Decoder): LocalTime =
51+
decoder.decodeStructure(descriptor) {
52+
var hour: Short? = null
53+
var minute: Short? = null
54+
var second: Short = 0
55+
var nanosecond = 0
56+
loop@while (true) {
57+
when (val index = decodeElementIndex(descriptor)) {
58+
0 -> hour = decodeShortElement(descriptor, 0)
59+
1 -> minute = decodeShortElement(descriptor, 1)
60+
2 -> second = decodeShortElement(descriptor, 2)
61+
3 -> nanosecond = decodeIntElement(descriptor, 3)
62+
CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262
63+
else -> throw SerializationException("Unexpected index: $index")
64+
}
65+
}
66+
if (hour == null) throw MissingFieldException("hour")
67+
if (minute == null) throw MissingFieldException("minute")
68+
LocalTime(hour.toInt(), minute.toInt(), second.toInt(), nanosecond)
69+
}
70+
71+
override fun serialize(encoder: Encoder, value: LocalTime) {
72+
encoder.encodeStructure(descriptor) {
73+
encodeShortElement(descriptor, 0, value.hour.toShort())
74+
encodeShortElement(descriptor, 1, value.minute.toShort())
75+
if (value.second != 0 || value.nanosecond != 0) {
76+
encodeShortElement(descriptor, 2, value.second.toShort())
77+
if (value.nanosecond != 0) {
78+
encodeIntElement(descriptor, 3, value.nanosecond)
79+
}
80+
}
81+
}
82+
}
83+
}

core/common/test/LocalDateTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class LocalDateTest {
6262
fun atTime() {
6363
val date = LocalDate(2016, Month.FEBRUARY, 29)
6464
val datetime = date.atTime(12, 1, 59)
65+
val datetimeWithLocalTime = date.atTime(LocalTime(12, 1, 59))
66+
assertEquals(datetime, datetimeWithLocalTime)
6567
checkComponents(datetime, 2016, 2, 29, 12, 1, 59)
6668
checkLocalDateTimePart(date, datetime)
6769
}

0 commit comments

Comments
 (0)