@@ -15,6 +15,7 @@ import kotlinx.serialization.Serializable
15
15
import kotlin.time.*
16
16
import kotlin.time.Duration.Companion.nanoseconds
17
17
import kotlin.time.Duration.Companion.seconds
18
+ import kotlin.math.absoluteValue
18
19
19
20
public actual enum class DayOfWeek {
20
21
MONDAY ,
@@ -168,6 +169,261 @@ public actual class Instant internal constructor(public actual val epochSeconds:
168
169
169
170
}
170
171
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
+
171
427
private fun Instant.toZonedDateTimeFailing (zone : TimeZone ): ZonedDateTime = try {
172
428
toZonedDateTime(zone)
173
429
} catch (e: IllegalArgumentException ) {
0 commit comments