Skip to content

Commit 3dbe188

Browse files
authored
Add LocalTime.[from/to]MillisecondOfDay (#213)
* The documentation for existing similar methods is improved. * `ThreeTenBpLocalTimeTest` is no longer needed due to more comprenensive testing in common code. * A constant introduced for the number of iterations in tests that repeatedly generate random data to test functionality.
1 parent d686d81 commit 3dbe188

15 files changed

+183
-197
lines changed

core/common/src/LocalTime.kt

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,42 @@ public expect class LocalTime : Comparable<LocalTime> {
4141
public fun parse(isoString: String): LocalTime
4242

4343
/**
44-
* Returns a LocalTime with the specified [secondOfDay]. The nanosecond field will be set to zero.
44+
* Constructs a [LocalTime] that represents the specified number of seconds since the start of a calendar day.
45+
* The fractional parts of the second will be zero.
4546
*
46-
* @throws IllegalArgumentException if the boundaries of [secondOfDay] are exceeded.
47+
* @throws IllegalArgumentException if [secondOfDay] is outside the `0 until 86400` range,
48+
* with 86400 being the number of seconds in a calendar day.
49+
*
50+
* @see LocalTime.toSecondOfDay
51+
* @see LocalTime.fromMillisecondOfDay
52+
* @see LocalTime.fromNanosecondOfDay
4753
*/
4854
public fun fromSecondOfDay(secondOfDay: Int): LocalTime
4955

5056
/**
51-
* Returns a LocalTime with the specified [nanosecondOfDay].
57+
* Constructs a [LocalTime] that represents the specified number of milliseconds since the start of
58+
* a calendar day.
59+
* The sub-millisecond parts of the `LocalTime` will be zero.
60+
*
61+
* @throws IllegalArgumentException if [millisecondOfDay] is outside the `0 until 86400 * 1_000` range,
62+
* with 86400 being the number of seconds in a calendar day.
63+
*
64+
* @see LocalTime.fromSecondOfDay
65+
* @see LocalTime.toMillisecondOfDay
66+
* @see LocalTime.fromNanosecondOfDay
67+
*/
68+
public fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime
69+
70+
/**
71+
* Constructs a [LocalTime] that represents the specified number of nanoseconds since the start of
72+
* a calendar day.
5273
*
53-
* @throws IllegalArgumentException if the boundaries of [nanosecondOfDay] are exceeded.
74+
* @throws IllegalArgumentException if [nanosecondOfDay] is outside the `0 until 86400 * 1_000_000_000` range,
75+
* with 86400 being the number of seconds in a calendar day.
76+
*
77+
* @see LocalTime.fromSecondOfDay
78+
* @see LocalTime.fromMillisecondOfDay
79+
* @see LocalTime.toNanosecondOfDay
5480
*/
5581
public fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime
5682

@@ -80,10 +106,13 @@ public expect class LocalTime : Comparable<LocalTime> {
80106
/** Returns the nanosecond-of-second time component of this time value. */
81107
public val nanosecond: Int
82108

83-
/** Returns the time as a second of a day, from 0 to 24 * 60 * 60 - 1. */
109+
/** Returns the time as a second of a day, in `0 until 24 * 60 * 60`. */
84110
public fun toSecondOfDay(): Int
85111

86-
/** Returns the time as a nanosecond of a day, from 0 to 24 * 60 * 60 * 1_000_000_000 - 1. */
112+
/** Returns the time as a millisecond of a day, in `0 until 24 * 60 * 60 * 1_000`. */
113+
public fun toMillisecondOfDay(): Int
114+
115+
/** Returns the time as a nanosecond of a day, in `0 until 24 * 60 * 60 * 1_000_000_000`. */
87116
public fun toNanosecondOfDay(): Long
88117

89118
/**

core/common/src/math.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,27 @@ internal fun Long.clampToInt(): Int =
1212
else -> toInt()
1313
}
1414

15+
internal const val SECONDS_PER_HOUR = 60 * 60
16+
17+
internal const val SECONDS_PER_MINUTE = 60
18+
19+
internal const val MINUTES_PER_HOUR = 60
20+
21+
internal const val HOURS_PER_DAY = 24
22+
23+
internal const val SECONDS_PER_DAY: Int = SECONDS_PER_HOUR * HOURS_PER_DAY
24+
25+
internal const val NANOS_PER_ONE = 1_000_000_000
1526
internal const val NANOS_PER_MILLI = 1_000_000
1627
internal const val MILLIS_PER_ONE = 1_000
17-
internal const val NANOS_PER_ONE = 1_000_000_000
28+
29+
internal const val NANOS_PER_DAY: Long = NANOS_PER_ONE * SECONDS_PER_DAY.toLong()
30+
31+
internal const val NANOS_PER_MINUTE: Long = NANOS_PER_ONE * SECONDS_PER_MINUTE.toLong()
32+
33+
internal const val NANOS_PER_HOUR = NANOS_PER_ONE * SECONDS_PER_HOUR.toLong()
34+
35+
internal const val MILLIS_PER_DAY: Int = SECONDS_PER_DAY * MILLIS_PER_ONE
1836

1937
internal expect fun safeMultiply(a: Long, b: Long): Long
2038
internal expect fun safeMultiply(a: Int, b: Int): Int

core/common/test/InstantTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ class InstantTest {
263263

264264
@Test
265265
fun diffInvariant() {
266-
repeat(1000) {
266+
repeat(STRESS_TEST_ITERATIONS) {
267267
val millis1 = Random.nextLong(2_000_000_000_000L)
268268
val millis2 = Random.nextLong(2_000_000_000_000L)
269269
val instant1 = Instant.fromEpochMilliseconds(millis1)
@@ -279,7 +279,7 @@ class InstantTest {
279279

280280
@Test
281281
fun diffInvariantSameAsDate() {
282-
repeat(1000) {
282+
repeat(STRESS_TEST_ITERATIONS) {
283283
val millis1 = Random.nextLong(2_000_000_000_000L)
284284
val millis2 = Random.nextLong(2_000_000_000_000L)
285285
with(TimeZone.UTC) TZ@ {

core/common/test/LocalDateTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class LocalDateTest {
119119
assertEquals(origin, origin.plus(0, DateTimeUnit.DAY))
120120
assertEquals(origin, origin.plus(DatePeriod(days = 0)))
121121

122-
repeat(1000) {
122+
repeat(STRESS_TEST_ITERATIONS) {
123123
val days1 = Random.nextInt(-3652..3652)
124124
val days2 = Random.nextInt(-3652..3652)
125125
val ldtBefore = origin + DatePeriod(days = days1)

core/common/test/LocalTimeTest.kt

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
package kotlinx.datetime.test
77

88
import kotlinx.datetime.*
9+
import kotlin.math.*
10+
import kotlin.random.*
911
import kotlin.test.*
1012

1113
class LocalTimeTest {
12-
14+
1315
@Test
1416
fun localTimeParsing() {
1517
fun checkParsedComponents(value: String, hour: Int, minute: Int, second: Int, nanosecond: Int) {
@@ -63,7 +65,8 @@ class LocalTimeTest {
6365
Pair(LocalTime(0, 0, 0, 9999), "00:00:00.000009999"),
6466
Pair(LocalTime(0, 0, 0, 999), "00:00:00.000000999"),
6567
Pair(LocalTime(0, 0, 0, 99), "00:00:00.000000099"),
66-
Pair(LocalTime(0, 0, 0, 9), "00:00:00.000000009"))
68+
Pair(LocalTime(0, 0, 0, 9), "00:00:00.000000009"),
69+
)
6770
for ((time, str) in data) {
6871
assertEquals(str, time.toString())
6972
assertEquals(time, LocalTime.parse(str))
@@ -89,9 +92,18 @@ class LocalTimeTest {
8992
0L to LocalTime(0, 0),
9093
5000000001L to LocalTime(0, 0, 5, 1),
9194
44105123456789L to LocalTime(12, 15, 5, 123456789),
92-
86399999999999L to LocalTime(23, 59, 59, 999999999),
93-
)
94-
95+
NANOS_PER_DAY - 1 to LocalTime(23, 59, 59, 999999999),
96+
) + buildMap {
97+
repeat(STRESS_TEST_ITERATIONS) {
98+
val hour = Random.nextInt(24)
99+
val minute = Random.nextInt(60)
100+
val second = Random.nextInt(60)
101+
val nanosecond = Random.nextInt(1_000_000_000)
102+
val nanosecondOfDay =
103+
hour * NANOS_PER_HOUR + minute * NANOS_PER_MINUTE + second * NANOS_PER_ONE.toLong() + nanosecond
104+
put(nanosecondOfDay, LocalTime(hour, minute, second, nanosecond))
105+
}
106+
}
95107
data.forEach { (nanosecondOfDay, localTime) ->
96108
assertEquals(nanosecondOfDay, localTime.toNanosecondOfDay())
97109
assertEquals(localTime, LocalTime.fromNanosecondOfDay(nanosecondOfDay))
@@ -101,30 +113,69 @@ class LocalTimeTest {
101113
@Test
102114
fun fromNanosecondOfDayInvalid() {
103115
assertFailsWith<IllegalArgumentException> { LocalTime.fromNanosecondOfDay(-1) }
104-
assertFailsWith<IllegalArgumentException> { LocalTime.fromNanosecondOfDay(86400000000000L) }
105-
assertFailsWith<IllegalArgumentException> { LocalTime.fromNanosecondOfDay(Long.MAX_VALUE) }
116+
assertFailsWith<IllegalArgumentException> { LocalTime.fromNanosecondOfDay(NANOS_PER_DAY) }
117+
repeat(STRESS_TEST_ITERATIONS) {
118+
assertFailsWith<IllegalArgumentException> {
119+
LocalTime.fromNanosecondOfDay(NANOS_PER_DAY + Random.nextLong().absoluteValue)
120+
}
121+
}
106122
}
107123

108124
@Test
109-
fun fromSecondOfDay() {
125+
fun fromMillisecondOfDay() {
110126
val data = mapOf(
111127
0 to LocalTime(0, 0),
112-
5 to LocalTime(0, 0, 5),
113-
44105 to LocalTime(12, 15, 5),
114-
86399 to LocalTime(23, 59, 59),
115-
)
128+
5001 to LocalTime(0, 0, 5, 1000000),
129+
44105123 to LocalTime(12, 15, 5, 123000000),
130+
MILLIS_PER_DAY - 1 to LocalTime(23, 59, 59, 999000000),
131+
) + buildMap {
132+
repeat(STRESS_TEST_ITERATIONS) {
133+
val hour = Random.nextInt(24)
134+
val minute = Random.nextInt(60)
135+
val second = Random.nextInt(60)
136+
val millisecond = Random.nextInt(1000)
137+
val millisecondOfDay =
138+
(hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second) * MILLIS_PER_ONE +
139+
millisecond
140+
put(millisecondOfDay, LocalTime(hour, minute, second, millisecond * NANOS_PER_MILLI))
141+
}
142+
}
143+
data.forEach { (millisecondOfDay, localTime) ->
144+
assertEquals(millisecondOfDay, localTime.toMillisecondOfDay())
145+
assertEquals(localTime, LocalTime.fromMillisecondOfDay(millisecondOfDay))
146+
}
147+
}
116148

117-
data.forEach { (secondOfDay, localTime) ->
118-
assertEquals(secondOfDay, localTime.toSecondOfDay())
119-
assertEquals(localTime, LocalTime.fromSecondOfDay(secondOfDay))
149+
@Test
150+
fun fromMillisecondOfDayInvalid() {
151+
assertFailsWith<IllegalArgumentException> { LocalTime.fromMillisecondOfDay(-1) }
152+
assertFailsWith<IllegalArgumentException> { LocalTime.fromMillisecondOfDay(MILLIS_PER_DAY) }
153+
repeat(STRESS_TEST_ITERATIONS) {
154+
assertFailsWith<IllegalArgumentException> {
155+
LocalTime.fromMillisecondOfDay(MILLIS_PER_DAY + Random.nextInt().absoluteValue)
156+
}
157+
}
158+
}
159+
160+
@Test
161+
fun fromSecondOfDay() {
162+
var t = LocalTime(0, 0, 0, 0)
163+
for (i in 0 until SECONDS_PER_DAY) {
164+
assertEquals(i, t.toSecondOfDay())
165+
assertEquals(t, LocalTime.fromSecondOfDay(t.toSecondOfDay()))
166+
t = t.plusSeconds(1)
120167
}
121168
}
122169

123170
@Test
124171
fun fromSecondOfDayInvalid() {
125172
assertFailsWith<IllegalArgumentException> { LocalTime.fromSecondOfDay(-1) }
126-
assertFailsWith<IllegalArgumentException> { LocalTime.fromSecondOfDay(86400) }
127-
assertFailsWith<IllegalArgumentException> { LocalTime.fromSecondOfDay(Int.MAX_VALUE) }
173+
assertFailsWith<IllegalArgumentException> { LocalTime.fromSecondOfDay(SECONDS_PER_DAY) }
174+
repeat(STRESS_TEST_ITERATIONS) {
175+
assertFailsWith<IllegalArgumentException> {
176+
LocalTime.fromSecondOfDay(SECONDS_PER_DAY + Random.nextInt().absoluteValue)
177+
}
178+
}
128179
}
129180

130181
@Test
@@ -166,3 +217,18 @@ fun checkEquals(expected: LocalTime, actual: LocalTime) {
166217
assertEquals(expected.hashCode(), actual.hashCode())
167218
assertEquals(expected.toString(), actual.toString())
168219
}
220+
221+
private fun LocalTime.plusSeconds(secondsToAdd: Long): LocalTime {
222+
if (secondsToAdd == 0L) {
223+
return this
224+
}
225+
val sofd: Int = hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second
226+
val newSofd: Int = ((secondsToAdd % SECONDS_PER_DAY).toInt() + sofd + SECONDS_PER_DAY) % SECONDS_PER_DAY
227+
if (sofd == newSofd) {
228+
return this
229+
}
230+
val newHour: Int = newSofd / SECONDS_PER_HOUR
231+
val newMinute: Int = newSofd / SECONDS_PER_MINUTE % MINUTES_PER_HOUR
232+
val newSecond: Int = newSofd % SECONDS_PER_MINUTE
233+
return LocalTime(newHour, newMinute, newSecond, nanosecond)
234+
}

core/common/test/MultiplyAndDivideTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/*
2-
* Copyright 2019-2020 JetBrains s.r.o.
2+
* Copyright 2019-2022 JetBrains s.r.o.
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
package kotlinx.datetime.test.math
6+
package kotlinx.datetime.test
77
import kotlin.random.*
88
import kotlin.test.*
99
import kotlinx.datetime.*
@@ -55,7 +55,7 @@ class MultiplyAndDivideTest {
5555

5656
@Test
5757
fun randomProductFitsInLong() {
58-
repeat(1000) {
58+
repeat(STRESS_TEST_ITERATIONS) {
5959
val a = Random.nextInt().toLong()
6060
val b = Random.nextInt().toLong()
6161
val m = Random.nextInt(1, Int.MAX_VALUE).toLong()

core/common/test/assertions.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ inline fun <T> assertIllegalArgument(message: String? = null, f: () -> T) {
3434
val result = f()
3535
fail(result.toString())
3636
}
37-
}
37+
}
38+
39+
/**
40+
* The number of iterations to perform in nondeterministic tests.
41+
*/
42+
const val STRESS_TEST_ITERATIONS = 1000

core/darwin/test/ConvertersTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class ConvertersTest {
3131
val gregorianCalendarStart = Instant.parse("1582-10-15T00:00:00Z").toEpochMilliseconds()
3232
val minBoundMillis = (NSDate.distantPast.timeIntervalSince1970 * 1000 + 0.5).toLong()
3333
val maxBoundMillis = (NSDate.distantFuture.timeIntervalSince1970 * 1000 - 0.5).toLong()
34-
repeat (1000) {
34+
repeat(STRESS_TEST_ITERATIONS) {
3535
val millis = Random.nextLong(minBoundMillis, maxBoundMillis)
3636
val instant = Instant.fromEpochMilliseconds(millis)
3737
val date = instant.toNSDate()

core/js/src/LocalTime.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
2727
public actual val second: Int get() = value.second().toInt()
2828
public actual val nanosecond: Int get() = value.nano().toInt()
2929
public actual fun toSecondOfDay(): Int = value.toSecondOfDay().toInt()
30+
public actual fun toMillisecondOfDay(): Int = (value.toNanoOfDay().toDouble() / NANOS_PER_MILLI).toInt()
3031
public actual fun toNanosecondOfDay(): Long = value.toNanoOfDay().toLong()
3132

3233
override fun equals(other: Any?): Boolean =
@@ -52,8 +53,15 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
5253
throw IllegalArgumentException(e)
5354
}
5455

56+
public actual fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime = try {
57+
jtLocalTime.ofNanoOfDay(millisecondOfDay * 1_000_000.0).let(::LocalTime)
58+
} catch (e: Throwable) {
59+
throw IllegalArgumentException(e)
60+
}
61+
5562
public actual fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime = try {
56-
jtLocalTime.ofNanoOfDay(nanosecondOfDay).let(::LocalTime)
63+
// number of nanoseconds in a day is much less than `Number.MAX_SAFE_INTEGER`.
64+
jtLocalTime.ofNanoOfDay(nanosecondOfDay.toDouble()).let(::LocalTime)
5765
} catch (e: Throwable) {
5866
throw IllegalArgumentException(e)
5967
}

core/jvm/src/LocalTime.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
3030
public actual val second: Int get() = value.second
3131
public actual val nanosecond: Int get() = value.nano
3232
public actual fun toSecondOfDay(): Int = value.toSecondOfDay()
33+
public actual fun toMillisecondOfDay(): Int = (value.toNanoOfDay() / NANOS_PER_MILLI).toInt()
3334
public actual fun toNanosecondOfDay(): Long = value.toNanoOfDay()
3435

3536
override fun equals(other: Any?): Boolean =
@@ -54,6 +55,12 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
5455
throw IllegalArgumentException(e)
5556
}
5657

58+
public actual fun fromMillisecondOfDay(millisecondOfDay: Int): LocalTime = try {
59+
jtLocalTime.ofNanoOfDay(millisecondOfDay * 1_000_000L).let(::LocalTime)
60+
} catch (e: Throwable) {
61+
throw IllegalArgumentException(e)
62+
}
63+
5764
public actual fun fromNanosecondOfDay(nanosecondOfDay: Long): LocalTime = try {
5865
jtLocalTime.ofNanoOfDay(nanosecondOfDay).let(::LocalTime)
5966
} catch (e: DateTimeException) {

core/jvm/test/ConvertersTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class ConvertersTest {
3030
assertEquals(jtInstant, ktInstant.toString().let(JTInstant::parse))
3131
}
3232

33-
repeat(1000) {
33+
repeat(STRESS_TEST_ITERATIONS) {
3434
val seconds = Random.nextLong(1_000_000_000_000)
3535
val nanos = Random.nextInt()
3636
test(seconds, nanos)
@@ -70,7 +70,7 @@ class ConvertersTest {
7070
assertEquals(jtDateTime, ktDateTime.toString().let(JTLocalDateTime::parse))
7171
}
7272

73-
repeat(1000) {
73+
repeat(STRESS_TEST_ITERATIONS) {
7474
test(randomDateTime())
7575
}
7676
}
@@ -87,7 +87,7 @@ class ConvertersTest {
8787
assertEquals(jtTime, ktTime.toString().let(JTLocalTime::parse))
8888
}
8989

90-
repeat(1000) {
90+
repeat(STRESS_TEST_ITERATIONS) {
9191
test(randomTime())
9292
}
9393
}
@@ -104,7 +104,7 @@ class ConvertersTest {
104104
assertEquals(jtDate, ktDate.toString().let(JTLocalDate::parse))
105105
}
106106

107-
repeat(1000) {
107+
repeat(STRESS_TEST_ITERATIONS) {
108108
test(randomDate())
109109
}
110110
}
@@ -128,7 +128,7 @@ class ConvertersTest {
128128
assertJtPeriodNormalizedEquals(jtPeriod, ktPeriod.toString().let(JTPeriod::parse))
129129
}
130130

131-
repeat(1000) {
131+
repeat(STRESS_TEST_ITERATIONS) {
132132
test(Random.nextInt(-1000, 1000), Random.nextInt(-1000, 1000), Random.nextInt(-1000, 1000))
133133
}
134134
}

0 commit comments

Comments
 (0)