Skip to content

Commit a35ae66

Browse files
committed
UtcOffset construction from components
1 parent cadd19f commit a35ae66

10 files changed

+156
-41
lines changed

core/common/src/UtcOffset.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ public expect class UtcOffset {
1717
public fun parse(offsetString: String): UtcOffset
1818
}
1919
}
20+
public expect fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset
21+
22+
@Deprecated("Use UtcOffset.ZERO instead", ReplaceWith("UtcOffset.ZERO"), DeprecationLevel.ERROR)
23+
public fun UtcOffset(): UtcOffset = UtcOffset.ZERO
2024

2125
public fun UtcOffset.asTimeZone(): FixedOffsetTimeZone = FixedOffsetTimeZone(this)

core/common/test/InstantTest.kt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -611,22 +611,3 @@ class InstantRangeTest {
611611
}
612612
}
613613

614-
615-
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
616-
@kotlin.internal.InlineOnly
617-
inline fun <T> assertArithmeticFails(message: String? = null, f: () -> T) {
618-
assertFailsWith<DateTimeArithmeticException>(message) {
619-
val result = f()
620-
fail(result.toString())
621-
}
622-
}
623-
624-
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
625-
@kotlin.internal.InlineOnly
626-
inline fun <T> assertInvalidFormat(message: String? = null, f: () -> T) {
627-
assertFailsWith<DateTimeFormatException>(message) {
628-
val result = f()
629-
fail(result.toString())
630-
}
631-
}
632-

core/common/test/UtcOffsetTest.kt

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,56 @@ class UtcOffsetTest {
3333
val offsetSecondsRange = -18 * 60 * 60 .. +18 * 60 * 60
3434
}
3535

36+
37+
@Test
38+
fun construction() {
39+
for (totalSeconds in offsetSecondsRange) {
40+
val hours = totalSeconds / (60 * 60)
41+
val totalMinutes = totalSeconds / 60
42+
val minutes = totalMinutes % 60
43+
val seconds = totalSeconds % 60
44+
val offset = UtcOffset(hours, minutes, seconds)
45+
val offsetSeconds = UtcOffset(seconds = totalSeconds)
46+
val offsetMinutes = UtcOffset(minutes = totalMinutes, seconds = seconds)
47+
assertEquals(totalSeconds, offset.totalSeconds)
48+
assertEquals(offset, offsetMinutes)
49+
assertEquals(offset, offsetSeconds)
50+
}
51+
}
52+
53+
@Test
54+
fun constructionErrors() {
55+
// total range
56+
assertIllegalArgument { UtcOffset(hours = -19) }
57+
assertIllegalArgument { UtcOffset(hours = +19) }
58+
assertIllegalArgument { UtcOffset(hours = -18, minutes = -1) }
59+
assertIllegalArgument { UtcOffset(hours = -18, seconds = -1) }
60+
assertIllegalArgument { UtcOffset(hours = +18, seconds = +1) }
61+
assertIllegalArgument { UtcOffset(hours = +18, seconds = +1) }
62+
assertIllegalArgument { UtcOffset(seconds = offsetSecondsRange.first - 1) }
63+
assertIllegalArgument { UtcOffset(seconds = offsetSecondsRange.last + 1) }
64+
// component ranges
65+
assertIllegalArgument { UtcOffset(hours = 0, minutes = 60) }
66+
assertIllegalArgument { UtcOffset(hours = 0, seconds = -60) }
67+
assertIllegalArgument { UtcOffset(minutes = 90, seconds = 90) }
68+
assertIllegalArgument { UtcOffset(minutes = 0, seconds = 90) }
69+
// component signs
70+
assertIllegalArgument { UtcOffset(hours = +1, minutes = -1) }
71+
assertIllegalArgument { UtcOffset(hours = +1, seconds = -1) }
72+
assertIllegalArgument { UtcOffset(hours = -1, minutes = +1) }
73+
assertIllegalArgument { UtcOffset(hours = -1, seconds = +1) }
74+
assertIllegalArgument { UtcOffset(minutes = +1, seconds = -1) }
75+
assertIllegalArgument { UtcOffset(minutes = -1, seconds = +1) }
76+
}
77+
78+
@Test
79+
fun utcOffsetToString() {
80+
assertEquals("+01:00", UtcOffset(hours = 1, minutes = 0, seconds = 0).toString())
81+
assertEquals("+01:02:03", UtcOffset(hours = 1, minutes = 2, seconds = 3).toString())
82+
assertEquals("-01:00:30", UtcOffset(hours = -1, minutes = 0, seconds = -30).toString())
83+
assertEquals("Z", UtcOffset.ZERO.toString())
84+
}
85+
3686
@Test
3787
fun invalidUtcOffsetStrings() {
3888
for (v in invalidUtcOffsetStrings) {
@@ -112,7 +162,7 @@ class UtcOffsetTest {
112162

113163
@Test
114164
fun asTimeZone() {
115-
val offset = UtcOffset.parse("+01:20:30")
165+
val offset = UtcOffset(hours = 1, minutes = 20, seconds = 30)
116166
val timeZone = offset.asTimeZone()
117167
assertIs<FixedOffsetTimeZone>(timeZone)
118168
assertEquals(offset, timeZone.offset)

core/common/test/assertions.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2019-2021 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+
package kotlinx.datetime.test
6+
7+
import kotlinx.datetime.DateTimeArithmeticException
8+
import kotlinx.datetime.DateTimeFormatException
9+
import kotlin.test.assertFailsWith
10+
import kotlin.test.fail
11+
12+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
13+
@kotlin.internal.InlineOnly
14+
inline fun <T> assertArithmeticFails(message: String? = null, f: () -> T) {
15+
assertFailsWith<DateTimeArithmeticException>(message) {
16+
val result = f()
17+
fail(result.toString())
18+
}
19+
}
20+
21+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
22+
@kotlin.internal.InlineOnly
23+
inline fun <T> assertInvalidFormat(message: String? = null, f: () -> T) {
24+
assertFailsWith<DateTimeFormatException>(message) {
25+
val result = f()
26+
fail(result.toString())
27+
}
28+
}
29+
30+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
31+
@kotlin.internal.InlineOnly
32+
inline fun <T> assertIllegalArgument(message: String? = null, f: () -> T) {
33+
assertFailsWith<IllegalArgumentException>(message) {
34+
val result = f()
35+
fail(result.toString())
36+
}
37+
}

core/js/src/UtcOffset.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,20 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
2828
throw e
2929
}
3030
}
31-
}
31+
}
32+
33+
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
34+
public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset =
35+
try {
36+
when {
37+
hours != null ->
38+
UtcOffset(ZoneOffset.ofHoursMinutesSeconds(hours, minutes ?: 0, seconds ?: 0))
39+
minutes != null ->
40+
UtcOffset(ZoneOffset.ofHoursMinutesSeconds(minutes / 60, minutes % 60, seconds ?: 0))
41+
else -> {
42+
UtcOffset(ZoneOffset.ofTotalSeconds(seconds ?: 0))
43+
}
44+
}
45+
} catch (e: Throwable) {
46+
if (e.isJodaDateTimeException()) throw IllegalArgumentException(e) else throw e
47+
}

core/jvm/src/UtcOffsetJvm.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,20 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
2828
throw DateTimeFormatException(e)
2929
}
3030
}
31-
}
31+
}
32+
33+
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
34+
public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset =
35+
try {
36+
when {
37+
hours != null ->
38+
UtcOffset(ZoneOffset.ofHoursMinutesSeconds(hours, minutes ?: 0, seconds ?: 0))
39+
minutes != null ->
40+
UtcOffset(ZoneOffset.ofHoursMinutesSeconds(minutes / 60, minutes % 60, seconds ?: 0))
41+
else -> {
42+
UtcOffset(ZoneOffset.ofTotalSeconds(seconds ?: 0))
43+
}
44+
}
45+
} catch (e: DateTimeException) {
46+
throw IllegalArgumentException(e)
47+
}

core/native/src/TimeZone.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public actual open class TimeZone internal constructor(internal val value: TimeZ
3434
return UtcOffset.parse(zoneId).asTimeZone()
3535
}
3636
if (zoneId == "UTC" || zoneId == "GMT" || zoneId == "UT") {
37-
return FixedOffsetTimeZone(UtcOffset(0), zoneId)
37+
return FixedOffsetTimeZone(UtcOffset.ZERO, zoneId)
3838
}
3939
if (zoneId.startsWith("UTC+") || zoneId.startsWith("GMT+") ||
4040
zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")

core/native/src/UtcOffset.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.math.abs
1111
import kotlin.native.concurrent.ThreadLocal
1212

1313
@Serializable(with = UtcOffsetSerializer::class)
14-
public actual class UtcOffset internal constructor(public actual val totalSeconds: Int) {
14+
public actual class UtcOffset private constructor(public actual val totalSeconds: Int) {
1515
private val id: String = zoneIdByOffset(totalSeconds)
1616

1717
override fun hashCode(): Int = totalSeconds
@@ -20,7 +20,7 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
2020

2121
public actual companion object {
2222

23-
public actual val ZERO: UtcOffset = UtcOffset(0)
23+
public actual val ZERO: UtcOffset = UtcOffset(totalSeconds = 0)
2424

2525
public actual fun parse(offsetString: String): UtcOffset {
2626
if (offsetString == "Z") {
@@ -76,6 +76,12 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
7676
}
7777
}
7878

79+
private fun validateTotal(totalSeconds: Int) {
80+
if (totalSeconds !in -18 * SECONDS_PER_HOUR .. 18 * SECONDS_PER_HOUR) {
81+
throw IllegalArgumentException("Total seconds value is out of range: $totalSeconds")
82+
}
83+
}
84+
7985
// org.threeten.bp.ZoneOffset#validate
8086
private fun validate(hours: Int, minutes: Int, seconds: Int) {
8187
if (hours < -18 || hours > 18) {
@@ -114,12 +120,14 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
114120
}
115121

116122
// org.threeten.bp.ZoneOffset#ofTotalSeconds
117-
internal fun ofSeconds(seconds: Int): UtcOffset =
118-
if (seconds % (15 * SECONDS_PER_MINUTE) == 0) {
119-
utcOffsetCache[seconds] ?: UtcOffset(seconds).also { utcOffsetCache[seconds] = it }
123+
internal fun ofSeconds(seconds: Int): UtcOffset {
124+
validateTotal(seconds)
125+
return if (seconds % (15 * SECONDS_PER_MINUTE) == 0) {
126+
utcOffsetCache[seconds] ?: UtcOffset(totalSeconds = seconds).also { utcOffsetCache[seconds] = it }
120127
} else {
121-
UtcOffset(seconds)
128+
UtcOffset(totalSeconds = seconds)
122129
}
130+
}
123131

124132
// org.threeten.bp.ZoneOffset#parseNumber
125133
private fun parseNumber(offsetId: CharSequence, pos: Int, precededByColon: Boolean): Int {
@@ -138,3 +146,16 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
138146

139147
@ThreadLocal
140148
private var utcOffsetCache: MutableMap<Int, UtcOffset> = mutableMapOf()
149+
150+
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
151+
public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: Int? = null): UtcOffset =
152+
when {
153+
hours != null ->
154+
UtcOffset.ofHoursMinutesSeconds(hours, minutes ?: 0, seconds ?: 0)
155+
minutes != null ->
156+
UtcOffset.ofHoursMinutesSeconds(minutes / MINUTES_PER_HOUR, minutes % MINUTES_PER_HOUR, seconds ?: 0)
157+
else -> {
158+
UtcOffset.ofSeconds(seconds ?: 0)
159+
}
160+
}
161+

core/native/test/ThreeTenBpLocalDateTimeTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ThreeTenBpLocalDateTimeTest {
1515
fun toSecondsAfterEpoch() {
1616
for (i in -5..4) {
1717
val iHours = i * 3600
18-
val offset = UtcOffset.ofSeconds(iHours)
18+
val offset = UtcOffset(seconds = iHours)
1919
for (j in 0..99999) {
2020
val a = LocalDateTime(1970, 1, 1, 0, 0, 0, 0).plusSeconds(j)
2121
assertEquals((j - iHours).toLong(), a.toEpochSecond(offset))

core/native/test/ThreeTenBpTimeZoneTest.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@ import kotlin.test.*
1818
*/
1919
class ThreeTenBpTimeZoneTest {
2020

21-
@Test
22-
fun utcOffsetToString() {
23-
var offset: UtcOffset = UtcOffset.ofHoursMinutesSeconds(1, 0, 0)
24-
assertEquals("+01:00", offset.toString())
25-
offset = UtcOffset.ofHoursMinutesSeconds(1, 2, 3)
26-
assertEquals("+01:02:03", offset.toString())
27-
offset = UtcOffset.ZERO
28-
assertEquals("Z", offset.toString())
29-
}
30-
3121
@Test
3222
fun utcIsCached() {
3323
val values = arrayOf(
@@ -53,7 +43,7 @@ class ThreeTenBpTimeZoneTest {
5343
val t = LocalDateTime(2007, 10, 28, 2, 30, 0, 0)
5444
val zone = TimeZone.of("Europe/Paris")
5545
assertEquals(ZonedDateTime(LocalDateTime(2007, 10, 28, 2, 30, 0, 0),
56-
zone, UtcOffset.ofSeconds(2 * 3600)), zone.atZone(t))
46+
zone, UtcOffset(seconds = 2 * 3600)), zone.atZone(t))
5747
}
5848

5949
}

0 commit comments

Comments
 (0)