Skip to content

Commit 9a4d5ca

Browse files
committed
Isolate the implementation of Instant
1 parent e1a2c61 commit 9a4d5ca

File tree

2 files changed

+653
-0
lines changed

2 files changed

+653
-0
lines changed

core/commonKotlin/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,
@@ -168,6 +169,261 @@ public actual class Instant internal constructor(public actual val epochSeconds:
168169

169170
}
170171

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

0 commit comments

Comments
 (0)