@@ -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 ,
@@ -153,6 +154,261 @@ public actual class Instant internal constructor(public actual val epochSeconds:
153
154
154
155
}
155
156
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
+
156
412
private fun Instant.toZonedDateTimeFailing (zone : TimeZone ): ZonedDateTime = try {
157
413
toZonedDateTime(zone)
158
414
} catch (e: IllegalArgumentException ) {
0 commit comments