Skip to content

Commit f83b355

Browse files
committed
Only allow the ISO extended format in UtcOffset.parse
1 parent aee5136 commit f83b355

File tree

10 files changed

+108
-93
lines changed

10 files changed

+108
-93
lines changed

core/common/src/UtcOffset.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public expect class UtcOffset {
4141
*
4242
* Examples of valid strings:
4343
* - `Z` or `+00:00`, an offset of zero;
44-
* - `+05`, five hours;
45-
* - `-02`, minus two hours;
44+
* - `+05:00`, five hours;
45+
* - `-02:00`, minus two hours;
4646
* - `+03:30`, three hours and thirty minutes;
4747
* - `+01:23:45`, an hour, 23 minutes, and 45 seconds.
4848
*/

core/common/test/InstantTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ class InstantTest {
124124
UtcOffset.parse("-03:12:14"),
125125
UtcOffset.parse("+02:35"),
126126
UtcOffset.parse("-02:35"),
127-
UtcOffset.parse("+04"),
128-
UtcOffset.parse("-04"),
127+
UtcOffset.parse("+04:00"),
128+
UtcOffset.parse("-04:00"),
129129
)
130130

131131
for (instant in instants) {

core/common/test/TimeZoneTest.kt

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -138,80 +138,80 @@ class TimeZoneTest {
138138
@Test
139139
fun newYorkOffset() {
140140
val test = TimeZone.of("America/New_York")
141-
val offset = UtcOffset.parse("-5")
141+
val offset = UtcOffset(hours = -5)
142142

143-
fun check(expectedOffset: String, dateTime: LocalDateTime) {
144-
assertEquals(UtcOffset.parse(expectedOffset), dateTime.toInstant(offset).offsetIn(test))
143+
fun check(expectedHours: Int, dateTime: LocalDateTime) {
144+
assertEquals(UtcOffset(hours = expectedHours), dateTime.toInstant(offset).offsetIn(test))
145145
}
146146

147-
check("-5", LocalDateTime(2008, 1, 1))
148-
check("-5", LocalDateTime(2008, 2, 1))
149-
check("-5", LocalDateTime(2008, 3, 1))
150-
check("-4", LocalDateTime(2008, 4, 1))
151-
check("-4", LocalDateTime(2008, 5, 1))
152-
check("-4", LocalDateTime(2008, 6, 1))
153-
check("-4", LocalDateTime(2008, 7, 1))
154-
check("-4", LocalDateTime(2008, 8, 1))
155-
check("-4", LocalDateTime(2008, 9, 1))
156-
check("-4", LocalDateTime(2008, 10, 1))
157-
check("-4", LocalDateTime(2008, 11, 1))
158-
check("-5", LocalDateTime(2008, 12, 1))
159-
check("-5", LocalDateTime(2008, 1, 28))
160-
check("-5", LocalDateTime(2008, 2, 28))
161-
check("-4", LocalDateTime(2008, 3, 28))
162-
check("-4", LocalDateTime(2008, 4, 28))
163-
check("-4", LocalDateTime(2008, 5, 28))
164-
check("-4", LocalDateTime(2008, 6, 28))
165-
check("-4", LocalDateTime(2008, 7, 28))
166-
check("-4", LocalDateTime(2008, 8, 28))
167-
check("-4", LocalDateTime(2008, 9, 28))
168-
check("-4", LocalDateTime(2008, 10, 28))
169-
check("-5", LocalDateTime(2008, 11, 28))
170-
check("-5", LocalDateTime(2008, 12, 28))
147+
check(-5, LocalDateTime(2008, 1, 1))
148+
check(-5, LocalDateTime(2008, 2, 1))
149+
check(-5, LocalDateTime(2008, 3, 1))
150+
check(-4, LocalDateTime(2008, 4, 1))
151+
check(-4, LocalDateTime(2008, 5, 1))
152+
check(-4, LocalDateTime(2008, 6, 1))
153+
check(-4, LocalDateTime(2008, 7, 1))
154+
check(-4, LocalDateTime(2008, 8, 1))
155+
check(-4, LocalDateTime(2008, 9, 1))
156+
check(-4, LocalDateTime(2008, 10, 1))
157+
check(-4, LocalDateTime(2008, 11, 1))
158+
check(-5, LocalDateTime(2008, 12, 1))
159+
check(-5, LocalDateTime(2008, 1, 28))
160+
check(-5, LocalDateTime(2008, 2, 28))
161+
check(-4, LocalDateTime(2008, 3, 28))
162+
check(-4, LocalDateTime(2008, 4, 28))
163+
check(-4, LocalDateTime(2008, 5, 28))
164+
check(-4, LocalDateTime(2008, 6, 28))
165+
check(-4, LocalDateTime(2008, 7, 28))
166+
check(-4, LocalDateTime(2008, 8, 28))
167+
check(-4, LocalDateTime(2008, 9, 28))
168+
check(-4, LocalDateTime(2008, 10, 28))
169+
check(-5, LocalDateTime(2008, 11, 28))
170+
check(-5, LocalDateTime(2008, 12, 28))
171171
}
172172

173173
// from 310bp
174174
@Test
175175
fun newYorkOffsetToDST() {
176176
val test = TimeZone.of("America/New_York")
177-
val offset = UtcOffset.parse("-5")
177+
val offset = UtcOffset(hours = -5)
178178

179-
fun check(expectedOffset: String, dateTime: LocalDateTime) {
180-
assertEquals(UtcOffset.parse(expectedOffset), dateTime.toInstant(offset).offsetIn(test))
179+
fun check(expectedHours: Int, dateTime: LocalDateTime) {
180+
assertEquals(UtcOffset(hours = expectedHours), dateTime.toInstant(offset).offsetIn(test))
181181
}
182182

183-
check("-5", LocalDateTime(2008, 3, 8))
184-
check("-5", LocalDateTime(2008, 3, 9))
185-
check("-4", LocalDateTime(2008, 3, 10))
186-
check("-4", LocalDateTime(2008, 3, 11))
187-
check("-4", LocalDateTime(2008, 3, 12))
188-
check("-4", LocalDateTime(2008, 3, 13))
189-
check("-4", LocalDateTime(2008, 3, 14))
183+
check(-5, LocalDateTime(2008, 3, 8))
184+
check(-5, LocalDateTime(2008, 3, 9))
185+
check(-4, LocalDateTime(2008, 3, 10))
186+
check(-4, LocalDateTime(2008, 3, 11))
187+
check(-4, LocalDateTime(2008, 3, 12))
188+
check(-4, LocalDateTime(2008, 3, 13))
189+
check(-4, LocalDateTime(2008, 3, 14))
190190
// cutover at 02:00 local
191-
check("-5", LocalDateTime(2008, 3, 9, 1, 59, 59, 999999999))
192-
check("-4", LocalDateTime(2008, 3, 9, 2, 0, 0, 0))
191+
check(-5, LocalDateTime(2008, 3, 9, 1, 59, 59, 999999999))
192+
check(-4, LocalDateTime(2008, 3, 9, 2, 0, 0, 0))
193193
}
194194

195195
// from 310bp
196196
@Test
197197
fun newYorkOffsetFromDST() {
198198
val test = TimeZone.of("America/New_York")
199-
val offset = UtcOffset.parse("-4")
199+
val offset = UtcOffset(hours = -4)
200200

201-
fun check(expectedOffset: String, dateTime: LocalDateTime) {
202-
assertEquals(UtcOffset.parse(expectedOffset), dateTime.toInstant(offset).offsetIn(test))
201+
fun check(expectedHours: Int, dateTime: LocalDateTime) {
202+
assertEquals(UtcOffset(hours = expectedHours), dateTime.toInstant(offset).offsetIn(test))
203203
}
204204

205-
check("-4", LocalDateTime(2008, 11, 1))
206-
check("-4", LocalDateTime(2008, 11, 2))
207-
check("-5", LocalDateTime(2008, 11, 3))
208-
check("-5", LocalDateTime(2008, 11, 4))
209-
check("-5", LocalDateTime(2008, 11, 5))
210-
check("-5", LocalDateTime(2008, 11, 6))
211-
check("-5", LocalDateTime(2008, 11, 7))
205+
check(-4, LocalDateTime(2008, 11, 1))
206+
check(-4, LocalDateTime(2008, 11, 2))
207+
check(-5, LocalDateTime(2008, 11, 3))
208+
check(-5, LocalDateTime(2008, 11, 4))
209+
check(-5, LocalDateTime(2008, 11, 5))
210+
check(-5, LocalDateTime(2008, 11, 6))
211+
check(-5, LocalDateTime(2008, 11, 7))
212212
// cutover at 02:00 local
213-
check("-4", LocalDateTime(2008, 11, 2, 1, 59, 59, 999999999))
214-
check("-5", LocalDateTime(2008, 11, 2, 2, 0, 0, 0))
213+
check(-4, LocalDateTime(2008, 11, 2, 1, 59, 59, 999999999))
214+
check(-5, LocalDateTime(2008, 11, 2, 2, 0, 0, 0))
215215
}
216216

217217
@Test

core/common/test/UtcOffsetTest.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ class UtcOffsetTest {
2727
"@01:00")
2828

2929
val fixedOffsetTimeZoneIds = listOf(
30-
"UTC", "UTC+0", "GMT+01", "UT-01", "Etc/UTC"
30+
"UTC", "UTC+0", "GMT+01", "UT-01", "Etc/UTC",
31+
"+0000", "+0100", "+1800", "+180000",
32+
"+4", "+0", "-0",
3133
)
3234

3335
val offsetSecondsRange = -18 * 60 * 60 .. +18 * 60 * 60
@@ -123,29 +125,21 @@ class UtcOffsetTest {
123125

124126

125127
check(offsetSeconds, "$sign${hours.pad()}:${minutes.pad()}:${seconds.pad()}", canonical = seconds != 0)
126-
check(offsetSeconds, "$sign${hours.pad()}${minutes.pad()}${seconds.pad()}")
127128
if (seconds == 0) {
128129
check(offsetSeconds, "$sign${hours.pad()}:${minutes.pad()}", canonical = offsetSeconds != 0)
129-
check(offsetSeconds, "$sign${hours.pad()}${minutes.pad()}")
130-
if (minutes == 0) {
131-
check(offsetSeconds, "$sign${hours.pad()}")
132-
check(offsetSeconds, "$sign$hours")
133-
}
134130
}
135131
}
136132
check(0, "+00:00")
137133
check(0, "-00:00")
138-
check(0, "+0")
139-
check(0, "-0")
140134
check(0, "Z", canonical = true)
141135
}
142136

143137
@Test
144138
fun equality() {
145139
val equalOffsets = listOf(
146-
listOf("Z", "+0", "+00", "+0000", "+00:00:00", "-00:00:00"),
147-
listOf("+4", "+04", "+04:00"),
148-
listOf("-18", "-1800", "-18:00:00"),
140+
listOf("Z", "+00:00", "-00:00", "+00:00:00", "-00:00:00"),
141+
listOf("+04:00", "+04:00:00"),
142+
listOf("-18:00", "-18:00:00"),
149143
)
150144
for (equalGroup in equalOffsets) {
151145
val offsets = equalGroup.map { UtcOffset.parse(it) }
@@ -167,4 +161,4 @@ class UtcOffsetTest {
167161
assertIs<FixedOffsetTimeZone>(timeZone)
168162
assertEquals(offset, timeZone.offset)
169163
}
170-
}
164+
}

core/commonJs/src/UtcOffset.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
package kotlinx.datetime
77

88
import kotlinx.datetime.internal.JSJoda.ZoneOffset as jtZoneOffset
9+
import kotlinx.datetime.internal.JSJoda.ChronoField as jtChronoField
10+
import kotlinx.datetime.internal.JSJoda.DateTimeFormatterBuilder as jtDateTimeFormatterBuilder
11+
import kotlinx.datetime.internal.JSJoda.ResolverStyle as jtResolverStyle
12+
import kotlinx.datetime.format.*
913
import kotlinx.datetime.serializers.UtcOffsetSerializer
1014
import kotlinx.serialization.Serializable
1115

@@ -18,15 +22,17 @@ public actual class UtcOffset internal constructor(internal val zoneOffset: jtZo
1822
override fun toString(): String = zoneOffset.toString()
1923

2024
public actual companion object {
25+
private val format = jtDateTimeFormatterBuilder().appendOffsetId().toFormatter(jtResolverStyle.STRICT)
2126

2227
public actual val ZERO: UtcOffset = UtcOffset(jtZoneOffset.UTC)
2328

24-
public actual fun parse(offsetString: String): UtcOffset = try {
25-
jsTry { jtZoneOffset.of(offsetString) }.let(::UtcOffset)
29+
public actual fun parse(offsetString: String): UtcOffset = UtcOffset(seconds = try {
30+
jsTry { format.parse(offsetString).get(jtChronoField.OFFSET_SECONDS) }
2631
} catch (e: Throwable) {
32+
if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e)
2733
if (e.isJodaDateTimeException()) throw DateTimeFormatException(e)
2834
throw e
29-
}
35+
})
3036
}
3137
}
3238

core/jvm/src/UtcOffsetJvm.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.datetime.serializers.UtcOffsetSerializer
99
import kotlinx.serialization.Serializable
1010
import java.time.DateTimeException
1111
import java.time.ZoneOffset
12+
import java.time.format.DateTimeFormatterBuilder
1213

1314
@Serializable(with = UtcOffsetSerializer::class)
1415
public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
@@ -19,11 +20,12 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
1920
override fun toString(): String = zoneOffset.toString()
2021

2122
public actual companion object {
23+
private val format = DateTimeFormatterBuilder().appendOffsetId().toFormatter()
2224

2325
public actual val ZERO: UtcOffset = UtcOffset(ZoneOffset.UTC)
2426

2527
public actual fun parse(offsetString: String): UtcOffset = try {
26-
ZoneOffset.of(offsetString).let(::UtcOffset)
28+
format.parse(offsetString, ZoneOffset::from).let(::UtcOffset)
2729
} catch (e: DateTimeException) {
2830
throw DateTimeFormatException(e)
2931
}

core/native/src/TimeZone.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package kotlinx.datetime
1010

11+
import kotlinx.datetime.format.*
1112
import kotlinx.datetime.internal.*
1213
import kotlinx.datetime.serializers.*
1314
import kotlinx.serialization.Serializable
@@ -40,7 +41,7 @@ public actual open class TimeZone internal constructor() {
4041
}
4142
try {
4243
if (zoneId.startsWith("+") || zoneId.startsWith("-")) {
43-
return UtcOffset.parse(zoneId).asTimeZone()
44+
return lenientOffsetFormat.parse(zoneId).asTimeZone()
4445
}
4546
if (zoneId == "UTC" || zoneId == "GMT" || zoneId == "UT") {
4647
return FixedOffsetTimeZone(UtcOffset.ZERO, zoneId)
@@ -49,14 +50,14 @@ public actual open class TimeZone internal constructor() {
4950
zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")
5051
) {
5152
val prefix = zoneId.take(3)
52-
val offset = UtcOffset.parse(zoneId.substring(3))
53+
val offset = lenientOffsetFormat.parse(zoneId.substring(3))
5354
return when (offset.totalSeconds) {
5455
0 -> FixedOffsetTimeZone(offset, prefix)
5556
else -> FixedOffsetTimeZone(offset, "$prefix$offset")
5657
}
5758
}
5859
if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) {
59-
val offset = UtcOffset.parse(zoneId.substring(2))
60+
val offset = lenientOffsetFormat.parse(zoneId.substring(2))
6061
return when (offset.totalSeconds) {
6162
0 -> FixedOffsetTimeZone(offset, "UT")
6263
else -> FixedOffsetTimeZone(offset, "UT$offset")
@@ -155,3 +156,26 @@ public actual fun LocalDateTime.toInstant(offset: UtcOffset): Instant =
155156

156157
public actual fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant =
157158
timeZone.atStartOfDay(this)
159+
160+
private val lenientOffsetFormat = UtcOffsetFormat.build {
161+
alternativeParsing(
162+
{
163+
offsetHours(Padding.NONE)
164+
},
165+
{
166+
isoOffset(
167+
zOnZero = false,
168+
useSeparator = false,
169+
outputMinute = WhenToOutput.IF_NONZERO,
170+
outputSecond = WhenToOutput.IF_NONZERO
171+
)
172+
}
173+
) {
174+
isoOffset(
175+
zOnZero = true,
176+
useSeparator = true,
177+
outputMinute = WhenToOutput.ALWAYS,
178+
outputSecond = WhenToOutput.IF_NONZERO
179+
)
180+
}
181+
}

core/native/src/UtcOffset.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public actual class UtcOffset private constructor(public actual val totalSeconds
2323

2424
public actual val ZERO: UtcOffset = UtcOffset(totalSeconds = 0)
2525

26-
public actual fun parse(offsetString: String): UtcOffset = lenientFormat.parse(offsetString)
26+
public actual fun parse(offsetString: String): UtcOffset = ISO_OFFSET.parse(offsetString)
2727

2828
private fun validateTotal(totalSeconds: Int) {
2929
if (totalSeconds !in -18 * SECONDS_PER_HOUR .. 18 * SECONDS_PER_HOUR) {
@@ -94,12 +94,3 @@ public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: I
9494
UtcOffset.ofSeconds(seconds ?: 0)
9595
}
9696
}
97-
98-
private val lenientFormat = UtcOffsetFormat.build {
99-
alternativeParsing(
100-
{ offsetHours(Padding.NONE) },
101-
{ offset(ISO_OFFSET_BASIC) }
102-
) {
103-
offset(ISO_OFFSET)
104-
}
105-
}

core/native/test/ThreeTenBpTimeZoneTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package kotlinx.datetime.test
1010

1111
import kotlinx.datetime.*
12+
import kotlinx.datetime.format.*
1213
import kotlin.test.*
1314

1415

@@ -19,14 +20,12 @@ class ThreeTenBpTimeZoneTest {
1920

2021
@Test
2122
fun utcIsCached() {
22-
val values = arrayOf(
23-
"Z", "+0",
24-
"+00", "+0000", "+00:00", "+000000", "+00:00:00",
25-
"-00", "-0000", "-00:00", "-000000", "-00:00:00")
23+
val values = arrayOf("Z", "+00:00", "+00:00:00", "-00:00", "-00:00:00")
2624
for (v in values) {
2725
val test = UtcOffset.parse(v)
2826
assertSame(test, UtcOffset.ZERO)
2927
}
28+
assertSame(UtcOffsetFormat.build { offsetHours(padding = Padding.NONE) }.parse("-0"), UtcOffset.ZERO)
3029
}
3130

3231
@Test

serialization/common/test/UtcOffsetSerializationTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ import kotlin.test.*
1515
class UtcOffsetSerializationTest {
1616

1717
private fun testSerializationAsPrimitive(serializer: KSerializer<UtcOffset>) {
18-
val offset2h = UtcOffset.parse("+2")
18+
val offset2h = UtcOffset(hours = 2)
1919
assertEquals("\"+02:00\"", Json.encodeToString(serializer, offset2h))
2020
assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02:00\""))
21-
assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02\""))
22-
assertEquals(offset2h, Json.decodeFromString(serializer, "\"+2\""))
21+
assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02:00:00\""))
2322

2423
assertFailsWith<IllegalArgumentException> {
2524
Json.decodeFromString(serializer, "\"UTC+02:00\"") // not an offset
@@ -36,4 +35,4 @@ class UtcOffsetSerializationTest {
3635
testSerializationAsPrimitive(UtcOffsetSerializer)
3736
testSerializationAsPrimitive(UtcOffset.serializer())
3837
}
39-
}
38+
}

0 commit comments

Comments
 (0)