Skip to content

Commit c768602

Browse files
committed
WIP: isolate the implementation of Instant
1 parent bc8adee commit c768602

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed

core/native/src/Instant.kt

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.serialization.Serializable
1515
import kotlin.time.*
1616
import kotlin.time.Duration.Companion.nanoseconds
1717
import kotlin.time.Duration.Companion.seconds
18+
import kotlin.math.absoluteValue
1819

1920
public actual enum class DayOfWeek {
2021
MONDAY,
@@ -153,6 +154,253 @@ public actual class Instant internal constructor(public actual val epochSeconds:
153154

154155
}
155156

157+
private class UnboundedLocalDateTime(
158+
val year: Int,
159+
val month: Int,
160+
val day: Int,
161+
val hour: Int,
162+
val minute: Int,
163+
val second: Int,
164+
val nanosecond: Int,
165+
) {
166+
fun toInstant(offsetSeconds: Int): Instant {
167+
fun isLeapYear(year: Int): Boolean =
168+
year and 3 == 0 && (year % 100 != 0 || year % 400 == 0)
169+
val epochSeconds = run {
170+
// org.threeten.bp.LocalDate#toEpochDay
171+
val epochDays = run {
172+
val y = year
173+
var total = 365 * y
174+
if (y >= 0) {
175+
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
176+
} else {
177+
total -= y / -4 - y / -100 + y / -400
178+
}
179+
total += ((367 * month - 362) / 12)
180+
total += day - 1
181+
if (month > 2) {
182+
total--
183+
if (!isLeapYear(year)) {
184+
total--
185+
}
186+
}
187+
total - DAYS_0000_TO_1970
188+
}
189+
// org.threeten.bp.LocalTime#toSecondOfDay
190+
val daySeconds = hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second
191+
// org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond
192+
epochDays * 86400L + daySeconds - offsetSeconds
193+
}
194+
return Instant.fromEpochSeconds(epochSeconds, nanosecond)
195+
}
196+
197+
companion object {
198+
fun fromInstant(instant: Instant, offsetSeconds: Int): UnboundedLocalDateTime {
199+
val localSecond: Long = instant.epochSeconds + offsetSeconds
200+
val epochDays = localSecond.floorDiv(SECONDS_PER_DAY.toLong()).toInt()
201+
val secsOfDay = localSecond.mod(SECONDS_PER_DAY.toLong()).toInt()
202+
val year: Int
203+
val month: Int
204+
val day: Int
205+
// org.threeten.bp.LocalDate#toEpochDay
206+
run {
207+
var zeroDay = epochDays + DAYS_0000_TO_1970
208+
// find the march-based year
209+
zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle
210+
211+
var adjust = 0
212+
if (zeroDay < 0) { // adjust negative years to positive for calculation
213+
val adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1
214+
adjust = adjustCycles * 400
215+
zeroDay += -adjustCycles * DAYS_PER_CYCLE
216+
}
217+
var yearEst = ((400 * zeroDay.toLong() + 591) / DAYS_PER_CYCLE).toInt()
218+
var doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
219+
if (doyEst < 0) { // fix estimate
220+
yearEst--
221+
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
222+
}
223+
yearEst += adjust // reset any negative year
224+
225+
val marchDoy0 = doyEst
226+
227+
// convert march-based values back to january-based
228+
val marchMonth0 = (marchDoy0 * 5 + 2) / 153
229+
month = (marchMonth0 + 2) % 12 + 1
230+
day = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1
231+
year = yearEst + marchMonth0 / 10
232+
}
233+
val hours = (secsOfDay / SECONDS_PER_HOUR)
234+
val secondWithoutHours = secsOfDay - hours * SECONDS_PER_HOUR
235+
val minutes = (secondWithoutHours / SECONDS_PER_MINUTE)
236+
val second = secondWithoutHours - minutes * SECONDS_PER_MINUTE
237+
return UnboundedLocalDateTime(year, month, day, hours, minutes, second, instant.nanosecondsOfSecond)
238+
}
239+
}
240+
}
241+
242+
internal fun parseIso(isoString: String): Instant {
243+
val s = isoString
244+
var i = 0
245+
if (s.isEmpty()) {
246+
TODO()
247+
}
248+
val yearSign = when (s[i]) {
249+
'+' -> { ++i; '+' }
250+
'-' -> { ++i; '-' }
251+
else -> ' '
252+
}
253+
val yearStart = i
254+
var absYear = 0
255+
while (i < s.length && s[i] in '0'..'9') {
256+
absYear = absYear * 10 + (s[i] - '0')
257+
++i
258+
}
259+
val year = when {
260+
i - yearStart > 9 -> {
261+
TODO()
262+
}
263+
i - yearStart < 4 -> {
264+
TODO()
265+
}
266+
else -> {
267+
if (yearSign == '+' && i - yearStart == 4) {
268+
TODO()
269+
}
270+
if (yearSign == ' ' && i - yearStart != 4) {
271+
TODO()
272+
}
273+
if (yearSign == '-') -absYear else absYear
274+
}
275+
}
276+
// reading at least -MM-DDTHH:MM:SSZ
277+
// 0123456789012345 16 chars
278+
if (s.length < i + 16) {
279+
TODO()
280+
}
281+
if (s[i] != '-') { TODO() }
282+
if (s[i + 3] != '-') { TODO() }
283+
if (s[i + 6] != 'T' && s[i + 6] != 't') { TODO() }
284+
if (s[i + 9] != ':') { TODO() }
285+
if (s[i + 12] != ':') { TODO() }
286+
for (j in listOf(1, 2, 4, 5, 7, 8, 10, 11, 13, 14)) {
287+
if (s[i + j] !in '0'..'9') {
288+
TODO()
289+
}
290+
}
291+
fun twoDigitNumber(index: Int) = s[index].code * 10 + s[index + 1].code - '0'.code * 11
292+
val month = twoDigitNumber(i + 1)
293+
val day = twoDigitNumber(i + 4)
294+
val hour = twoDigitNumber(i + 7)
295+
val minute = twoDigitNumber(i + 10)
296+
val second = twoDigitNumber(i + 13)
297+
val nanosecond = if (s[i + 15] == '.') {
298+
val fractionStart = i + 16
299+
i = fractionStart
300+
var fraction = 0
301+
while (i < s.length && s[i] in '0'..'9') {
302+
fraction = fraction * 10 + (s[i] - '0')
303+
++i
304+
}
305+
if (i <= fractionStart + 9) {
306+
fraction * POWERS_OF_TEN[fractionStart + 9 - i]
307+
} else {
308+
TODO()
309+
}
310+
} else {
311+
i += 15
312+
0
313+
}
314+
val offsetSeconds = when (val sign = s.getOrNull(i)) {
315+
null -> {
316+
TODO()
317+
}
318+
'z', 'Z' -> if (s.length == i + 1) {
319+
0
320+
} else {
321+
TODO()
322+
}
323+
'-', '+' -> {
324+
val offsetStrLength = s.length - i
325+
if (offsetStrLength % 3 != 0) { TODO() }
326+
if (offsetStrLength > 9) { TODO() }
327+
for (j in listOf(3, 6)) {
328+
if (s.getOrNull(i + j) ?: break != ':')
329+
TODO("Expected ':' at index ${i + j}, got '${s[i + j]}' when parsing instant from $s")
330+
}
331+
for (j in listOf(1, 2, 4, 5, 7, 8)) {
332+
if (s.getOrNull(i + j) ?: break !in '0'..'9')
333+
TODO("Expected a digit at index ${i + j}, got '${s[i + j]}' when parsing instant from $s")
334+
}
335+
val offsetHour = twoDigitNumber(i + 1)
336+
val offsetMinute = if (offsetStrLength > 3) { twoDigitNumber(i + 4) } else { 0 }
337+
val offsetSecond = if (offsetStrLength > 6) { twoDigitNumber(i + 7) } else { 0 }
338+
if (offsetMinute !in 0..59) { TODO("Expected offset-minute-of-hour in 0..59, got $offsetMinute when parsing instant from $s") }
339+
if (offsetSecond !in 0..59) { TODO("Expected offset-second-of-minute in 0..59, got $offsetSecond when parsing instant from $s") }
340+
if (offsetHour !in 0..17 && !(offsetHour == 18 && offsetMinute == 0 && offsetMinute == 0)) {
341+
TODO()
342+
}
343+
(offsetHour * 3600 + offsetMinute * 60 + offsetSecond) * if (sign == '-') -1 else 1
344+
}
345+
else -> {
346+
TODO()
347+
}
348+
}
349+
if (month !in 1..12) { TODO() }
350+
if (day !in 1..month.monthLength(isLeapYear(year))) { TODO() }
351+
if (hour !in 0..23) { TODO() }
352+
if (minute !in 0..59) { TODO() }
353+
if (second !in 0..59) { TODO() }
354+
return UnboundedLocalDateTime(year, month, day, hour, minute, second, nanosecond).toInstant(offsetSeconds)
355+
}
356+
357+
internal fun formatIso(instant: Instant): String = buildString {
358+
val ldt = UnboundedLocalDateTime.fromInstant(instant, 0)
359+
fun Appendable.appendTwoDigits(number: Int) {
360+
if (number < 10) append('0')
361+
append(number)
362+
}
363+
run {
364+
val number = ldt.year
365+
when {
366+
number.absoluteValue < 1_000 -> {
367+
val innerBuilder = StringBuilder()
368+
if (number >= 0) {
369+
innerBuilder.append((number + 10_000)).deleteAt(0)
370+
} else {
371+
innerBuilder.append((number - 10_000)).deleteAt(1)
372+
}
373+
append(innerBuilder)
374+
}
375+
else -> {
376+
if (number >= 10_000) append('+')
377+
append(number)
378+
}
379+
}
380+
}
381+
append('-')
382+
appendTwoDigits(ldt.month)
383+
append('-')
384+
appendTwoDigits(ldt.day)
385+
append('T')
386+
appendTwoDigits(ldt.hour)
387+
append(':')
388+
appendTwoDigits(ldt.minute)
389+
append(':')
390+
appendTwoDigits(ldt.second)
391+
if (ldt.nanosecond != 0) {
392+
append('.')
393+
var zerosToStrip = 0
394+
while (ldt.nanosecond % POWERS_OF_TEN[zerosToStrip + 1] == 0) {
395+
++zerosToStrip
396+
}
397+
zerosToStrip -= (zerosToStrip.mod(3)) // rounding down to a multiple of 3
398+
val numberToOutput = ldt.nanosecond / POWERS_OF_TEN[zerosToStrip]
399+
append((numberToOutput + POWERS_OF_TEN[9 - zerosToStrip]).toString().substring(1))
400+
}
401+
append('Z')
402+
}
403+
156404
private fun Instant.toZonedDateTimeFailing(zone: TimeZone): ZonedDateTime = try {
157405
toZonedDateTime(zone)
158406
} catch (e: IllegalArgumentException) {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
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
7+
8+
import kotlin.test.*
9+
10+
class InstantIsoStringsTest {
11+
12+
/* Based on the ThreeTenBp project.
13+
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
14+
*/
15+
@Test
16+
fun parseIsoString() {
17+
val instants = arrayOf(
18+
Triple("1970-01-01T00:00:00Z", 0, 0),
19+
Triple("1970-01-01t00:00:00Z", 0, 0),
20+
Triple("1970-01-01T00:00:00z", 0, 0),
21+
Triple("1970-01-01T00:00:00.0Z", 0, 0),
22+
Triple("1970-01-01T00:00:00.000000000Z", 0, 0),
23+
Triple("1970-01-01T00:00:00.000000001Z", 0, 1),
24+
Triple("1970-01-01T00:00:00.100000000Z", 0, 100000000),
25+
Triple("1970-01-01T00:00:01Z", 1, 0),
26+
Triple("1970-01-01T00:01:00Z", 60, 0),
27+
Triple("1970-01-01T00:01:01Z", 61, 0),
28+
Triple("1970-01-01T00:01:01.000000001Z", 61, 1),
29+
Triple("1970-01-01T01:00:00.000000000Z", 3600, 0),
30+
Triple("1970-01-01T01:01:01.000000001Z", 3661, 1),
31+
Triple("1970-01-02T01:01:01.100000000Z", 90061, 100000000))
32+
instants.forEach {
33+
val (str, seconds, nanos) = it
34+
val instant = parseIso(str)
35+
assertEquals(seconds.toLong() * 1000 + nanos / 1000000, instant.toEpochMilliseconds())
36+
}
37+
38+
assertInvalidFormat { parseIso("1970-01-01T23:59:60Z")}
39+
assertInvalidFormat { parseIso("1970-01-01T24:00:00Z")}
40+
assertInvalidFormat { parseIso("1970-01-01T23:59Z")}
41+
assertInvalidFormat { parseIso("x") }
42+
assertInvalidFormat { parseIso("12020-12-31T23:59:59.000000000Z") }
43+
// this string represents an Instant that is currently larger than Instant.MAX any of the implementations:
44+
assertInvalidFormat { parseIso("+1000000001-12-31T23:59:59.000000000Z") }
45+
}
46+
47+
@Test
48+
fun parseStringsWithOffsets() {
49+
val strings = arrayOf(
50+
Pair("2020-01-01T00:01:01.02+18:00", "2019-12-31T06:01:01.020Z"),
51+
Pair("2020-01-01T00:01:01.123456789-17:59:59", "2020-01-01T18:01:00.123456789Z"),
52+
Pair("2020-01-01T00:01:01.010203040+17:59:59", "2019-12-31T06:01:02.010203040Z"),
53+
Pair("2020-01-01T00:01:01.010203040+17:59", "2019-12-31T06:02:01.010203040Z"),
54+
Pair("2020-01-01T00:01:01+00", "2020-01-01T00:01:01Z"),
55+
)
56+
strings.forEach { (str, strInZ) ->
57+
val instant = parseIso(str)
58+
assertEquals(parseIso(strInZ), instant, str)
59+
assertEquals(strInZ, formatIso(instant), str)
60+
}
61+
assertInvalidFormat { parseIso("2020-01-01T00:01:01+18:01") }
62+
assertInvalidFormat { parseIso("2020-01-01T00:01:01+1801") }
63+
assertInvalidFormat { parseIso("2020-01-01T00:01:01+0") }
64+
assertInvalidFormat { parseIso("2020-01-01T00:01:01+") }
65+
assertInvalidFormat { parseIso("2020-01-01T00:01:01") }
66+
assertInvalidFormat { parseIso("2020-01-01T00:01:01+000000") }
67+
68+
val instants = listOf(
69+
Instant.DISTANT_FUTURE,
70+
Instant.DISTANT_PAST,
71+
Instant.fromEpochSeconds(0, 0))
72+
73+
/*
74+
val offsets = listOf(
75+
0 to "Z",
76+
3 * 3600 + 12 * 60 + 14 to "+03:12:14",
77+
- 3 * 3600 - 12 * 60 - 14 to "-03:12:14",
78+
2 * 3600 + 35 * 60 to "+02:35",
79+
- 2 * 3600 - 35 * 60 to "-02:35",
80+
4 * 3600 to "+04",
81+
- 4 * 3600 to "-04",
82+
)
83+
84+
for (instant in instants) {
85+
for ((offsetSeconds, offsetString) in offsets) {
86+
val str = instant.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET, offsets[offsetIx])
87+
val offsetString = offsets[offsetIx].toString()
88+
assertEquals(offsetString, offsetString.commonSuffixWith(str))
89+
assertEquals(instant, Instant.parse(str, DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET))
90+
assertEquals(instant, Instant.parse(str))
91+
}
92+
}
93+
94+
*/
95+
}
96+
}
97+
98+
99+
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
100+
@kotlin.internal.InlineOnly
101+
private fun <T> assertInvalidFormat(message: String? = null, f: () -> T) {
102+
assertFailsWith<NotImplementedError>(message) {
103+
val result = f()
104+
fail(result.toString())
105+
}
106+
}

0 commit comments

Comments
 (0)