Skip to content

Commit fd78536

Browse files
committed
Isolate the implementation of Instant
1 parent f74fdab commit fd78536

File tree

2 files changed

+653
-0
lines changed

2 files changed

+653
-0
lines changed

core/native/src/Instant.kt

Lines changed: 256 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,261 @@ 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+
val epochSeconds = run {
168+
// org.threeten.bp.LocalDate#toEpochDay
169+
val epochDays = run {
170+
val y = year
171+
var total = 365 * y
172+
if (y >= 0) {
173+
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
174+
} else {
175+
total -= y / -4 - y / -100 + y / -400
176+
}
177+
total += ((367 * month - 362) / 12)
178+
total += day - 1
179+
if (month > 2) {
180+
total--
181+
if (!isLeapYear(year)) {
182+
total--
183+
}
184+
}
185+
total - DAYS_0000_TO_1970
186+
}
187+
// org.threeten.bp.LocalTime#toSecondOfDay
188+
val daySeconds = hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second
189+
// org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond
190+
epochDays * 86400L + daySeconds - offsetSeconds
191+
}
192+
if (epochSeconds < Instant.MIN.epochSeconds || epochSeconds > Instant.MAX.epochSeconds)
193+
throw DateTimeFormatException(
194+
"The parsed date is outside the range representable by Instant (Unix epoch second $epochSeconds)"
195+
)
196+
return Instant.fromEpochSeconds(epochSeconds, nanosecond)
197+
}
198+
199+
companion object {
200+
fun fromInstant(instant: Instant, offsetSeconds: Int): UnboundedLocalDateTime {
201+
val localSecond: Long = instant.epochSeconds + offsetSeconds
202+
val epochDays = localSecond.floorDiv(SECONDS_PER_DAY.toLong()).toInt()
203+
val secsOfDay = localSecond.mod(SECONDS_PER_DAY.toLong()).toInt()
204+
val year: Int
205+
val month: Int
206+
val day: Int
207+
// org.threeten.bp.LocalDate#toEpochDay
208+
run {
209+
var zeroDay = epochDays + DAYS_0000_TO_1970
210+
// find the march-based year
211+
zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle
212+
213+
var adjust = 0
214+
if (zeroDay < 0) { // adjust negative years to positive for calculation
215+
val adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1
216+
adjust = adjustCycles * 400
217+
zeroDay += -adjustCycles * DAYS_PER_CYCLE
218+
}
219+
var yearEst = ((400 * zeroDay.toLong() + 591) / DAYS_PER_CYCLE).toInt()
220+
var doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
221+
if (doyEst < 0) { // fix estimate
222+
yearEst--
223+
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
224+
}
225+
yearEst += adjust // reset any negative year
226+
227+
val marchDoy0 = doyEst
228+
229+
// convert march-based values back to january-based
230+
val marchMonth0 = (marchDoy0 * 5 + 2) / 153
231+
month = (marchMonth0 + 2) % 12 + 1
232+
day = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1
233+
year = yearEst + marchMonth0 / 10
234+
}
235+
val hours = (secsOfDay / SECONDS_PER_HOUR)
236+
val secondWithoutHours = secsOfDay - hours * SECONDS_PER_HOUR
237+
val minutes = (secondWithoutHours / SECONDS_PER_MINUTE)
238+
val second = secondWithoutHours - minutes * SECONDS_PER_MINUTE
239+
return UnboundedLocalDateTime(year, month, day, hours, minutes, second, instant.nanosecondsOfSecond)
240+
}
241+
}
242+
}
243+
244+
internal fun parseIso(isoString: String): Instant {
245+
fun parseFailure(error: String): Nothing {
246+
throw IllegalArgumentException("$error when parsing an Instant from $isoString")
247+
}
248+
inline fun expect(what: String, where: Int, predicate: (Char) -> Boolean) {
249+
val c = isoString[where]
250+
if (!predicate(c)) {
251+
parseFailure("Expected $what, but got $c at position $where")
252+
}
253+
}
254+
val s = isoString
255+
var i = 0
256+
require(s.isNotEmpty()) { "An empty string is not a valid Instant" }
257+
val yearSign = when (val c = s[i]) {
258+
'+', '-' -> { ++i; c }
259+
else -> ' '
260+
}
261+
val yearStart = i
262+
var absYear = 0
263+
while (i < s.length && s[i] in '0'..'9') {
264+
absYear = absYear * 10 + (s[i] - '0')
265+
++i
266+
}
267+
val year = when {
268+
i > yearStart + 9 -> {
269+
parseFailure("Expected at most 9 digits for the year number, got ${i - yearStart}")
270+
}
271+
i - yearStart < 4 -> {
272+
parseFailure("The year number must be padded to 4 digits, got ${i - yearStart} digits")
273+
}
274+
else -> {
275+
if (yearSign == '+' && i - yearStart == 4) {
276+
parseFailure("The '+' sign at the start is only valid for year numbers longer than 4 digits")
277+
}
278+
if (yearSign == ' ' && i - yearStart != 4) {
279+
parseFailure("A '+' or '-' sign is required for year numbers longer than 4 digits")
280+
}
281+
if (yearSign == '-') -absYear else absYear
282+
}
283+
}
284+
// reading at least -MM-DDTHH:MM:SSZ
285+
// 0123456789012345 16 chars
286+
if (s.length < i + 16) {
287+
parseFailure("The input string is too short")
288+
}
289+
expect("'-'", i) { it == '-' }
290+
expect("'-'", i + 3) { it == '-' }
291+
expect("'T' or 't'", i + 6) { it == 'T' || it == 't' }
292+
expect("':'", i + 9) { it == ':' }
293+
expect("':'", i + 12) { it == ':' }
294+
for (j in listOf(1, 2, 4, 5, 7, 8, 10, 11, 13, 14)) {
295+
expect("an ASCII digit", i + j) { it in '0'..'9' }
296+
}
297+
fun twoDigitNumber(index: Int) = s[index].code * 10 + s[index + 1].code - '0'.code * 11
298+
val month = twoDigitNumber(i + 1)
299+
val day = twoDigitNumber(i + 4)
300+
val hour = twoDigitNumber(i + 7)
301+
val minute = twoDigitNumber(i + 10)
302+
val second = twoDigitNumber(i + 13)
303+
val nanosecond = if (s[i + 15] == '.') {
304+
val fractionStart = i + 16
305+
i = fractionStart
306+
var fraction = 0
307+
while (i < s.length && s[i] in '0'..'9') {
308+
fraction = fraction * 10 + (s[i] - '0')
309+
++i
310+
}
311+
if (i - fractionStart in 1..9) {
312+
fraction * POWERS_OF_TEN[fractionStart + 9 - i]
313+
} else {
314+
parseFailure("1..9 digits are supported for the fraction of the second, got {i - fractionStart}")
315+
}
316+
} else {
317+
i += 15
318+
0
319+
}
320+
val offsetSeconds = when (val sign = s.getOrNull(i)) {
321+
null -> {
322+
parseFailure("The UTC offset at the end of the string is missing")
323+
}
324+
'z', 'Z' -> if (s.length == i + 1) {
325+
0
326+
} else {
327+
parseFailure("Extra text after the instant at position ${i + 1}")
328+
}
329+
'-', '+' -> {
330+
val offsetStrLength = s.length - i
331+
if (offsetStrLength % 3 != 0) { parseFailure("Invalid UTC offset string '${s.substring(i)}'") }
332+
if (offsetStrLength > 9) { parseFailure("The UTC offset string '${s.substring(i)}' is too long") }
333+
for (j in listOf(3, 6)) {
334+
if (s.getOrNull(i + j) ?: break != ':')
335+
parseFailure("Expected ':' at index ${i + j}, got '${s[i + j]}'")
336+
}
337+
for (j in listOf(1, 2, 4, 5, 7, 8)) {
338+
if (s.getOrNull(i + j) ?: break !in '0'..'9')
339+
parseFailure("Expected a digit at index ${i + j}, got '${s[i + j]}'")
340+
}
341+
val offsetHour = twoDigitNumber(i + 1)
342+
val offsetMinute = if (offsetStrLength > 3) { twoDigitNumber(i + 4) } else { 0 }
343+
val offsetSecond = if (offsetStrLength > 6) { twoDigitNumber(i + 7) } else { 0 }
344+
if (offsetMinute > 59) { parseFailure("Expected offset-minute-of-hour in 0..59, got $offsetMinute") }
345+
if (offsetSecond > 59) { parseFailure("Expected offset-second-of-minute in 0..59, got $offsetSecond") }
346+
if (offsetHour > 17 && !(offsetHour == 18 && offsetMinute == 0 && offsetSecond == 0)) {
347+
parseFailure("Expected an offset in -18:00..+18:00, got $sign$offsetHour:$offsetMinute:$offsetSecond")
348+
}
349+
(offsetHour * 3600 + offsetMinute * 60 + offsetSecond) * if (sign == '-') -1 else 1
350+
}
351+
else -> {
352+
parseFailure("Expected the UTC offset at position $i, got '$sign'")
353+
}
354+
}
355+
if (month !in 1..12) { parseFailure("Expected a month number in 1..12, got $month") }
356+
if (day !in 1..month.monthLength(isLeapYear(year))) {
357+
parseFailure("Expected a valid day-of-month for $year-$month, got $day")
358+
}
359+
if (hour > 23) { parseFailure("Expected hour in 0..23, got $hour") }
360+
if (minute > 59) { parseFailure("Expected minute-of-hour in 0..59, got $minute") }
361+
if (second > 59) { parseFailure("Expected second-of-minute in 0..59, got $second") }
362+
return UnboundedLocalDateTime(year, month, day, hour, minute, second, nanosecond).toInstant(offsetSeconds)
363+
}
364+
365+
internal fun formatIso(instant: Instant): String = buildString {
366+
val ldt = UnboundedLocalDateTime.fromInstant(instant, 0)
367+
fun Appendable.appendTwoDigits(number: Int) {
368+
if (number < 10) append('0')
369+
append(number)
370+
}
371+
run {
372+
val number = ldt.year
373+
when {
374+
number.absoluteValue < 1_000 -> {
375+
val innerBuilder = StringBuilder()
376+
if (number >= 0) {
377+
innerBuilder.append((number + 10_000)).deleteAt(0)
378+
} else {
379+
innerBuilder.append((number - 10_000)).deleteAt(1)
380+
}
381+
append(innerBuilder)
382+
}
383+
else -> {
384+
if (number >= 10_000) append('+')
385+
append(number)
386+
}
387+
}
388+
}
389+
append('-')
390+
appendTwoDigits(ldt.month)
391+
append('-')
392+
appendTwoDigits(ldt.day)
393+
append('T')
394+
appendTwoDigits(ldt.hour)
395+
append(':')
396+
appendTwoDigits(ldt.minute)
397+
append(':')
398+
appendTwoDigits(ldt.second)
399+
if (ldt.nanosecond != 0) {
400+
append('.')
401+
var zerosToStrip = 0
402+
while (ldt.nanosecond % POWERS_OF_TEN[zerosToStrip + 1] == 0) {
403+
++zerosToStrip
404+
}
405+
zerosToStrip -= (zerosToStrip.mod(3)) // rounding down to a multiple of 3
406+
val numberToOutput = ldt.nanosecond / POWERS_OF_TEN[zerosToStrip]
407+
append((numberToOutput + POWERS_OF_TEN[9 - zerosToStrip]).toString().substring(1))
408+
}
409+
append('Z')
410+
}
411+
156412
private fun Instant.toZonedDateTimeFailing(zone: TimeZone): ZonedDateTime = try {
157413
toZonedDateTime(zone)
158414
} catch (e: IllegalArgumentException) {

0 commit comments

Comments
 (0)