Skip to content

Commit 3e11ed8

Browse files
committed
Fixes
1 parent e1952e7 commit 3e11ed8

File tree

5 files changed

+119
-75
lines changed

5 files changed

+119
-75
lines changed

core/common/src/Instant.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ public expect class Instant : Comparable<Instant> {
125125
* Parses a string that represents an instant in ISO-8601 format including date and time components and
126126
* the mandatory time zone offset and returns the parsed [Instant] value.
127127
*
128-
* In addition to allowing the `Z` designator of the UTC+0 time zone to be passed as the time zone offset, which
129-
* is required by the ISO-8601, this parser also accepts the other possible offsets, but does not provide a way
130-
* to query the result for which offset was specified in the string.
128+
* Supports the following ways of specifying the time zone offset:
129+
* - the `Z` designator for the UTC+0 time zone,
130+
* - a custom time zone offset specified with `+hh`, or `+hh:mm`, or `+hh:mm:ss`
131+
* (with `+` being replaced with `-` for the negative offsets)
131132
*
132133
* Examples of instants in the ISO-8601 format:
133134
* - `2020-08-30T18:43:00Z`
@@ -476,3 +477,5 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long =
476477

477478
internal const val DISTANT_PAST_SECONDS = -3217862419201
478479
internal const val DISTANT_FUTURE_SECONDS = 3093527980800
480+
481+
internal expect fun Instant.toStringWithOffset(offset: ZoneOffset): String

core/common/test/InstantTest.kt

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,47 @@ class InstantTest {
8787

8888
@Test
8989
fun parseStringsWithOffsets() {
90-
val instants = arrayOf(
91-
Triple("2020-01-01T00:01:01.02+18:00", 1577772061L, 20_000_000),
92-
Triple("2020-01-01T00:01:01.123456789-17:59:59", 1577901660L, 123456789),
93-
Triple("2020-01-01T00:01:01.010203040+17:59:59", 1577772062L, 10203040),
94-
Triple("2020-01-01T00:01:01.010203040+17:59", 1577772121L, 10203040),
95-
// Triple("2020-01-01T00:01:01+00", 1577836861L, 0), // fails on JS, passes everywhere else
90+
val strings = arrayOf(
91+
Pair("2020-01-01T00:01:01.02+18:00", "2019-12-31T06:01:01.020Z"),
92+
Pair("2020-01-01T00:01:01.123456789-17:59:59", "2020-01-01T18:01:00.123456789Z"),
93+
Pair("2020-01-01T00:01:01.010203040+17:59:59", "2019-12-31T06:01:02.010203040Z"),
94+
Pair("2020-01-01T00:01:01.010203040+17:59", "2019-12-31T06:02:01.010203040Z"),
95+
Pair("2020-01-01T00:01:01+00", "2020-01-01T00:01:01Z"),
9696
)
97-
instants.forEach {
98-
val (str, seconds, nanos) = it
97+
strings.forEach {
98+
val (str, strInZ) = it
9999
val instant = Instant.parse(str)
100-
assertEquals(nanos, instant.nanosecondsOfSecond, str)
101-
assertEquals(seconds, instant.epochSeconds, str)
100+
assertEquals(Instant.parse(strInZ), instant, str)
101+
assertEquals(strInZ, instant.toString(), str)
102102
}
103103
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01+18:01") }
104104
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01+1801") }
105105
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01+0") }
106+
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01+") }
107+
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01") }
106108
assertInvalidFormat { Instant.parse("2020-01-01T00:01:01+000000") }
109+
110+
val instants = listOf(
111+
Instant.DISTANT_FUTURE,
112+
Instant.DISTANT_PAST,
113+
Instant.fromEpochSeconds(0, 0))
114+
115+
val offsets = listOf(
116+
TimeZone.of("Z") as ZoneOffset,
117+
TimeZone.of("+03:12:14") as ZoneOffset,
118+
TimeZone.of("-03:12:14") as ZoneOffset,
119+
TimeZone.of("+02:35") as ZoneOffset,
120+
TimeZone.of("-02:35") as ZoneOffset,
121+
TimeZone.of("+04") as ZoneOffset,
122+
TimeZone.of("-04") as ZoneOffset,
123+
)
124+
125+
for (instant in instants) {
126+
for (offset in offsets) {
127+
val str = instant.toStringWithOffset(TimeZone.of("+03:12:14") as ZoneOffset)
128+
assertEquals(instant, Instant.parse(str))
129+
}
130+
}
107131
}
108132

109133
@OptIn(ExperimentalTime::class)

core/js/src/Instant.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,21 @@ public actual class Instant internal constructor(internal val value: jtInstant)
7777
}
7878

7979
public actual fun parse(isoString: String): Instant = try {
80-
Instant(jtOffsetDateTime.parse(isoString).toInstant())
80+
Instant(jtOffsetDateTime.parse(fixOffsetRepresentation(isoString)).toInstant())
8181
} catch (e: Throwable) {
8282
if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e)
8383
throw e
8484
}
8585

86+
/** A workaround for a bug where the string representations of Instant that have an offset of the form
87+
* "+XX" are not recognized by [jtOffsetDateTime.parse], while "+XX:XX" work fine.
88+
* See [the Github issue](https://github.com/js-joda/js-joda/issues/492). */
89+
private fun fixOffsetRepresentation(isoString: String): String {
90+
val time = isoString.split("T").elementAtOrNull(1) ?: return isoString
91+
val offset = time.split("+", "-").elementAtOrNull(1) ?: return isoString
92+
return if (offset.contains(":")) isoString else "$isoString:00"
93+
}
94+
8695
public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant = try {
8796
/* Performing normalization here because otherwise this fails:
8897
assertEquals((Long.MAX_VALUE % 1_000_000_000).toInt(),
@@ -211,3 +220,6 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
211220
} catch (e: Throwable) {
212221
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e) else throw e
213222
}
223+
224+
internal actual fun Instant.toStringWithOffset(offset: ZoneOffset): String =
225+
jtOffsetDateTime.ofInstant(this.value, offset.zoneId).toString()

core/jvm/src/Instant.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,5 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
171171
if (this.value < other.value) Long.MAX_VALUE else Long.MIN_VALUE
172172
}
173173

174+
internal actual fun Instant.toStringWithOffset(offset: ZoneOffset): String =
175+
jtOffsetDateTime.ofInstant(this.value, offset.zoneId).toString()

core/native/src/Instant.kt

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -185,67 +185,7 @@ public actual class Instant internal constructor(public actual val epochSeconds:
185185
(epochSeconds xor (epochSeconds ushr 32)).toInt() + 51 * nanosecondsOfSecond
186186

187187
// org.threeten.bp.format.DateTimeFormatterBuilder.InstantPrinterParser#print
188-
actual override fun toString(): String {
189-
val buf = StringBuilder()
190-
val inNano: Int = nanosecondsOfSecond
191-
if (epochSeconds >= -SECONDS_0000_TO_1970) { // current era
192-
val zeroSecs: Long = epochSeconds - SECONDS_PER_10000_YEARS + SECONDS_0000_TO_1970
193-
val hi: Long = floorDiv(zeroSecs, SECONDS_PER_10000_YEARS) + 1
194-
val lo: Long = floorMod(zeroSecs, SECONDS_PER_10000_YEARS)
195-
val ldt: LocalDateTime = Instant(lo - SECONDS_0000_TO_1970, 0)
196-
.toLocalDateTime(TimeZone.UTC)
197-
if (hi > 0) {
198-
buf.append('+').append(hi)
199-
}
200-
buf.append(ldt)
201-
if (ldt.second == 0) {
202-
buf.append(":00")
203-
}
204-
} else { // before current era
205-
val zeroSecs: Long = epochSeconds + SECONDS_0000_TO_1970
206-
val hi: Long = zeroSecs / SECONDS_PER_10000_YEARS
207-
val lo: Long = zeroSecs % SECONDS_PER_10000_YEARS
208-
val ldt: LocalDateTime = Instant(lo - SECONDS_0000_TO_1970, 0)
209-
.toLocalDateTime(TimeZone.UTC)
210-
val pos = buf.length
211-
buf.append(ldt)
212-
if (ldt.second == 0) {
213-
buf.append(":00")
214-
}
215-
if (hi < 0) {
216-
when {
217-
ldt.year == -10000 -> {
218-
buf.deleteAt(pos)
219-
buf.deleteAt(pos)
220-
buf.insert(pos, (hi - 1).toString())
221-
}
222-
lo == 0L -> {
223-
buf.insert(pos, hi)
224-
}
225-
else -> {
226-
buf.insert(pos + 1, abs(hi))
227-
}
228-
}
229-
}
230-
}
231-
//fraction
232-
if (inNano != 0) {
233-
buf.append('.')
234-
when {
235-
inNano % 1000000 == 0 -> {
236-
buf.append((inNano / 1000000 + 1000).toString().substring(1))
237-
}
238-
inNano % 1000 == 0 -> {
239-
buf.append((inNano / 1000 + 1000000).toString().substring(1))
240-
}
241-
else -> {
242-
buf.append((inNano + 1000000000).toString().substring(1))
243-
}
244-
}
245-
}
246-
buf.append('Z')
247-
return buf.toString()
248-
}
188+
actual override fun toString(): String = toStringWithOffset(ZoneOffset.UTC)
249189

250190
public actual companion object {
251191
internal actual val MIN = Instant(MIN_SECOND, 0)
@@ -378,3 +318,66 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
378318
until(other, unit)
379319
}
380320
}
321+
322+
internal actual fun Instant.toStringWithOffset(offset: ZoneOffset): String {
323+
val buf = StringBuilder()
324+
val inNano: Int = nanosecondsOfSecond
325+
val seconds = epochSeconds + offset.totalSeconds
326+
if (seconds >= -SECONDS_0000_TO_1970) { // current era
327+
val zeroSecs: Long = seconds - SECONDS_PER_10000_YEARS + SECONDS_0000_TO_1970
328+
val hi: Long = floorDiv(zeroSecs, SECONDS_PER_10000_YEARS) + 1
329+
val lo: Long = floorMod(zeroSecs, SECONDS_PER_10000_YEARS)
330+
val ldt: LocalDateTime = Instant(lo - SECONDS_0000_TO_1970, 0)
331+
.toLocalDateTime(TimeZone.UTC)
332+
if (hi > 0) {
333+
buf.append('+').append(hi)
334+
}
335+
buf.append(ldt)
336+
if (ldt.second == 0) {
337+
buf.append(":00")
338+
}
339+
} else { // before current era
340+
val zeroSecs: Long = seconds + SECONDS_0000_TO_1970
341+
val hi: Long = zeroSecs / SECONDS_PER_10000_YEARS
342+
val lo: Long = zeroSecs % SECONDS_PER_10000_YEARS
343+
val ldt: LocalDateTime = Instant(lo - SECONDS_0000_TO_1970, 0)
344+
.toLocalDateTime(TimeZone.UTC)
345+
val pos = buf.length
346+
buf.append(ldt)
347+
if (ldt.second == 0) {
348+
buf.append(":00")
349+
}
350+
if (hi < 0) {
351+
when {
352+
ldt.year == -10000 -> {
353+
buf.deleteAt(pos)
354+
buf.deleteAt(pos)
355+
buf.insert(pos, (hi - 1).toString())
356+
}
357+
lo == 0L -> {
358+
buf.insert(pos, hi)
359+
}
360+
else -> {
361+
buf.insert(pos + 1, abs(hi))
362+
}
363+
}
364+
}
365+
}
366+
//fraction
367+
if (inNano != 0) {
368+
buf.append('.')
369+
when {
370+
inNano % 1000000 == 0 -> {
371+
buf.append((inNano / 1000000 + 1000).toString().substring(1))
372+
}
373+
inNano % 1000 == 0 -> {
374+
buf.append((inNano / 1000 + 1000000).toString().substring(1))
375+
}
376+
else -> {
377+
buf.append((inNano + 1000000000).toString().substring(1))
378+
}
379+
}
380+
}
381+
buf.append(offset.id)
382+
return buf.toString()
383+
}

0 commit comments

Comments
 (0)