Skip to content

Commit 3da3dd6

Browse files
committed
Ensure UtcOffset.parse throws DateTimeFormatException
Extract some former ZoneOffset tests into common UtcOffset tests
1 parent 651f254 commit 3da3dd6

File tree

4 files changed

+138
-57
lines changed

4 files changed

+138
-57
lines changed

core/common/test/TimeZoneTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ class TimeZoneTest {
5757

5858
assertFailsWith<IllegalTimeZoneException> { TimeZone.of("Mars/Standard") }
5959
assertFailsWith<IllegalTimeZoneException> { TimeZone.of("UTC+X") }
60+
}
6061

62+
@Test
63+
fun ofFailsOnInvalidOffset() {
64+
for (v in UtcOffsetTest.invalidUtcOffsetStrings) {
65+
assertFailsWith<IllegalTimeZoneException> { TimeZone.of(v) }
66+
}
6167
}
6268

6369
// from 310bp

core/common/test/UtcOffsetTest.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
6+
package kotlinx.datetime.test
7+
8+
import kotlinx.datetime.*
9+
import kotlin.math.abs
10+
import kotlin.test.*
11+
12+
class UtcOffsetTest {
13+
14+
companion object {
15+
val invalidUtcOffsetStrings = listOf(
16+
"", *('A'..'Y').map { it.toString() }.toTypedArray(), "ZZ",
17+
"0", "+0:00", "+00:0", "+0:0",
18+
"+000", "+00000",
19+
"+0:00:00", "+00:0:00", "+00:00:0", "+0:0:0", "+0:0:00", "+00:0:0", "+0:00:0",
20+
"1", "+01_00", "+01;00", "+01@00", "+01:AA",
21+
"+19", "+19:00", "+18:01", "+18:00:01", "+1801", "+180001",
22+
"-0:00", "-00:0", "-0:0",
23+
"-000", "-00000",
24+
"-0:00:00", "-00:0:00", "-00:00:0", "-0:0:0", "-0:0:00", "-00:0:0", "-0:00:0",
25+
"-19", "-19:00", "-18:01", "-18:00:01", "-1801", "-180001",
26+
"-01_00", "-01;00", "-01@00", "-01:AA",
27+
"@01:00")
28+
29+
val fixedOffsetTimeZoneIds = listOf(
30+
"UTC", "UTC+0", "GMT+01", "UT-01", "Etc/UTC"
31+
)
32+
33+
val offsetSecondsRange = -18 * 60 * 60 .. +18 * 60 * 60
34+
}
35+
36+
@Test
37+
fun invalidUtcOffsetStrings() {
38+
for (v in invalidUtcOffsetStrings) {
39+
assertFailsWith<DateTimeFormatException>("Should fail: $v") { UtcOffset.parse(v) }
40+
}
41+
for (v in fixedOffsetTimeZoneIds) {
42+
assertFailsWith<DateTimeFormatException>("Time zone name should not be parsed as UtcOffset: $v") { UtcOffset.parse(v) }
43+
}
44+
}
45+
46+
@Test
47+
fun parseAllValidValues() {
48+
fun Int.pad() = toString().padStart(2, '0')
49+
fun check(offsetSeconds: Int, offsetString: String) {
50+
val offset = UtcOffset.parse(offsetString)
51+
if (offsetSeconds != offset.totalSeconds) {
52+
fail("Expected string $offsetString to be parsed as $offset and have $offsetSeconds offset, got ${offset.totalSeconds}")
53+
}
54+
}
55+
56+
for (offsetSeconds in offsetSecondsRange) {
57+
val sign = when {
58+
offsetSeconds < 0 -> "-"
59+
else -> "+"
60+
}
61+
val hours = abs(offsetSeconds / 60 / 60)
62+
val minutes = abs(offsetSeconds / 60 % 60)
63+
val seconds = abs(offsetSeconds % 60)
64+
65+
66+
check(offsetSeconds, "$sign${hours.pad()}:${minutes.pad()}:${seconds.pad()}")
67+
check(offsetSeconds, "$sign${hours.pad()}${minutes.pad()}${seconds.pad()}")
68+
if (seconds == 0) {
69+
check(offsetSeconds, "$sign${hours.pad()}:${minutes.pad()}")
70+
check(offsetSeconds, "$sign${hours.pad()}${minutes.pad()}")
71+
if (minutes == 0) {
72+
check(offsetSeconds, "$sign${hours.pad()}")
73+
check(offsetSeconds, "$sign$hours")
74+
}
75+
}
76+
}
77+
check(0, "+00:00")
78+
check(0, "-00:00")
79+
check(0, "+0")
80+
check(0, "-0")
81+
check(0, "Z")
82+
}
83+
84+
@Test
85+
fun asTimeZone() {
86+
val offset = UtcOffset.parse("+01:20:30")
87+
val timeZone = offset.asTimeZone()
88+
assertIs<FixedOffsetTimeZone>(timeZone)
89+
assertEquals(offset, timeZone.utcOffset)
90+
}
91+
}

core/native/src/TimeZone.kt

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,32 @@ public actual open class TimeZone internal constructor(internal val value: TimeZ
3333
if (zoneId.length == 1) {
3434
throw IllegalTimeZoneException("Invalid zone ID: $zoneId")
3535
}
36-
if (zoneId.startsWith("+") || zoneId.startsWith("-")) {
37-
return UtcOffset.parse(zoneId).asTimeZone()
38-
}
39-
if (zoneId == "UTC" || zoneId == "GMT" || zoneId == "UT") {
40-
return FixedOffsetTimeZone(UtcOffset(0), zoneId)
41-
}
42-
if (zoneId.startsWith("UTC+") || zoneId.startsWith("GMT+") ||
43-
zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")) {
44-
val prefix = zoneId.take(3)
45-
val offset = UtcOffset.parse(zoneId.substring(3))
46-
return when (offset.totalSeconds) {
47-
0 -> FixedOffsetTimeZone(offset, prefix)
48-
else -> FixedOffsetTimeZone(offset, "$prefix$offset")
36+
try {
37+
if (zoneId.startsWith("+") || zoneId.startsWith("-")) {
38+
return UtcOffset.parse(zoneId).asTimeZone()
4939
}
50-
}
51-
if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) {
52-
val offset = UtcOffset.parse(zoneId.substring(2))
53-
return when (offset.totalSeconds) {
54-
0 -> FixedOffsetTimeZone(offset, "UT")
55-
else -> FixedOffsetTimeZone(offset, "UT$offset")
40+
if (zoneId == "UTC" || zoneId == "GMT" || zoneId == "UT") {
41+
return FixedOffsetTimeZone(UtcOffset(0), zoneId)
42+
}
43+
if (zoneId.startsWith("UTC+") || zoneId.startsWith("GMT+") ||
44+
zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")
45+
) {
46+
val prefix = zoneId.take(3)
47+
val offset = UtcOffset.parse(zoneId.substring(3))
48+
return when (offset.totalSeconds) {
49+
0 -> FixedOffsetTimeZone(offset, prefix)
50+
else -> FixedOffsetTimeZone(offset, "$prefix$offset")
51+
}
52+
}
53+
if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) {
54+
val offset = UtcOffset.parse(zoneId.substring(2))
55+
return when (offset.totalSeconds) {
56+
0 -> FixedOffsetTimeZone(offset, "UT")
57+
else -> FixedOffsetTimeZone(offset, "UT$offset")
58+
}
5659
}
60+
} catch (e: DateTimeFormatException) {
61+
throw IllegalTimeZoneException(e)
5762
}
5863
return TimeZone(PlatformTimeZoneImpl.of(zoneId))
5964
}
@@ -154,11 +159,11 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
154159
minutes = parseNumber(offsetString, 4, true)
155160
seconds = parseNumber(offsetString, 7, true)
156161
}
157-
else -> throw IllegalTimeZoneException("Invalid ID for UtcOffset, invalid format: $offsetString")
162+
else -> throw DateTimeFormatException("Invalid ID for UtcOffset, invalid format: $offsetString")
158163
}
159164
val first: Char = offsetString[0]
160165
if (first != '+' && first != '-') {
161-
throw IllegalTimeZoneException(
166+
throw DateTimeFormatException(
162167
"Invalid ID for UtcOffset, plus/minus not found when expected: $offsetString")
163168
}
164169
return if (first == '-') {
@@ -171,30 +176,30 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
171176
// org.threeten.bp.ZoneOffset#validate
172177
private fun validate(hours: Int, minutes: Int, seconds: Int) {
173178
if (hours < -18 || hours > 18) {
174-
throw IllegalTimeZoneException("Zone offset hours not in valid range: value " + hours +
179+
throw DateTimeFormatException("Zone offset hours not in valid range: value " + hours +
175180
" is not in the range -18 to 18")
176181
}
177182
if (hours > 0) {
178183
if (minutes < 0 || seconds < 0) {
179-
throw IllegalTimeZoneException("Zone offset minutes and seconds must be positive because hours is positive")
184+
throw DateTimeFormatException("Zone offset minutes and seconds must be positive because hours is positive")
180185
}
181186
} else if (hours < 0) {
182187
if (minutes > 0 || seconds > 0) {
183-
throw IllegalTimeZoneException("Zone offset minutes and seconds must be negative because hours is negative")
188+
throw DateTimeFormatException("Zone offset minutes and seconds must be negative because hours is negative")
184189
}
185190
} else if (minutes > 0 && seconds < 0 || minutes < 0 && seconds > 0) {
186-
throw IllegalTimeZoneException("Zone offset minutes and seconds must have the same sign")
191+
throw DateTimeFormatException("Zone offset minutes and seconds must have the same sign")
187192
}
188193
if (abs(minutes) > 59) {
189-
throw IllegalTimeZoneException("Zone offset minutes not in valid range: abs(value) " +
194+
throw DateTimeFormatException("Zone offset minutes not in valid range: abs(value) " +
190195
abs(minutes) + " is not in the range 0 to 59")
191196
}
192197
if (abs(seconds) > 59) {
193-
throw IllegalTimeZoneException("Zone offset seconds not in valid range: abs(value) " +
198+
throw DateTimeFormatException("Zone offset seconds not in valid range: abs(value) " +
194199
abs(seconds) + " is not in the range 0 to 59")
195200
}
196201
if (abs(hours) == 18 && (abs(minutes) > 0 || abs(seconds) > 0)) {
197-
throw IllegalTimeZoneException("Zone offset not in valid range: -18:00 to +18:00")
202+
throw DateTimeFormatException("Utc offset not in valid range: -18:00 to +18:00")
198203
}
199204
}
200205

@@ -216,12 +221,12 @@ public actual class UtcOffset internal constructor(public actual val totalSecond
216221
// org.threeten.bp.ZoneOffset#parseNumber
217222
private fun parseNumber(offsetId: CharSequence, pos: Int, precededByColon: Boolean): Int {
218223
if (precededByColon && offsetId[pos - 1] != ':') {
219-
throw IllegalTimeZoneException("Invalid ID for ZoneOffset, colon not found when expected: $offsetId")
224+
throw DateTimeFormatException("Invalid ID for UtcOffset, colon not found when expected: $offsetId")
220225
}
221226
val ch1 = offsetId[pos]
222227
val ch2 = offsetId[pos + 1]
223228
if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
224-
throw IllegalTimeZoneException("Invalid ID for ZoneOffset, non numeric characters found: $offsetId")
229+
throw DateTimeFormatException("Invalid ID for UtcOffset, non numeric characters found: $offsetId")
225230
}
226231
return (ch1 - '0') * 10 + (ch2 - '0')
227232
}

core/native/test/ThreeTenBpTimeZoneTest.kt

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,7 @@ class ThreeTenBpTimeZoneTest {
4040
}
4141
}
4242

43-
@Test
44-
fun invalidZoneOffsetNames() {
45-
val values = arrayOf(
46-
"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
47-
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "ZZ",
48-
"0", "+0:00", "+00:0", "+0:0",
49-
"+000", "+00000",
50-
"+0:00:00", "+00:0:00", "+00:00:0", "+0:0:0", "+0:0:00", "+00:0:0", "+0:00:0",
51-
"1", "+01_00", "+01;00", "+01@00", "+01:AA",
52-
"+19", "+19:00", "+18:01", "+18:00:01", "+1801", "+180001",
53-
"-0:00", "-00:0", "-0:0",
54-
"-000", "-00000",
55-
"-0:00:00", "-00:0:00", "-00:00:0", "-0:0:0", "-0:0:00", "-00:0:0", "-0:00:0",
56-
"-19", "-19:00", "-18:01", "-18:00:01", "-1801", "-180001",
57-
"-01_00", "-01;00", "-01@00", "-01:AA",
58-
"@01:00")
59-
for (v in values) {
60-
assertFailsWith(IllegalTimeZoneException::class, "should fail: $v") { UtcOffset.parse(v) }
61-
}
62-
}
63-
64-
private fun zoneOffsetCheck(offset: UtcOffset, hours: Int, minutes: Int, seconds: Int) {
43+
private fun utcOffsetCheck(offset: UtcOffset, hours: Int, minutes: Int, seconds: Int) {
6544
assertEquals(offset.totalSeconds, hours * 60 * 60 + minutes * 60 + seconds)
6645
val id: String
6746
if (hours == 0 && minutes == 0 && seconds == 0) {
@@ -84,7 +63,7 @@ class ThreeTenBpTimeZoneTest {
8463
}
8564

8665
@Test
87-
fun zoneOffsetEquals() {
66+
fun utcOffsetEquals() {
8867
val offset1 = UtcOffset.ofHoursMinutesSeconds(1, 2, 3)
8968
val offset2 = UtcOffset.ofHoursMinutesSeconds(2, 3, 4)
9069
val offset2b = UtcOffset.ofHoursMinutesSeconds(2, 3, 4)
@@ -99,7 +78,7 @@ class ThreeTenBpTimeZoneTest {
9978
}
10079

10180
@Test
102-
fun zoneOffsetParsingFullForm() {
81+
fun utcOffsetParsingFullForm() {
10382
for (i in -17..17) {
10483
for (j in -59..59) {
10584
for (k in -59..59) {
@@ -110,15 +89,15 @@ class ThreeTenBpTimeZoneTest {
11089
(abs(j) + 100).toString().substring(1) + ":" +
11190
(abs(k) + 100).toString().substring(1)
11291
val test = UtcOffset.parse(str)
113-
zoneOffsetCheck(test, i, j, k)
92+
utcOffsetCheck(test, i, j, k)
11493
}
11594
}
11695
}
11796
}
11897
val test1 = UtcOffset.parse("-18:00:00")
119-
zoneOffsetCheck(test1, -18, 0, 0)
98+
utcOffsetCheck(test1, -18, 0, 0)
12099
val test2 = UtcOffset.parse("+18:00:00")
121-
zoneOffsetCheck(test2, 18, 0, 0)
100+
utcOffsetCheck(test2, 18, 0, 0)
122101
}
123102

124103
@Test

0 commit comments

Comments
 (0)